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