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