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   +render-loop-initialization
136   color:num <- copy 7/white
137   row:num <- copy 1/top
138   column:num <- copy left
139   cursor-row:num <- get *editor, cursor-row:offset
140   cursor-column:num <- get *editor, cursor-column:offset
141   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
142   screen <- move-cursor screen, row, column
143   {
144     +next-character
145     break-unless curr
146     off-screen?:bool <- greater-or-equal row, screen-height
147     break-if off-screen?
148     # update editor.before-cursor
149     # Doing so at the start of each iteration ensures it stays one step behind
150     # the current character.
151     {
152       at-cursor-row?:bool <- equal row, cursor-row
153       break-unless at-cursor-row?
154       at-cursor?:bool <- equal column, cursor-column
155       break-unless at-cursor?
156       before-cursor <- copy prev
157     }
158     c:char <- get *curr, value:offset
159     <character-c-received>
160     {
161       # newline? move to left rather than 0
162       newline?:bool <- equal c, 10/newline
163       break-unless newline?
164       # adjust cursor if necessary
165       {
166         at-cursor-row?:bool <- equal row, cursor-row
167         break-unless at-cursor-row?
168         left-of-cursor?:bool <- lesser-than column, cursor-column
169         break-unless left-of-cursor?
170         cursor-column <- copy column
171         before-cursor <- prev curr
172       }
173       # clear rest of line in this window
174       clear-line-until screen, right
175       # skip to next line
176       row <- add row, 1
177       column <- copy left
178       screen <- move-cursor screen, row, column
179       curr <- next curr
180       prev <- next prev
181       loop +next-character
182     }
183     {
184       # at right? wrap. even if there's only one more letter left; we need
185       # room for clicking on the cursor after it.
186       at-right?:bool <- equal column, right
187       break-unless at-right?
188       # print wrap icon
189       wrap-icon:char <- copy 8617/loop-back-to-left
190       print screen, wrap-icon, 245/grey
191       column <- copy left
192       row <- add row, 1
193       screen <- move-cursor screen, row, column
194       # don't increment curr
195       loop +next-character
196     }
197     print screen, c, color
198     curr <- next curr
199     prev <- next prev
200     column <- add column, 1
201     loop
202   }
203   # save first character off-screen
204   *editor <- put *editor, bottom-of-screen:offset, curr
205   # is cursor to the right of the last line? move to end
206   {
207     at-cursor-row?:bool <- equal row, cursor-row
208     cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
209     before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
210     above-cursor-row?:bool <- lesser-than row, cursor-row
211     before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
212     break-unless before-cursor?
213     cursor-row <- copy row
214     cursor-column <- copy column
215     before-cursor <- copy prev
216   }
217   *editor <- put *editor, bottom:offset, row
218   *editor <- put *editor, cursor-row:offset, cursor-row
219   *editor <- put *editor, cursor-column:offset, cursor-column
220   *editor <- put *editor, before-cursor:offset, before-cursor
221   return row, column
222 ]
223 
224 def clear-screen-from screen:&:screen, row:num, column:num, left:num, right:num -> screen:&:screen [
225   local-scope
226   load-ingredients
227   # if it's the real screen, use the optimized primitive
228   {
229     break-if screen
230     clear-display-from row, column, left, right
231     return
232   }
233   # if not, go the slower route
234   screen <- move-cursor screen, row, column
235   clear-line-until screen, right
236   clear-rest-of-screen screen, row, left, right
237 ]
238 
239 def clear-rest-of-screen screen:&:screen, row:num, left:num, right:num -> screen:&:screen [
240   local-scope
241   load-ingredients
242   row <- add row, 1
243   screen <- move-cursor screen, row, left
244   screen-height:num <- screen-height screen
245   {
246     at-bottom-of-screen?:bool <- greater-or-equal row, screen-height
247     break-if at-bottom-of-screen?
248     screen <- move-cursor screen, row, left
249     clear-line-until screen, right
250     row <- add row, 1
251     loop
252   }
253 ]
254 
255 scenario editor-prints-multiple-lines [
256   local-scope
257   assume-screen 5/width, 5/height
258   s:text <- new [abc
259 def]
260   e:&:editor <- new-editor s, 0/left, 5/right
261   run [
262     render screen, e
263   ]
264   screen-should-contain [
265     .     .
266     .abc  .
267     .def  .
268     .     .
269   ]
270 ]
271 
272 scenario editor-handles-offsets [
273   local-scope
274   assume-screen 5/width, 5/height
275   e:&:editor <- new-editor [abc], 1/left, 5/right
276   run [
277     render screen, e
278   ]
279   screen-should-contain [
280     .     .
281     . abc .
282     .     .
283   ]
284 ]
285 
286 scenario editor-prints-multiple-lines-at-offset [
287   local-scope
288   assume-screen 5/width, 5/height
289   s:text <- new [abc
290 def]
291   e:&:editor <- new-editor s, 1/left, 5/right
292   run [
293     render screen, e
294   ]
295   screen-should-contain [
296     .     .
297     . abc .
298     . def .
299     .     .
300   ]
301 ]
302 
303 scenario editor-wraps-long-lines [
304   local-scope
305   assume-screen 5/width, 5/height
306   e:&:editor <- new-editor [abc def], 0/left, 5/right
307   run [
308     render screen, e
309   ]
310   screen-should-contain [
311     .     .
312     .abc ↩.
313     .def  .
314     .     .
315   ]
316   screen-should-contain-in-color 245/grey [
317     .     .
318     .    ↩.
319     .     .
320     .     .
321   ]
322 ]
323 
324 scenario editor-wraps-barely-long-lines [
325   local-scope
326   assume-screen 5/width, 5/height
327   e:&:editor <- new-editor [abcde], 0/left, 5/right
328   run [
329     render screen, e
330   ]
331   # still wrap, even though the line would fit. We need room to click on the
332   # end of the line
333   screen-should-contain [
334     .     .
335     .abcd↩.
336     .e    .
337     .     .
338   ]
339   screen-should-contain-in-color 245/grey [
340     .     .
341     .    ↩.
342     .     .
343     .     .
344   ]
345 ]
346 
347 scenario editor-with-empty-text [
348   local-scope
349   assume-screen 5/width, 5/height
350   e:&:editor <- new-editor [], 0/left, 5/right
351   run [
352     render screen, e
353     3:num/raw <- get *e, cursor-row:offset
354     4:num/raw <- get *e, cursor-column:offset
355   ]
356   screen-should-contain [
357     .     .
358     .     .
359     .     .
360   ]
361   memory-should-contain [
362     3 <- 1  # cursor row
363     4 <- 0  # cursor column
364   ]
365 ]
366 
367 # just a little color for Mu code
368 
369 scenario render-colors-comments [
370   local-scope
371   assume-screen 5/width, 5/height
372   s:text <- new [abc
373 # de
374 f]
375   e:&:editor <- new-editor s, 0/left, 5/right
376   run [
377     render screen, e
378   ]
379   screen-should-contain [
380     .     .
381     .abc  .
382     .# de .
383     .f    .
384     .     .
385   ]
386   screen-should-contain-in-color 12/lightblue, [
387     .     .
388     .     .
389     .# de .
390     .     .
391     .     .
392   ]
393   screen-should-contain-in-color 7/white, [
394     .     .
395     .abc  .
396     .     .
397     .f    .
398     .     .
399   ]
400 ]
401 
402 after <character-c-received> [
403   color <- get-color color, c
404 ]
405 
406 # so far the previous color is all the information we need; that may change
407 def get-color color:num, c:char -> color:num [
408   local-scope
409   load-ingredients
410   color-is-white?:bool <- equal color, 7/white
411   # if color is white and next character is '#', switch color to blue
412   {
413     break-unless color-is-white?
414     starting-comment?:bool <- equal c, 35/#
415     break-unless starting-comment?
416     trace 90, [app], [switch color back to blue]
417     color <- copy 12/lightblue
418     jump +exit
419   }
420   # if color is blue and next character is newline, switch color to white
421   {
422     color-is-blue?:bool <- equal color, 12/lightblue
423     break-unless color-is-blue?
424     ending-comment?:bool <- equal c, 10/newline
425     break-unless ending-comment?
426     trace 90, [app], [switch color back to white]
427     color <- copy 7/white
428     jump +exit
429   }
430   # if color is white (no comments) and next character is '<', switch color to red
431   {
432     break-unless color-is-white?
433     starting-assignment?:bool <- equal c, 60/<
434     break-unless starting-assignment?
435     color <- copy 1/red
436     jump +exit
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     color <- copy 7/white
445     jump +exit
446   }
447   # otherwise no change
448   +exit
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 ]