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