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