1 ## the basic editor data structure, and how it displays text to the screen
  2 
  3 # temporary main for this layer: just render the given text at the given
  4 # screen dimensions, then stop
  5 def main text:text [
  6   local-scope
  7   load-ingredients
  8   open-console
  9   clear-screen 0/screen  # non-scrolling app
 10   e:&:editor <- new-editor text, 0/left, 5/right
 11   render 0/screen, e
 12   wait-for-event 0/console
 13   close-console
 14 ]
 15 
 16 scenario editor-renders-text-to-screen [
 17   local-scope
 18   assume-screen 10/width, 5/height
 19   e:&:editor <- new-editor [abc], 0/left, 10/right
 20   run [
 21   ¦ render screen, e
 22   ]
 23   screen-should-contain [
 24   ¦ # top line of screen reserved for menu
 25   ¦ .          .
 26   ¦ .abc       .
 27   ¦ .          .
 28   ]
 29 ]
 30 
 31 container editor [
 32   # editable text: doubly linked list of characters (head contains a special sentinel)
 33   data:&:duplex-list:char
 34   top-of-screen:&:duplex-list:char
 35   bottom-of-screen:&:duplex-list:char
 36   # location before cursor inside data
 37   before-cursor:&:duplex-list:char
 38 
 39   # raw bounds of display area on screen
 40   # always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen
 41   left:num
 42   right:num
 43   bottom:num
 44   # raw screen coordinates of cursor
 45   cursor-row:num
 46   cursor-column:num
 47 ]
 48 
 49 # creates a new editor widget
 50 #   right is exclusive
 51 def new-editor s:text, left:num, right:num -> result:&:editor [
 52   local-scope
 53   load-ingredients
 54   # no clipping of bounds
 55   right <- subtract right, 1
 56   result <- new editor:type
 57   # initialize screen-related fields
 58   *result <- put *result, left:offset, left
 59   *result <- put *result, right:offset, right
 60   # initialize cursor coordinates
 61   *result <- put *result, cursor-row:offset, 1/top
 62   *result <- put *result, cursor-column:offset, left
 63   # initialize empty contents
 64   init:&:duplex-list:char <- push 167/§, 0/tail
 65   *result <- put *result, data:offset, init
 66   *result <- put *result, top-of-screen:offset, init
 67   *result <- put *result, before-cursor:offset, init
 68   result <- insert-text result, s
 69   <editor-initialization>
 70 ]
 71 
 72 def insert-text editor:&:editor, text:text -> editor:&:editor [
 73   local-scope
 74   load-ingredients
 75   curr:&:duplex-list:char <- get *editor, data:offset
 76   insert curr, text
 77 ]
 78 
 79 scenario editor-initializes-without-data [
 80   local-scope
 81   assume-screen 5/width, 3/height
 82   run [
 83   ¦ e:&:editor <- new-editor 0/data, 2/left, 5/right
 84   ¦ 2:editor/raw <- copy *e
 85   ]
 86   memory-should-contain [
 87   ¦ # 2 (data) <- just the § sentinel
 88   ¦ # 3 (top of screen) <- the § sentinel
 89   ¦ 4 <- 0  # bottom-of-screen; null since text fits on screen
 90   ¦ # 5 (before cursor) <- the § sentinel
 91   ¦ 6 <- 2  # left
 92   ¦ 7 <- 4  # right  (inclusive)
 93   ¦ 8 <- 0  # bottom (not set until render)
 94   ¦ 9 <- 1  # cursor row
 95   ¦ 10 <- 2  # cursor column
 96   ]
 97   screen-should-contain [
 98   ¦ .     .
 99   ¦ .     .
100   ¦ .     .
101   ]
102 ]
103 
104 # Assumes cursor should be at coordinates (cursor-row, cursor-column) and
105 # updates before-cursor to match. Might also move coordinates if they're
106 # outside text.
107 def render screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
108   local-scope
109   load-ingredients
110   return-unless editor, 1/top, 0/left
111   left:num <- get *editor, left:offset
112   screen-height:num <- screen-height screen
113   right:num <- get *editor, right:offset
114   # traversing editor
115   curr:&:duplex-list:char <- get *editor, top-of-screen:offset
116   prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
117   curr <- next curr
118   # traversing screen
119   color:num <- copy 7/white
120   row:num <- copy 1/top
121   column:num <- copy left
122   cursor-row:num <- get *editor, cursor-row:offset
123   cursor-column:num <- get *editor, cursor-column:offset
124   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
125   screen <- move-cursor screen, row, column
126   {
127   ¦ +next-character
128   ¦ break-unless curr
129   ¦ off-screen?:bool <- greater-or-equal row, screen-height
130   ¦ break-if off-screen?
131   ¦ # update editor.before-cursor
132   ¦ # Doing so at the start of each iteration ensures it stays one step behind
133   ¦ # the current character.
134   ¦ {
135   ¦ ¦ at-cursor-row?:bool <- equal row, cursor-row
136   ¦ ¦ break-unless at-cursor-row?
137   ¦ ¦ at-cursor?:bool <- equal column, cursor-column
138   ¦ ¦ break-unless at-cursor?
139   ¦ ¦ before-cursor <- copy prev
140   ¦ }
141   ¦ c:char <- get *curr, value:offset
142   ¦ <character-c-received>
143   ¦ {
144   ¦ ¦ # newline? move to left rather than 0
145   ¦ ¦ newline?:bool <- equal c, 10/newline
146   ¦ ¦ break-unless newline?
147   ¦ ¦ # adjust cursor if necessary
148   ¦ ¦ {
149   ¦ ¦ ¦ at-cursor-row?:bool <- equal row, cursor-row
150   ¦ ¦ ¦ break-unless at-cursor-row?
151   ¦ ¦ ¦ left-of-cursor?:bool <- lesser-than column, cursor-column
152   ¦ ¦ ¦ break-unless left-of-cursor?
153   ¦ ¦ ¦ cursor-column <- copy column
154   ¦ ¦ ¦ before-cursor <- prev curr
155   ¦ ¦ }
156   ¦ ¦ # clear rest of line in this window
157   ¦ ¦ clear-line-until screen, right
158   ¦ ¦ # skip to next line
159   ¦ ¦ row <- add row, 1
160   ¦ ¦ column <- copy left
161   ¦ ¦ screen <- move-cursor screen, row, column
162   ¦ ¦ curr <- next curr
163   ¦ ¦ prev <- next prev
164   ¦ ¦ loop +next-character
165   ¦ }
166   ¦ {
167   ¦ ¦ # at right? wrap. even if there's only one more letter left; we need
168   ¦ ¦ # room for clicking on the cursor after it.
169   ¦ ¦ at-right?:bool <- equal column, right
170   ¦ ¦ break-unless at-right?
171   ¦ ¦ # print wrap icon
172   ¦ ¦ wrap-icon:char <- copy 8617/loop-back-to-left
173   ¦ ¦ print screen, wrap-icon, 245/grey
174   ¦ ¦ column <- copy left
175   ¦ ¦ row <- add row, 1
176   ¦ ¦ screen <- move-cursor screen, row, column
177   ¦ ¦ # don't increment curr
178   ¦ ¦ loop +next-character
179   ¦ }
180   ¦ print screen, c, color
181   ¦ curr <- next curr
182   ¦ prev <- next prev
183   ¦ column <- add column, 1
184   ¦ loop
185   }
186   # save first character off-screen
187   *editor <- put *editor, bottom-of-screen:offset, curr
188   # is cursor to the right of the last line? move to end
189   {
190   ¦ at-cursor-row?:bool <- equal row, cursor-row
191   ¦ cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
192   ¦ before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
193   ¦ above-cursor-row?:bool <- lesser-than row, cursor-row
194   ¦ before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
195   ¦ break-unless before-cursor?
196   ¦ cursor-row <- copy row
197   ¦ cursor-column <- copy column
198   ¦ before-cursor <- copy prev
199   }
200   *editor <- put *editor, bottom:offset, row
201   *editor <- put *editor, cursor-row:offset, cursor-row
202   *editor <- put *editor, cursor-column:offset, cursor-column
203   *editor <- put *editor, before-cursor:offset, before-cursor
204   return row, column
205 ]
206 
207 def clear-screen-from screen:&:screen, row:num, column:num, left:num, right:num -> screen:&:screen [
208   local-scope
209   load-ingredients
210   # if it's the real screen, use the optimized primitive
211   {
212   ¦ break-if screen
213   ¦ clear-display-from row, column, left, right
214   ¦ return
215   }
216   # if not, go the slower route
217   screen <- move-cursor screen, row, column
218   clear-line-until screen, right
219   clear-rest-of-screen screen, row, left, right
220 ]
221 
222 def clear-rest-of-screen screen:&:screen, row:num, left:num, right:num -> screen:&:screen [
223   local-scope
224   load-ingredients
225   row <- add row, 1
226   # if it's the real screen, use the optimized primitive
227   {
228   ¦ break-if screen
229   ¦ clear-display-from row, left, left, right
230   ¦ return
231   }
232   screen <- move-cursor screen, row, left
233   screen-height:num <- screen-height screen
234   {
235   ¦ at-bottom-of-screen?:bool <- greater-or-equal row, screen-height
236   ¦ break-if at-bottom-of-screen?
237   ¦ screen <- move-cursor screen, row, left
238   ¦ clear-line-until screen, right
239   ¦ row <- add row, 1
240   ¦ loop
241   }
242 ]
243 
244 scenario editor-prints-multiple-lines [
245   local-scope
246   assume-screen 5/width, 5/height
247   s:text <- new [abc
248 def]
249   e:&:editor <- new-editor s, 0/left, 5/right
250   run [
251   ¦ render screen, e
252   ]
253   screen-should-contain [
254   ¦ .     .
255   ¦ .abc  .
256   ¦ .def  .
257   ¦ .     .
258   ]
259 ]
260 
261 scenario editor-handles-offsets [
262   local-scope
263   assume-screen 5/width, 5/height
264   e:&:editor <- new-editor [abc], 1/left, 5/right
265   run [
266   ¦ render screen, e
267   ]
268   screen-should-contain [
269   ¦ .     .
270   ¦ . abc .
271   ¦ .     .
272   ]
273 ]
274 
275 scenario editor-prints-multiple-lines-at-offset [
276   local-scope
277   assume-screen 5/width, 5/height
278   s:text <- new [abc
279 def]
280   e:&:editor <- new-editor s, 1/left, 5/right
281   run [
282   ¦ render screen, e
283   ]
284   screen-should-contain [
285   ¦ .     .
286   ¦ . abc .
287   ¦ . def .
288   ¦ .     .
289   ]
290 ]
291 
292 scenario editor-wraps-long-lines [
293   local-scope
294   assume-screen 5/width, 5/height
295   e:&:editor <- new-editor [abc def], 0/left, 5/right
296   run [
297   ¦ render screen, e
298   ]
299   screen-should-contain [
300   ¦ .     .
301   ¦ .abc ↩.
302   ¦ .def  .
303   ¦ .     .
304   ]
305   screen-should-contain-in-color 245/grey [
306   ¦ .     .
307   ¦ .    ↩.
308   ¦ .     .
309   ¦ .     .
310   ]
311 ]
312 
313 scenario editor-wraps-barely-long-lines [
314   local-scope
315   assume-screen 5/width, 5/height
316   e:&:editor <- new-editor [abcde], 0/left, 5/right
317   run [
318   ¦ render screen, e
319   ]
320   # still wrap, even though the line would fit. We need room to click on the
321   # end of the line
322   screen-should-contain [
323   ¦ .     .
324   ¦ .abcd↩.
325   ¦ .e    .
326   ¦ .     .
327   ]
328   screen-should-contain-in-color 245/grey [
329   ¦ .     .
330   ¦ .    ↩.
331   ¦ .     .
332   ¦ .     .
333   ]
334 ]
335 
336 scenario editor-with-empty-text [
337   local-scope
338   assume-screen 5/width, 5/height
339   e:&:editor <- new-editor [], 0/left, 5/right
340   run [
341   ¦ render screen, e
342   ¦ 3:num/raw <- get *e, cursor-row:offset
343   ¦ 4:num/raw <- get *e, cursor-column:offset
344   ]
345   screen-should-contain [
346   ¦ .     .
347   ¦ .     .
348   ¦ .     .
349   ]
350   memory-should-contain [
351   ¦ 3 <- 1  # cursor row
352   ¦ 4 <- 0  # cursor column
353   ]
354 ]
355 
356 # just a little color for Mu code
357 
358 scenario render-colors-comments [
359   local-scope
360   assume-screen 5/width, 5/height
361   s:text <- new [abc
362 # de
363 f]
364   e:&:editor <- new-editor s, 0/left, 5/right
365   run [
366   ¦ render screen, e
367   ]
368   screen-should-contain [
369   ¦ .     .
370   ¦ .abc  .
371   ¦ .# de .
372   ¦ .f    .
373   ¦ .     .
374   ]
375   screen-should-contain-in-color 12/lightblue, [
376   ¦ .     .
377   ¦ .     .
378   ¦ .# de .
379   ¦ .     .
380   ¦ .     .
381   ]
382   screen-should-contain-in-color 7/white, [
383   ¦ .     .
384   ¦ .abc  .
385   ¦ .     .
386   ¦ .f    .
387   ¦ .     .
388   ]
389 ]
390 
391 after <character-c-received> [
392   color <- get-color color, c
393 ]
394 
395 # so far the previous color is all the information we need; that may change
396 def get-color color:num, c:char -> color:num [
397   local-scope
398   load-ingredients
399   color-is-white?:bool <- equal color, 7/white
400   # if color is white and next character is '#', switch color to blue
401   {
402   ¦ break-unless color-is-white?
403   ¦ starting-comment?:bool <- equal c, 35/#
404   ¦ break-unless starting-comment?
405   ¦ trace 90, [app], [switch color back to blue]
406   ¦ return 12/lightblue
407   }
408   # if color is blue and next character is newline, switch color to white
409   {
410   ¦ color-is-blue?:bool <- equal color, 12/lightblue
411   ¦ break-unless color-is-blue?
412   ¦ ending-comment?:bool <- equal c, 10/newline
413   ¦ break-unless ending-comment?
414   ¦ trace 90, [app], [switch color back to white]
415   ¦ return 7/white
416   }
417   # if color is white (no comments) and next character is '<', switch color to red
418   {
419   ¦ break-unless color-is-white?
420   ¦ starting-assignment?:bool <- equal c, 60/<
421   ¦ break-unless starting-assignment?
422   ¦ return 1/red
423   }
424   # if color is red and next character is space, switch color to white
425   {
426   ¦ color-is-red?:bool <- equal color, 1/red
427   ¦ break-unless color-is-red?
428   ¦ ending-assignment?:bool <- equal c, 32/space
429   ¦ break-unless ending-assignment?
430   ¦ return 7/white
431   }
432   # otherwise no change
433   return color
434 ]
435 
436 scenario render-colors-assignment [
437   local-scope
438   assume-screen 8/width, 5/height
439   s:text <- new [abc
440 d <- e
441 f]
442   e:&:editor <- new-editor s, 0/left, 8/right
443   run [
444   ¦ render screen, e
445   ]
446   screen-should-contain [
447   ¦ .        .
448   ¦ .abc     .
449   ¦ .d <- e  .
450   ¦ .f       .
451   ¦ .        .
452   ]
453   screen-should-contain-in-color 1/red, [
454   ¦ .        .
455   ¦ .        .
456   ¦ .  <-    .
457   ¦ .        .
458   ¦ .        .
459   ]
460 ]