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