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   new-editor text, 0/left, 5/right
 11   show-screen 0/screen
 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   +render-loop-initialization
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   screen <- move-cursor screen, row, left
243   screen-height:num <- screen-height screen
244   {
245     at-bottom-of-screen?:bool <- greater-or-equal row, screen-height
246     break-if at-bottom-of-screen?
247     screen <- move-cursor screen, row, left
248     clear-line-until screen, right
249     row <- add row, 1
250     loop
251   }
252 ]
253 
254 scenario editor-prints-multiple-lines [
255   local-scope
256   assume-screen 5/width, 5/height
257   s:text <- new [abc
258 def]
259   e:&:editor <- new-editor s, 0/left, 5/right
260   run [
261     render screen, e
262   ]
263   screen-should-contain [
264     .     .
265     .abc  .
266     .def  .
267     .     .
268   ]
269 ]
270 
271 scenario editor-handles-offsets [
272   local-scope
273   assume-screen 5/width, 5/height
274   e:&:editor <- new-editor [abc], 1/left, 5/right
275   run [
276     render screen, e
277   ]
278   screen-should-contain [
279     .     .
280     . abc .
281     .     .
282   ]
283 ]
284 
285 scenario editor-prints-multiple-lines-at-offset [
286   local-scope
287   assume-screen 5/width, 5/height
288   s:text <- new [abc
289 def]
290   e:&:editor <- new-editor s, 1/left, 5/right
291   run [
292     render screen, e
293   ]
294   screen-should-contain [
295     .     .
296     . abc .
297     . def .
298     .     .
299   ]
300 ]
301 
302 scenario editor-wraps-long-lines [
303   local-scope
304   assume-screen 5/width, 5/height
305   e:&:editor <- new-editor [abc def], 0/left, 5/right
306   run [
307     render screen, e
308   ]
309   screen-should-contain [
310     .     .
311     .abc ↩.
312     .def  .
313     .     .
314   ]
315   screen-should-contain-in-color 245/grey [
316     .     .
317     .    ↩.
318     .     .
319     .     .
320   ]
321 ]
322 
323 scenario editor-wraps-barely-long-lines [
324   local-scope
325   assume-screen 5/width, 5/height
326   e:&:editor <- new-editor [abcde], 0/left, 5/right
327   run [
328     render screen, e
329   ]
330   # still wrap, even though the line would fit. We need room to click on the
331   # end of the line
332   screen-should-contain [
333     .     .
334     .abcd↩.
335     .e    .
336     .     .
337   ]
338   screen-should-contain-in-color 245/grey [
339     .     .
340     .    ↩.
341     .     .
342     .     .
343   ]
344 ]
345 
346 scenario editor-with-empty-text [
347   local-scope
348   assume-screen 5/width, 5/height
349   e:&:editor <- new-editor [], 0/left, 5/right
350   run [
351     render screen, e
352     3:num/raw <- get *e, cursor-row:offset
353     4:num/raw <- get *e, cursor-column:offset
354   ]
355   screen-should-contain [
356     .     .
357     .     .
358     .     .
359   ]
360   memory-should-contain [
361     3 <- 1  # cursor row
362     4 <- 0  # cursor column
363   ]
364 ]
365 
366 # just a little color for Mu code
367 
368 scenario render-colors-comments [
369   local-scope
370   assume-screen 5/width, 5/height
371   s:text <- new [abc
372 # de
373 f]
374   e:&:editor <- new-editor s, 0/left, 5/right
375   run [
376     render screen, e
377   ]
378   screen-should-contain [
379     .     .
380     .abc  .
381     .# de .
382     .f    .
383     .     .
384   ]
385   screen-should-contain-in-color 12/lightblue, [
386     .     .
387     .     .
388     .# de .
389     .     .
390     .     .
391   ]
392   screen-should-contain-in-color 7/white, [
393     .     .
394     .abc  .
395     .     .
396     .f    .
397     .     .
398   ]
399 ]
400 
401 after <character-c-received> [
402   color <- get-color color, c
403 ]
404 
405 # so far the previous color is all the information we need; that may change
406 def get-color color:num, c:char -> color:num [
407   local-scope
408   load-ingredients
409   color-is-white?:bool <- equal color, 7/white
410   # if color is white and next character is '#', switch color to blue
411   {
412     break-unless color-is-white?
413     starting-comment?:bool <- equal c, 35/#
414     break-unless starting-comment?
415     trace 90, [app], [switch color back to blue]
416     color <- copy 12/lightblue
417     jump +exit
418   }
419   # if color is blue and next character is newline, switch color to white
420   {
421     color-is-blue?:bool <- equal color, 12/lightblue
422     break-unless color-is-blue?
423     ending-comment?:bool <- equal c, 10/newline
424     break-unless ending-comment?
425     trace 90, [app], [switch color back to white]
426     color <- copy 7/white
427     jump +exit
428   }
429   # if color is white (no comments) and next character is '<', switch color to red
430   {
431     break-unless color-is-white?
432     starting-assignment?:bool <- equal c, 60/<
433     break-unless starting-assignment?
434     color <- copy 1/red
435     jump +exit
436   }
437   # if color is red and next character is space, switch color to white
438   {
439     color-is-red?:bool <- equal color, 1/red
440     break-unless color-is-red?
441     ending-assignment?:bool <- equal c, 32/space
442     break-unless ending-assignment?
443     color <- copy 7/white
444     jump +exit
445   }
446   # otherwise no change
447   +exit
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 ]