1 -- text editor, particularly text drawing, horizontal wrap, vertical scrolling
4 -- draw a line starting from startpos to screen at y between State.left and State.right
5 -- return y for the next line, and position of start of final screen line drawn
6 function Text
.draw(State
, line_index
, y
, startpos
, hide_cursor
)
7 local line
= State
.lines
[line_index
]
8 local line_cache
= State
.line_cache
[line_index
]
10 line_cache
.startpos
= startpos
12 local final_screen_line_starting_pos
= startpos
-- track value to return
13 Text
.populate_screen_line_starting_pos(State
, line_index
)
14 Text
.populate_link_offsets(State
, line_index
)
16 assert(#line_cache
.screen_line_starting_pos
>= 1)
17 for i
=1,#line_cache
.screen_line_starting_pos
do
18 local pos
= line_cache
.screen_line_starting_pos
[i
]
19 if pos
< startpos
then
21 --? print('skipping', screen_line)
23 final_screen_line_starting_pos
= pos
24 local screen_line
= Text
.screen_line(line
, line_cache
, i
)
25 --? print('text.draw:', screen_line, 'at', line_index,pos, 'after', x,y)
26 local frag_len
= utf8
.len(screen_line
)
27 -- render any highlights
28 for _
,link_offsets
in ipairs(line_cache
.link_offsets
) do
29 -- render link decorations
30 local s
,e
,filename
= unpack(link_offsets
)
31 local lo
, hi
= Text
.clip_wikiword_with_screen_line(line
, line_cache
, i
, s
, e
)
33 button(State
, 'link', {x
=State
.left
+lo
, y
=y
, w
=hi
-lo
, h
=State
.line_height
, color
={1,1,1},
34 icon
= icon
.hyperlink_decoration
,
36 if file_exists(filename
) then
37 source
.switch_to_file(filename
)
43 if State
.selection1
.line
then
44 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
45 Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, lo
,hi
)
47 if not hide_cursor
and line_index
== State
.cursor1
.line
then
48 -- render search highlight or cursor
49 if State
.search_term
then
50 local data
= State
.lines
[State
.cursor1
.line
].data
51 local cursor_offset
= Text
.offset(data
, State
.cursor1
.pos
)
52 if data
:sub(cursor_offset
, cursor_offset
+#State
.search_term
-1) == State
.search_term
then
53 local save_selection
= State
.selection1
54 State
.selection1
= {line
=line_index
, pos
=State
.cursor1
.pos
+utf8
.len(State
.search_term
)}
55 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
56 Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, lo
,hi
)
57 State
.selection1
= save_selection
59 elseif Focus
== 'edit' then
60 if pos
<= State
.cursor1
.pos
and pos
+ frag_len
> State
.cursor1
.pos
then
61 Text
.draw_cursor(State
, State
.left
+Text
.x(screen_line
, State
.cursor1
.pos
-pos
+1), y
)
62 elseif pos
+ frag_len
== State
.cursor1
.pos
then
63 -- Show cursor at end of line.
64 -- This place also catches end of wrapping screen lines. That doesn't seem worth distinguishing.
65 -- It seems useful to see a cursor whether your eye is on the left or right margin.
66 Text
.draw_cursor(State
, State
.left
+Text
.x(screen_line
, State
.cursor1
.pos
-pos
+1), y
)
70 -- render colorized text
72 for frag
in screen_line
:gmatch('%S*%s*') do
74 App
.screen
.print(frag
, x
,y
)
77 y
= y
+ State
.line_height
78 if y
>= App
.screen
.height
then
83 return y
, final_screen_line_starting_pos
86 function Text
.screen_line(line
, line_cache
, i
)
87 local pos
= line_cache
.screen_line_starting_pos
[i
]
88 local offset
= Text
.offset(line
.data
, pos
)
89 if i
>= #line_cache
.screen_line_starting_pos
then
90 return line
.data
:sub(offset
)
92 local endpos
= line_cache
.screen_line_starting_pos
[i
+1]-1
93 local end_offset
= Text
.offset(line
.data
, endpos
)
94 return line
.data
:sub(offset
, end_offset
)
97 function Text
.draw_cursor(State
, x
, y
)
99 if math
.floor(Cursor_time
*2)%2 == 0 then
100 App
.color(Cursor_color
)
101 love
.graphics
.rectangle('fill', x
,y
, 3,State
.line_height
)
104 State
.cursor_y
= y
+State
.line_height
107 function Text
.populate_screen_line_starting_pos(State
, line_index
)
108 local line
= State
.lines
[line_index
]
109 if line
.mode
~= 'text' then return end
110 local line_cache
= State
.line_cache
[line_index
]
111 if line_cache
.screen_line_starting_pos
then
114 line_cache
.screen_line_starting_pos
= {1}
117 -- try to wrap at word boundaries
118 for frag
in line
.data
:gmatch('%S*%s*') do
119 local frag_width
= App
.width(frag
)
120 --? print('-- frag:', frag, pos, x, frag_width, State.width)
121 while x
+ frag_width
> State
.width
do
122 --? print('frag:', frag, pos, x, frag_width, State.width)
123 if x
< 0.8 * State
.width
then
124 -- long word; chop it at some letter
125 -- We're not going to reimplement TeX here.
126 local bpos
= Text
.nearest_pos_less_than(frag
, State
.width
- x
)
127 -- everything works if bpos == 0, but is a little inefficient
129 local boffset
= Text
.offset(frag
, bpos
+1) -- byte _after_ bpos
130 frag
= string.sub(frag
, boffset
)
132 --? print('after chop:', frag)
134 frag_width
= App
.width(frag
)
136 --? print('screen line:', pos)
137 table.insert(line_cache
.screen_line_starting_pos
, pos
)
138 x
= 0 -- new screen line
141 pos
= pos
+ utf8
.len(frag
)
145 function Text
.populate_link_offsets(State
, line_index
)
146 local line
= State
.lines
[line_index
]
147 if line
.mode
~= 'text' then return end
148 local line_cache
= State
.line_cache
[line_index
]
149 if line_cache
.link_offsets
then
152 line_cache
.link_offsets
= {}
154 -- try to wrap at word boundaries
156 while s
<= #line
.data
do
157 s
, e
= line
.data
:find('%[%[%S+%]%]', s
)
158 if s
== nil then break end
159 local word
= line
.data
:sub(s
+2, e
-2) -- strip out surrounding '[[..]]'
160 --? print('wikiword:', s, e, word)
161 table.insert(line_cache
.link_offsets
, {s
, e
, word
})
166 -- Intersect the filename between byte offsets s,e with the bounds of screen line i.
167 -- Return the left/right pixel coordinates of of the intersection,
168 -- or nil if it doesn't intersect with screen line i.
169 function Text
.clip_wikiword_with_screen_line(line
, line_cache
, i
, s
, e
)
170 local spos
= line_cache
.screen_line_starting_pos
[i
]
171 local soff
= Text
.offset(line
.data
, spos
)
176 if i
< #line_cache
.screen_line_starting_pos
then
177 local epos
= line_cache
.screen_line_starting_pos
[i
+1]
178 eoff
= Text
.offset(line
.data
, epos
)
183 local loff
= math
.max(s
, soff
)
186 hoff
= math
.min(e
, eoff
)
190 --? print(s, e, soff, eoff, loff, hoff)
191 return App
.width(line
.data
:sub(soff
, loff
-1)), App
.width(line
.data
:sub(soff
, hoff
))
194 function Text
.text_input(State
, t
)
195 if App
.mouse_down(1) then return end
196 if App
.ctrl_down() or App
.alt_down() or App
.cmd_down() then return end
197 local before
= snapshot(State
, State
.cursor1
.line
)
198 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
199 Text
.insert_at_cursor(State
, t
)
200 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
201 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
202 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
204 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
207 function Text
.insert_at_cursor(State
, t
)
208 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
209 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
210 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_offset
-1)..t
..string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_offset
)
211 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
212 State
.cursor1
.pos
= State
.cursor1
.pos
+1
215 -- Don't handle any keys here that would trigger text_input above.
216 function Text
.keychord_press(State
, chord
)
217 --? print('chord', chord, State.selection1.line, State.selection1.pos)
218 --== shortcuts that mutate text
219 if chord
== 'return' then
220 local before_line
= State
.cursor1
.line
221 local before
= snapshot(State
, before_line
)
222 Text
.insert_return(State
)
223 State
.selection1
= {}
224 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
225 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
228 record_undo_event(State
, {before
=before
, after
=snapshot(State
, before_line
, State
.cursor1
.line
)})
229 elseif chord
== 'tab' then
230 local before
= snapshot(State
, State
.cursor1
.line
)
231 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
232 Text
.insert_at_cursor(State
, '\t')
233 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
234 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
235 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
236 --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
239 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
240 elseif chord
== 'backspace' then
241 if State
.selection1
.line
then
242 Text
.delete_selection(State
, State
.left
, State
.right
)
247 if State
.cursor1
.pos
> 1 then
248 before
= snapshot(State
, State
.cursor1
.line
)
249 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1)
250 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
253 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)..string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_end
)
255 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
257 State
.cursor1
.pos
= State
.cursor1
.pos
-1
259 elseif State
.cursor1
.line
> 1 then
260 before
= snapshot(State
, State
.cursor1
.line
-1, State
.cursor1
.line
)
261 if State
.lines
[State
.cursor1
.line
-1].mode
== 'drawing' then
262 table.remove(State
.lines
, State
.cursor1
.line
-1)
263 table.remove(State
.line_cache
, State
.cursor1
.line
-1)
266 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
-1].data
)+1
267 State
.lines
[State
.cursor1
.line
-1].data
= State
.lines
[State
.cursor1
.line
-1].data
..State
.lines
[State
.cursor1
.line
].data
268 table.remove(State
.lines
, State
.cursor1
.line
)
269 table.remove(State
.line_cache
, State
.cursor1
.line
)
271 State
.cursor1
.line
= State
.cursor1
.line
-1
273 if State
.screen_top1
.line
> #State
.lines
then
274 Text
.populate_screen_line_starting_pos(State
, #State
.lines
)
275 local line_cache
= State
.line_cache
[#State
.line_cache
]
276 State
.screen_top1
= {line
=#State
.lines
, pos
=line_cache
.screen_line_starting_pos
[#line_cache
.screen_line_starting_pos
]}
277 elseif Text
.lt1(State
.cursor1
, State
.screen_top1
) then
278 State
.screen_top1
= {
279 line
=State
.cursor1
.line
,
280 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
282 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
284 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
285 assert(Text
.le1(State
.screen_top1
, State
.cursor1
))
287 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
288 elseif chord
== 'delete' then
289 if State
.selection1
.line
then
290 Text
.delete_selection(State
, State
.left
, State
.right
)
295 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
296 before
= snapshot(State
, State
.cursor1
.line
)
298 before
= snapshot(State
, State
.cursor1
.line
, State
.cursor1
.line
+1)
300 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
301 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
302 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
+1)
305 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)..string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_end
)
307 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
309 -- no change to State.cursor1.pos
311 elseif State
.cursor1
.line
< #State
.lines
then
312 if State
.lines
[State
.cursor1
.line
+1].mode
== 'text' then
314 State
.lines
[State
.cursor1
.line
].data
= State
.lines
[State
.cursor1
.line
].data
..State
.lines
[State
.cursor1
.line
+1].data
316 table.remove(State
.lines
, State
.cursor1
.line
+1)
317 table.remove(State
.line_cache
, State
.cursor1
.line
+1)
319 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
321 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
322 --== shortcuts that move the cursor
323 elseif chord
== 'left' then
325 State
.selection1
= {}
326 elseif chord
== 'right' then
328 State
.selection1
= {}
329 elseif chord
== 'S-left' then
330 if State
.selection1
.line
== nil then
331 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
334 elseif chord
== 'S-right' then
335 if State
.selection1
.line
== nil then
336 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
339 -- C- hotkeys reserved for drawings, so we'll use M-
340 elseif chord
== 'M-left' then
341 Text
.word_left(State
)
342 State
.selection1
= {}
343 elseif chord
== 'M-right' then
344 Text
.word_right(State
)
345 State
.selection1
= {}
346 elseif chord
== 'M-S-left' then
347 if State
.selection1
.line
== nil then
348 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
350 Text
.word_left(State
)
351 elseif chord
== 'M-S-right' then
352 if State
.selection1
.line
== nil then
353 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
355 Text
.word_right(State
)
356 elseif chord
== 'home' then
357 Text
.start_of_line(State
)
358 State
.selection1
= {}
359 elseif chord
== 'end' then
360 Text
.end_of_line(State
)
361 State
.selection1
= {}
362 elseif chord
== 'S-home' then
363 if State
.selection1
.line
== nil then
364 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
366 Text
.start_of_line(State
)
367 elseif chord
== 'S-end' then
368 if State
.selection1
.line
== nil then
369 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
371 Text
.end_of_line(State
)
372 elseif chord
== 'up' then
374 State
.selection1
= {}
375 elseif chord
== 'down' then
377 State
.selection1
= {}
378 elseif chord
== 'S-up' then
379 if State
.selection1
.line
== nil then
380 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
383 elseif chord
== 'S-down' then
384 if State
.selection1
.line
== nil then
385 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
388 elseif chord
== 'pageup' then
390 State
.selection1
= {}
391 elseif chord
== 'pagedown' then
393 State
.selection1
= {}
394 elseif chord
== 'S-pageup' then
395 if State
.selection1
.line
== nil then
396 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
399 elseif chord
== 'S-pagedown' then
400 if State
.selection1
.line
== nil then
401 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
407 function Text
.insert_return(State
)
408 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
409 table.insert(State
.lines
, State
.cursor1
.line
+1, {mode
='text', data
=string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_offset
)})
410 table.insert(State
.line_cache
, State
.cursor1
.line
+1, {})
411 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_offset
-1)
412 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
413 State
.cursor1
= {line
=State
.cursor1
.line
+1, pos
=1}
416 function Text
.pageup(State
)
418 -- duplicate some logic from love.draw
419 local top2
= Text
.to2(State
, State
.screen_top1
)
420 --? print(App.screen.height)
421 local y
= App
.screen
.height
- State
.line_height
422 while y
>= State
.top
do
423 --? print(y, top2.line, top2.screen_line, top2.screen_pos)
424 if State
.screen_top1
.line
== 1 and State
.screen_top1
.pos
== 1 then break end
425 if State
.lines
[State
.screen_top1
.line
].mode
== 'text' then
426 y
= y
- State
.line_height
427 elseif State
.lines
[State
.screen_top1
.line
].mode
== 'drawing' then
428 y
= y
- Drawing_padding_height
- Drawing
.pixels(State
.lines
[State
.screen_top1
.line
].h
, State
.width
)
430 top2
= Text
.previous_screen_line(State
, top2
)
432 State
.screen_top1
= Text
.to1(State
, top2
)
433 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
434 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
435 --? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
436 --? print('pageup end')
439 function Text
.pagedown(State
)
440 --? print('pagedown')
441 State
.screen_top1
= {line
=State
.screen_bottom1
.line
, pos
=State
.screen_bottom1
.pos
}
442 --? print('setting top to', State.screen_top1.line, State.screen_top1.pos)
443 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
444 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
445 --? print('top now', State.screen_top1.line)
446 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
447 --? print('pagedown end')
450 function Text
.up(State
)
451 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
452 --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
453 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
454 if screen_line_starting_pos
== 1 then
455 --? print('cursor is at first screen line of its line')
456 -- line is done; skip to previous text line
457 local new_cursor_line
= State
.cursor1
.line
458 while new_cursor_line
> 1 do
459 new_cursor_line
= new_cursor_line
-1
460 if State
.lines
[new_cursor_line
].mode
== 'text' then
461 --? print('found previous text line')
462 State
.cursor1
= {line
=new_cursor_line
, pos
=nil}
463 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
464 -- previous text line found, pick its final screen line
465 --? print('has multiple screen lines')
466 local screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
467 --? print(#screen_line_starting_pos)
468 screen_line_starting_pos
= screen_line_starting_pos
[#screen_line_starting_pos
]
469 local screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_pos
)
470 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_byte_offset
)
471 State
.cursor1
.pos
= screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
476 -- move up one screen line in current line
477 assert(screen_line_index
> 1)
478 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
-1]
479 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
480 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
481 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
482 --? print('cursor pos is now '..tostring(State.cursor1.pos))
484 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
485 State
.screen_top1
= {
486 line
=State
.cursor1
.line
,
487 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
489 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
493 function Text
.down(State
)
494 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
495 --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
496 assert(State
.cursor1
.pos
)
497 if Text
.cursor_at_final_screen_line(State
) then
498 -- line is done, skip to next text line
499 --? print('cursor at final screen line of its line')
500 local new_cursor_line
= State
.cursor1
.line
501 while new_cursor_line
< #State
.lines
do
502 new_cursor_line
= new_cursor_line
+1
503 if State
.lines
[new_cursor_line
].mode
== 'text' then
505 line
= new_cursor_line
,
506 pos
= Text
.nearest_cursor_pos(State
.lines
[new_cursor_line
].data
, State
.cursor_x
, State
.left
),
508 --? print(State.cursor1.pos)
512 if State
.cursor1
.line
> State
.screen_bottom1
.line
then
513 --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
514 --? print('scroll up preserving cursor')
515 Text
.snap_cursor_to_bottom_of_screen(State
)
516 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
519 -- move down one screen line in current line
520 local scroll_down
= Text
.le1(State
.screen_bottom1
, State
.cursor1
)
521 --? print('cursor is NOT at final screen line of its line')
522 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
523 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
524 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
+1]
525 --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
526 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
527 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
528 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
529 --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
531 --? print('scroll up preserving cursor')
532 Text
.snap_cursor_to_bottom_of_screen(State
)
533 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
536 --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
539 function Text
.start_of_line(State
)
540 State
.cursor1
.pos
= 1
541 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
542 State
.screen_top1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
} -- copy
546 function Text
.end_of_line(State
)
547 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) + 1
548 if Text
.cursor_out_of_screen(State
) then
549 Text
.snap_cursor_to_bottom_of_screen(State
)
553 function Text
.word_left(State
)
554 -- skip some whitespace
556 if State
.cursor1
.pos
== 1 then
559 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%S') then
564 -- skip some non-whitespace
567 if State
.cursor1
.pos
== 1 then
570 assert(State
.cursor1
.pos
> 1)
571 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%s') then
577 function Text
.word_right(State
)
578 -- skip some whitespace
580 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
583 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%S') then
586 Text
.right_without_scroll(State
)
589 Text
.right_without_scroll(State
)
590 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
593 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%s') then
597 if Text
.cursor_out_of_screen(State
) then
598 Text
.snap_cursor_to_bottom_of_screen(State
)
602 function Text
.match(s
, pos
, pat
)
603 local start_offset
= Text
.offset(s
, pos
)
605 local end_offset
= Text
.offset(s
, pos
+1)
606 assert(end_offset
> start_offset
)
607 local curr
= s
:sub(start_offset
, end_offset
-1)
608 return curr
:match(pat
)
611 function Text
.left(State
)
612 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
613 if State
.cursor1
.pos
> 1 then
614 State
.cursor1
.pos
= State
.cursor1
.pos
-1
616 local new_cursor_line
= State
.cursor1
.line
617 while new_cursor_line
> 1 do
618 new_cursor_line
= new_cursor_line
-1
619 if State
.lines
[new_cursor_line
].mode
== 'text' then
621 line
= new_cursor_line
,
622 pos
= utf8
.len(State
.lines
[new_cursor_line
].data
) + 1,
628 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
629 State
.screen_top1
= {
630 line
=State
.cursor1
.line
,
631 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
633 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
637 function Text
.right(State
)
638 Text
.right_without_scroll(State
)
639 if Text
.cursor_out_of_screen(State
) then
640 Text
.snap_cursor_to_bottom_of_screen(State
)
644 function Text
.right_without_scroll(State
)
645 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
646 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
647 State
.cursor1
.pos
= State
.cursor1
.pos
+1
649 local new_cursor_line
= State
.cursor1
.line
650 while new_cursor_line
<= #State
.lines
-1 do
651 new_cursor_line
= new_cursor_line
+1
652 if State
.lines
[new_cursor_line
].mode
== 'text' then
653 State
.cursor1
= {line
=new_cursor_line
, pos
=1}
660 -- result: pos, index of screen line
661 function Text
.pos_at_start_of_screen_line(State
, loc1
)
662 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
663 local line_cache
= State
.line_cache
[loc1
.line
]
664 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
665 local spos
= line_cache
.screen_line_starting_pos
[i
]
666 if spos
<= loc1
.pos
then
673 function Text
.pos_at_end_of_screen_line(State
, loc1
)
674 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
675 local line_cache
= State
.line_cache
[loc1
.line
]
676 local most_recent_final_pos
= utf8
.len(State
.lines
[loc1
.line
].data
)+1
677 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
678 local spos
= line_cache
.screen_line_starting_pos
[i
]
679 if spos
<= loc1
.pos
then
680 return most_recent_final_pos
682 most_recent_final_pos
= spos
-1
687 function Text
.cursor_at_final_screen_line(State
)
688 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
689 local screen_lines
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
690 --? print(screen_lines[#screen_lines], State.cursor1.pos)
691 return screen_lines
[#screen_lines
] <= State
.cursor1
.pos
694 function Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
696 while State
.cursor1
.line
<= #State
.lines
do
697 if State
.lines
[State
.cursor1
.line
].mode
== 'text' then
700 --? print('cursor skips', State.cursor1.line)
701 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[State
.cursor1
.line
].h
, State
.width
)
702 State
.cursor1
.line
= State
.cursor1
.line
+ 1
704 if State
.cursor1
.pos
== nil then
705 State
.cursor1
.pos
= 1
707 -- hack: insert a text line at bottom of file if necessary
708 if State
.cursor1
.line
> #State
.lines
then
709 assert(State
.cursor1
.line
== #State
.lines
+1)
710 table.insert(State
.lines
, {mode
='text', data
=''})
711 table.insert(State
.line_cache
, {})
713 --? print(y, App.screen.height, App.screen.height-State.line_height)
714 if y
> App
.screen
.height
- State
.line_height
then
715 --? print('scroll up')
716 Text
.snap_cursor_to_bottom_of_screen(State
)
720 -- should never modify State.cursor1
721 function Text
.snap_cursor_to_bottom_of_screen(State
)
722 --? print('to2:', State.cursor1.line, State.cursor1.pos)
723 local top2
= Text
.to2(State
, State
.cursor1
)
724 --? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
725 -- slide to start of screen line
726 top2
.screen_pos
= 1 -- start of screen line
727 --? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
728 --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
729 local y
= App
.screen
.height
- State
.line_height
730 -- duplicate some logic from love.draw
732 --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
733 if top2
.line
== 1 and top2
.screen_line
== 1 then break end
734 if top2
.screen_line
> 1 or State
.lines
[top2
.line
-1].mode
== 'text' then
735 local h
= State
.line_height
736 if y
- h
< State
.top
then
741 assert(top2
.line
> 1)
742 assert(State
.lines
[top2
.line
-1].mode
== 'drawing')
743 -- We currently can't draw partial drawings, so either skip it entirely
745 local h
= Drawing_padding_height
+ Drawing
.pixels(State
.lines
[top2
.line
-1].h
, State
.width
)
746 if y
- h
< State
.top
then
749 --? print('skipping drawing of height', h)
752 top2
= Text
.previous_screen_line(State
, top2
)
754 --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
755 State
.screen_top1
= Text
.to1(State
, top2
)
756 --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
757 --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
758 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
761 function Text
.in_line(State
, line_index
, x
,y
)
762 local line
= State
.lines
[line_index
]
763 local line_cache
= State
.line_cache
[line_index
]
764 if line_cache
.starty
== nil then return false end -- outside current page
765 if y
< line_cache
.starty
then return false end
766 Text
.populate_screen_line_starting_pos(State
, line_index
)
767 return y
< line_cache
.starty
+ State
.line_height
*(#line_cache
.screen_line_starting_pos
- Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
) + 1)
770 -- convert mx,my in pixels to schema-1 coordinates
771 function Text
.to_pos_on_line(State
, line_index
, mx
, my
)
772 local line
= State
.lines
[line_index
]
773 local line_cache
= State
.line_cache
[line_index
]
774 assert(my
>= line_cache
.starty
)
775 -- duplicate some logic from Text.draw
776 local y
= line_cache
.starty
777 local start_screen_line_index
= Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
)
778 for screen_line_index
= start_screen_line_index
,#line_cache
.screen_line_starting_pos
do
779 local screen_line_starting_pos
= line_cache
.screen_line_starting_pos
[screen_line_index
]
780 local screen_line_starting_byte_offset
= Text
.offset(line
.data
, screen_line_starting_pos
)
781 --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
782 local nexty
= y
+ State
.line_height
784 -- On all wrapped screen lines but the final one, clicks past end of
785 -- line position cursor on final character of screen line.
786 -- (The final screen line positions past end of screen line as always.)
787 if screen_line_index
< #line_cache
.screen_line_starting_pos
and mx
> State
.left
+ Text
.screen_line_width(State
, line_index
, screen_line_index
) then
788 --? print('past end of non-final line; return')
789 return line_cache
.screen_line_starting_pos
[screen_line_index
+1]-1
791 local s
= string.sub(line
.data
, screen_line_starting_byte_offset
)
792 --? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
793 return screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, mx
, State
.left
) - 1
800 function Text
.screen_line_width(State
, line_index
, i
)
801 local line
= State
.lines
[line_index
]
802 local line_cache
= State
.line_cache
[line_index
]
803 local start_pos
= line_cache
.screen_line_starting_pos
[i
]
804 local start_offset
= Text
.offset(line
.data
, start_pos
)
806 if i
< #line_cache
.screen_line_starting_pos
then
807 local past_end_pos
= line_cache
.screen_line_starting_pos
[i
+1]
808 local past_end_offset
= Text
.offset(line
.data
, past_end_pos
)
809 screen_line
= string.sub(line
.data
, start_offset
, past_end_offset
-1)
811 screen_line
= string.sub(line
.data
, start_pos
)
813 return App
.width(screen_line
)
816 function Text
.screen_line_index(screen_line_starting_pos
, pos
)
817 for i
= #screen_line_starting_pos
,1,-1 do
818 if screen_line_starting_pos
[i
] <= pos
then
824 -- convert x pixel coordinate to pos
825 -- oblivious to wrapping
826 -- result: 1 to len+1
827 function Text
.nearest_cursor_pos(line
, x
, left
)
831 local len
= utf8
.len(line
)
832 local max_x
= left
+Text
.x(line
, len
+1)
836 local leftpos
, rightpos
= 1, len
+1
837 --? print('-- nearest', x)
839 --? print('nearest', x, '^'..line..'$', leftpos, rightpos)
840 if leftpos
== rightpos
then
843 local curr
= math
.floor((leftpos
+rightpos
)/2)
844 local currxmin
= left
+Text
.x(line
, curr
)
845 local currxmax
= left
+Text
.x(line
, curr
+1)
846 --? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
847 if currxmin
<= x
and x
< currxmax
then
848 if x
-currxmin
< currxmax
-x
then
854 if leftpos
>= rightpos
-1 then
866 -- return the nearest index of line (in utf8 code points) which lies entirely
867 -- within x pixels of the left margin
868 -- result: 0 to len+1
869 function Text
.nearest_pos_less_than(line
, x
)
870 --? print('', '-- nearest_pos_less_than', line, x)
871 local len
= utf8
.len(line
)
872 local max_x
= Text
.x_after(line
, len
)
876 local left
, right
= 0, len
+1
878 local curr
= math
.floor((left
+right
)/2)
879 local currxmin
= Text
.x_after(line
, curr
+1)
880 local currxmax
= Text
.x_after(line
, curr
+2)
881 --? print('', x, left, right, curr, currxmin, currxmax)
882 if currxmin
<= x
and x
< currxmax
then
885 if left
>= right
-1 then
897 function Text
.x_after(s
, pos
)
898 local offset
= Text
.offset(s
, math
.min(pos
+1, #s
+1))
899 local s_before
= s
:sub(1, offset
-1)
900 --? print('^'..s_before..'$')
901 return App
.width(s_before
)
904 function Text
.x(s
, pos
)
905 local offset
= Text
.offset(s
, pos
)
906 local s_before
= s
:sub(1, offset
-1)
907 return App
.width(s_before
)
910 function Text
.to2(State
, loc1
)
911 if State
.lines
[loc1
.line
].mode
== 'drawing' then
912 return {line
=loc1
.line
, screen_line
=1, screen_pos
=1}
914 local result
= {line
=loc1
.line
}
915 local line_cache
= State
.line_cache
[loc1
.line
]
916 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
917 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
918 local spos
= line_cache
.screen_line_starting_pos
[i
]
919 if spos
<= loc1
.pos
then
920 result
.screen_line
= i
921 result
.screen_pos
= loc1
.pos
- spos
+ 1
925 assert(result
.screen_pos
)
929 function Text
.to1(State
, loc2
)
930 local result
= {line
=loc2
.line
, pos
=loc2
.screen_pos
}
931 if loc2
.screen_line
> 1 then
932 result
.pos
= State
.line_cache
[loc2
.line
].screen_line_starting_pos
[loc2
.screen_line
] + loc2
.screen_pos
- 1
937 function Text
.eq1(a
, b
)
938 return a
.line
== b
.line
and a
.pos
== b
.pos
941 function Text
.lt1(a
, b
)
942 if a
.line
< b
.line
then
945 if a
.line
> b
.line
then
951 function Text
.le1(a
, b
)
952 if a
.line
< b
.line
then
955 if a
.line
> b
.line
then
958 return a
.pos
<= b
.pos
961 function Text
.offset(s
, pos1
)
962 if pos1
== 1 then return 1 end
963 local result
= utf8
.offset(s
, pos1
)
964 if result
== nil then
971 function Text
.previous_screen_line(State
, loc2
)
972 if loc2
.screen_line
> 1 then
973 return {line
=loc2
.line
, screen_line
=loc2
.screen_line
-1, screen_pos
=1}
974 elseif loc2
.line
== 1 then
976 elseif State
.lines
[loc2
.line
-1].mode
== 'drawing' then
977 return {line
=loc2
.line
-1, screen_line
=1, screen_pos
=1}
979 local l
= State
.lines
[loc2
.line
-1]
980 Text
.populate_screen_line_starting_pos(State
, loc2
.line
-1)
981 return {line
=loc2
.line
-1, screen_line
=#State
.line_cache
[loc2
.line
-1].screen_line_starting_pos
, screen_pos
=1}
986 function Text
.tweak_screen_top_and_cursor(State
)
987 if State
.screen_top1
.pos
== 1 then return end
988 Text
.populate_screen_line_starting_pos(State
, State
.screen_top1
.line
)
989 local line
= State
.lines
[State
.screen_top1
.line
]
990 local line_cache
= State
.line_cache
[State
.screen_top1
.line
]
991 for i
=2,#line_cache
.screen_line_starting_pos
do
992 local pos
= line_cache
.screen_line_starting_pos
[i
]
993 if pos
== State
.screen_top1
.pos
then
996 if pos
> State
.screen_top1
.pos
then
997 -- make sure screen top is at start of a screen line
998 local prev
= line_cache
.screen_line_starting_pos
[i
-1]
999 if State
.screen_top1
.pos
- prev
< pos
- State
.screen_top1
.pos
then
1000 State
.screen_top1
.pos
= prev
1002 State
.screen_top1
.pos
= pos
1007 -- make sure cursor is on screen
1008 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
1009 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
1010 elseif State
.cursor1
.line
>= State
.screen_bottom1
.line
then
1011 --? print('too low')
1012 if Text
.cursor_out_of_screen(State
) then
1015 line
=State
.screen_bottom1
.line
,
1016 pos
=Text
.to_pos_on_line(State
, State
.screen_bottom1
.line
, State
.right
-5, App
.screen
.height
-5),
1022 -- slightly expensive since it redraws the screen
1023 function Text
.cursor_out_of_screen(State
)
1025 return State
.cursor_y
== nil
1026 -- this approach is cheaper and almost works, except on the final screen
1027 -- where file ends above bottom of screen
1028 --? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)
1029 --? local botline1 = {line=State.cursor1.line, pos=botpos}
1030 --? return Text.lt1(State.screen_bottom1, botline1)
1033 function Text
.redraw_all(State
)
1034 --? print('clearing fragments')
1035 State
.line_cache
= {}
1036 for i
=1,#State
.lines
do
1037 State
.line_cache
[i
] = {}
1041 function Text
.clear_screen_line_cache(State
, line_index
)
1042 State
.line_cache
[line_index
].screen_line_starting_pos
= nil
1043 State
.line_cache
[line_index
].link_offsets
= nil
1047 return s
:gsub('^%s+', ''):gsub('%s+$', '')
1051 return s
:gsub('^%s+', '')
1055 return s
:gsub('%s+$', '')
1058 function starts_with(s
, prefix
)
1059 if #s
< #prefix
then
1063 if s
:sub(i
,i
) ~= prefix
:sub(i
,i
) then
1070 function ends_with(s
, suffix
)
1071 if #s
< #suffix
then
1074 for i
=0,#suffix
-1 do
1075 if s
:sub(#s
-i
,#s
-i
) ~= suffix
:sub(#suffix
-i
,#suffix
-i
) then