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
)
7 --? print('text.draw', line_index, y)
8 local line
= State
.lines
[line_index
]
9 local line_cache
= State
.line_cache
[line_index
]
11 line_cache
.startpos
= startpos
13 local final_screen_line_starting_pos
= startpos
-- track value to return
14 Text
.populate_screen_line_starting_pos(State
, line_index
)
15 assert(#line_cache
.screen_line_starting_pos
>= 1)
16 for i
=1,#line_cache
.screen_line_starting_pos
do
17 local pos
= line_cache
.screen_line_starting_pos
[i
]
18 if pos
< startpos
then
21 final_screen_line_starting_pos
= pos
22 local screen_line
= Text
.screen_line(line
, line_cache
, i
)
23 --? print('text.draw:', screen_line, 'at', line_index,pos, 'after', x,y)
24 local frag_len
= utf8
.len(screen_line
)
25 -- render any highlights
26 if State
.selection1
.line
then
27 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
28 Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, lo
,hi
)
30 if line_index
== State
.cursor1
.line
then
31 -- render search highlight or cursor
32 if State
.search_term
then
33 local data
= State
.lines
[State
.cursor1
.line
].data
34 local cursor_offset
= Text
.offset(data
, State
.cursor1
.pos
)
35 if data
:sub(cursor_offset
, cursor_offset
+#State
.search_term
-1) == State
.search_term
then
36 local save_selection
= State
.selection1
37 State
.selection1
= {line
=line_index
, pos
=State
.cursor1
.pos
+utf8
.len(State
.search_term
)}
38 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
39 Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, lo
,hi
)
40 State
.selection1
= save_selection
43 if pos
<= State
.cursor1
.pos
and pos
+ frag_len
> State
.cursor1
.pos
then
44 Text
.draw_cursor(State
, State
.left
+Text
.x(screen_line
, State
.cursor1
.pos
-pos
+1), y
)
45 elseif pos
+ frag_len
== State
.cursor1
.pos
then
46 -- Show cursor at end of line.
47 -- This place also catches end of wrapping screen lines. That doesn't seem worth distinguishing.
48 -- It seems useful to see a cursor whether your eye is on the left or right margin.
49 Text
.draw_cursor(State
, State
.left
+Text
.x(screen_line
, State
.cursor1
.pos
-pos
+1), y
)
55 App
.screen
.print(screen_line
, State
.left
,y
)
56 y
= y
+ State
.line_height
57 if y
>= App
.screen
.height
then
62 return y
, final_screen_line_starting_pos
65 function Text
.screen_line(line
, line_cache
, i
)
66 local pos
= line_cache
.screen_line_starting_pos
[i
]
67 local offset
= Text
.offset(line
.data
, pos
)
68 if i
>= #line_cache
.screen_line_starting_pos
then
69 return line
.data
:sub(offset
)
71 local endpos
= line_cache
.screen_line_starting_pos
[i
+1]-1
72 local end_offset
= Text
.offset(line
.data
, endpos
)
73 return line
.data
:sub(offset
, end_offset
)
76 function Text
.draw_cursor(State
, x
, y
)
78 if math
.floor(Cursor_time
*2)%2 == 0 then
79 App
.color(Cursor_color
)
80 love
.graphics
.rectangle('fill', x
,y
, 3,State
.line_height
)
83 State
.cursor_y
= y
+State
.line_height
86 function Text
.populate_screen_line_starting_pos(State
, line_index
)
87 local line
= State
.lines
[line_index
]
88 if line
.mode
~= 'text' then return end
89 local line_cache
= State
.line_cache
[line_index
]
90 if line_cache
.screen_line_starting_pos
then
93 line_cache
.screen_line_starting_pos
= {1}
96 -- try to wrap at word boundaries
97 for frag
in line
.data
:gmatch('%S*%s*') do
98 local frag_width
= App
.width(frag
)
99 --? print('-- frag:', frag, pos, x, frag_width, State.width)
100 while x
+ frag_width
> State
.width
do
101 --? print('frag:', frag, pos, x, frag_width, State.width)
102 if x
< 0.8 * State
.width
then
103 -- long word; chop it at some letter
104 -- We're not going to reimplement TeX here.
105 local bpos
= Text
.nearest_pos_less_than(frag
, State
.width
- x
)
106 -- everything works if bpos == 0, but is a little inefficient
108 local boffset
= Text
.offset(frag
, bpos
+1) -- byte _after_ bpos
109 frag
= string.sub(frag
, boffset
)
111 --? print('after chop:', frag)
113 frag_width
= App
.width(frag
)
115 --? print('screen line:', pos)
116 table.insert(line_cache
.screen_line_starting_pos
, pos
)
117 x
= 0 -- new screen line
120 pos
= pos
+ utf8
.len(frag
)
124 function Text
.text_input(State
, t
)
125 if App
.mouse_down(1) then return end
126 if App
.ctrl_down() or App
.alt_down() or App
.cmd_down() then return end
127 local before
= snapshot(State
, State
.cursor1
.line
)
128 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
129 Text
.insert_at_cursor(State
, t
)
130 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
131 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
132 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
134 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
137 function Text
.insert_at_cursor(State
, t
)
138 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
139 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
140 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
)
141 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
142 State
.cursor1
.pos
= State
.cursor1
.pos
+1
145 -- Don't handle any keys here that would trigger text_input above.
146 function Text
.keychord_press(State
, chord
)
147 --? print('chord', chord, State.selection1.line, State.selection1.pos)
148 --== shortcuts that mutate text
149 if chord
== 'return' then
150 local before_line
= State
.cursor1
.line
151 local before
= snapshot(State
, before_line
)
152 Text
.insert_return(State
)
153 State
.selection1
= {}
154 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
155 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
158 record_undo_event(State
, {before
=before
, after
=snapshot(State
, before_line
, State
.cursor1
.line
)})
159 elseif chord
== 'tab' then
160 local before
= snapshot(State
, State
.cursor1
.line
)
161 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
162 Text
.insert_at_cursor(State
, '\t')
163 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
164 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
165 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
166 --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
169 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
170 elseif chord
== 'backspace' then
171 if State
.selection1
.line
then
172 Text
.delete_selection(State
, State
.left
, State
.right
)
177 if State
.cursor1
.pos
> 1 then
178 before
= snapshot(State
, State
.cursor1
.line
)
179 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1)
180 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
183 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
)
185 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
187 State
.cursor1
.pos
= State
.cursor1
.pos
-1
189 elseif State
.cursor1
.line
> 1 then
190 before
= snapshot(State
, State
.cursor1
.line
-1, State
.cursor1
.line
)
191 if State
.lines
[State
.cursor1
.line
-1].mode
== 'drawing' then
192 table.remove(State
.lines
, State
.cursor1
.line
-1)
193 table.remove(State
.line_cache
, State
.cursor1
.line
-1)
196 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
-1].data
)+1
197 State
.lines
[State
.cursor1
.line
-1].data
= State
.lines
[State
.cursor1
.line
-1].data
..State
.lines
[State
.cursor1
.line
].data
198 table.remove(State
.lines
, State
.cursor1
.line
)
199 table.remove(State
.line_cache
, State
.cursor1
.line
)
201 State
.cursor1
.line
= State
.cursor1
.line
-1
203 if State
.screen_top1
.line
> #State
.lines
then
204 Text
.populate_screen_line_starting_pos(State
, #State
.lines
)
205 local line_cache
= State
.line_cache
[#State
.line_cache
]
206 State
.screen_top1
= {line
=#State
.lines
, pos
=line_cache
.screen_line_starting_pos
[#line_cache
.screen_line_starting_pos
]}
207 elseif Text
.lt1(State
.cursor1
, State
.screen_top1
) then
208 State
.screen_top1
= {
209 line
=State
.cursor1
.line
,
210 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
212 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
214 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
215 assert(Text
.le1(State
.screen_top1
, State
.cursor1
))
217 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
218 elseif chord
== 'delete' then
219 if State
.selection1
.line
then
220 Text
.delete_selection(State
, State
.left
, State
.right
)
225 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
226 before
= snapshot(State
, State
.cursor1
.line
)
228 before
= snapshot(State
, State
.cursor1
.line
, State
.cursor1
.line
+1)
230 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
231 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
232 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
+1)
235 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
)
237 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
239 -- no change to State.cursor1.pos
241 elseif State
.cursor1
.line
< #State
.lines
then
242 if State
.lines
[State
.cursor1
.line
+1].mode
== 'text' then
244 State
.lines
[State
.cursor1
.line
].data
= State
.lines
[State
.cursor1
.line
].data
..State
.lines
[State
.cursor1
.line
+1].data
246 table.remove(State
.lines
, State
.cursor1
.line
+1)
247 table.remove(State
.line_cache
, State
.cursor1
.line
+1)
249 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
251 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
252 --== shortcuts that move the cursor
253 elseif chord
== 'left' then
255 State
.selection1
= {}
256 elseif chord
== 'right' then
258 State
.selection1
= {}
259 elseif chord
== 'S-left' then
260 if State
.selection1
.line
== nil then
261 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
264 elseif chord
== 'S-right' then
265 if State
.selection1
.line
== nil then
266 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
269 -- C- hotkeys reserved for drawings, so we'll use M-
270 elseif chord
== 'M-left' then
271 Text
.word_left(State
)
272 State
.selection1
= {}
273 elseif chord
== 'M-right' then
274 Text
.word_right(State
)
275 State
.selection1
= {}
276 elseif chord
== 'M-S-left' then
277 if State
.selection1
.line
== nil then
278 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
280 Text
.word_left(State
)
281 elseif chord
== 'M-S-right' then
282 if State
.selection1
.line
== nil then
283 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
285 Text
.word_right(State
)
286 elseif chord
== 'home' then
287 Text
.start_of_line(State
)
288 State
.selection1
= {}
289 elseif chord
== 'end' then
290 Text
.end_of_line(State
)
291 State
.selection1
= {}
292 elseif chord
== 'S-home' then
293 if State
.selection1
.line
== nil then
294 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
296 Text
.start_of_line(State
)
297 elseif chord
== 'S-end' then
298 if State
.selection1
.line
== nil then
299 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
301 Text
.end_of_line(State
)
302 elseif chord
== 'up' then
304 State
.selection1
= {}
305 elseif chord
== 'down' then
307 State
.selection1
= {}
308 elseif chord
== 'S-up' then
309 if State
.selection1
.line
== nil then
310 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
313 elseif chord
== 'S-down' then
314 if State
.selection1
.line
== nil then
315 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
318 elseif chord
== 'pageup' then
320 State
.selection1
= {}
321 elseif chord
== 'pagedown' then
323 State
.selection1
= {}
324 elseif chord
== 'S-pageup' then
325 if State
.selection1
.line
== nil then
326 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
329 elseif chord
== 'S-pagedown' then
330 if State
.selection1
.line
== nil then
331 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
337 function Text
.insert_return(State
)
338 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
339 table.insert(State
.lines
, State
.cursor1
.line
+1, {mode
='text', data
=string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_offset
)})
340 table.insert(State
.line_cache
, State
.cursor1
.line
+1, {})
341 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_offset
-1)
342 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
343 State
.cursor1
= {line
=State
.cursor1
.line
+1, pos
=1}
346 function Text
.pageup(State
)
348 -- duplicate some logic from love.draw
349 local top2
= Text
.to2(State
, State
.screen_top1
)
350 --? print(App.screen.height)
351 local y
= App
.screen
.height
- State
.line_height
352 while y
>= State
.top
do
353 --? print(y, top2.line, top2.screen_line, top2.screen_pos)
354 if State
.screen_top1
.line
== 1 and State
.screen_top1
.pos
== 1 then break end
355 if State
.lines
[State
.screen_top1
.line
].mode
== 'text' then
356 y
= y
- State
.line_height
357 elseif State
.lines
[State
.screen_top1
.line
].mode
== 'drawing' then
358 y
= y
- Drawing_padding_height
- Drawing
.pixels(State
.lines
[State
.screen_top1
.line
].h
, State
.width
)
360 top2
= Text
.previous_screen_line(State
, top2
)
362 State
.screen_top1
= Text
.to1(State
, top2
)
363 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
364 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
365 --? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
366 --? print('pageup end')
369 function Text
.pagedown(State
)
370 --? print('pagedown')
371 -- If a line/paragraph gets to a page boundary, I often want to scroll
372 -- before I get to the bottom.
373 -- However, only do this if it makes forward progress.
374 local bot2
= Text
.to2(State
, State
.screen_bottom1
)
375 if bot2
.screen_line
> 1 then
376 bot2
.screen_line
= math
.max(bot2
.screen_line
-10, 1)
378 local new_top1
= Text
.to1(State
, bot2
)
379 if Text
.lt1(State
.screen_top1
, new_top1
) then
380 State
.screen_top1
= new_top1
382 State
.screen_top1
= {line
=State
.screen_bottom1
.line
, pos
=State
.screen_bottom1
.pos
}
384 --? print('setting top to', State.screen_top1.line, State.screen_top1.pos)
385 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
386 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
387 --? print('top now', State.screen_top1.line)
388 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
389 --? print('pagedown end')
392 function Text
.up(State
)
393 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
394 --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
395 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
396 if screen_line_starting_pos
== 1 then
397 --? print('cursor is at first screen line of its line')
398 -- line is done; skip to previous text line
399 local new_cursor_line
= State
.cursor1
.line
400 while new_cursor_line
> 1 do
401 new_cursor_line
= new_cursor_line
-1
402 if State
.lines
[new_cursor_line
].mode
== 'text' then
403 --? print('found previous text line')
404 State
.cursor1
= {line
=new_cursor_line
, pos
=nil}
405 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
406 -- previous text line found, pick its final screen line
407 --? print('has multiple screen lines')
408 local screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
409 --? print(#screen_line_starting_pos)
410 screen_line_starting_pos
= screen_line_starting_pos
[#screen_line_starting_pos
]
411 local screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_pos
)
412 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_byte_offset
)
413 State
.cursor1
.pos
= screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
418 -- move up one screen line in current line
419 assert(screen_line_index
> 1)
420 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
-1]
421 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
422 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
423 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
424 --? print('cursor pos is now '..tostring(State.cursor1.pos))
426 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
427 State
.screen_top1
= {
428 line
=State
.cursor1
.line
,
429 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
431 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
435 function Text
.down(State
)
436 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
437 --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
438 assert(State
.cursor1
.pos
)
439 if Text
.cursor_at_final_screen_line(State
) then
440 -- line is done, skip to next text line
441 --? print('cursor at final screen line of its line')
442 local new_cursor_line
= State
.cursor1
.line
443 while new_cursor_line
< #State
.lines
do
444 new_cursor_line
= new_cursor_line
+1
445 if State
.lines
[new_cursor_line
].mode
== 'text' then
447 line
= new_cursor_line
,
448 pos
= Text
.nearest_cursor_pos(State
.lines
[new_cursor_line
].data
, State
.cursor_x
, State
.left
),
450 --? print(State.cursor1.pos)
454 if State
.cursor1
.line
> State
.screen_bottom1
.line
then
455 --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
456 --? print('scroll up preserving cursor')
457 Text
.snap_cursor_to_bottom_of_screen(State
)
458 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
461 -- move down one screen line in current line
462 local scroll_down
= Text
.le1(State
.screen_bottom1
, State
.cursor1
)
463 --? print('cursor is NOT at final screen line of its line')
464 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
465 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
466 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
+1]
467 --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
468 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
469 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
470 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
471 --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
473 --? print('scroll up preserving cursor')
474 Text
.snap_cursor_to_bottom_of_screen(State
)
475 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
478 --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
481 function Text
.start_of_line(State
)
482 State
.cursor1
.pos
= 1
483 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
484 State
.screen_top1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
} -- copy
488 function Text
.end_of_line(State
)
489 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) + 1
490 if Text
.cursor_out_of_screen(State
) then
491 Text
.snap_cursor_to_bottom_of_screen(State
)
495 function Text
.word_left(State
)
496 -- skip some whitespace
498 if State
.cursor1
.pos
== 1 then
501 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%S') then
506 -- skip some non-whitespace
509 if State
.cursor1
.pos
== 1 then
512 assert(State
.cursor1
.pos
> 1)
513 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%s') then
519 function Text
.word_right(State
)
520 -- skip some whitespace
522 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
525 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%S') then
528 Text
.right_without_scroll(State
)
531 Text
.right_without_scroll(State
)
532 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
535 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%s') then
539 if Text
.cursor_out_of_screen(State
) then
540 Text
.snap_cursor_to_bottom_of_screen(State
)
544 function Text
.match(s
, pos
, pat
)
545 local start_offset
= Text
.offset(s
, pos
)
547 local end_offset
= Text
.offset(s
, pos
+1)
548 assert(end_offset
> start_offset
)
549 local curr
= s
:sub(start_offset
, end_offset
-1)
550 return curr
:match(pat
)
553 function Text
.left(State
)
554 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
555 if State
.cursor1
.pos
> 1 then
556 State
.cursor1
.pos
= State
.cursor1
.pos
-1
558 local new_cursor_line
= State
.cursor1
.line
559 while new_cursor_line
> 1 do
560 new_cursor_line
= new_cursor_line
-1
561 if State
.lines
[new_cursor_line
].mode
== 'text' then
563 line
= new_cursor_line
,
564 pos
= utf8
.len(State
.lines
[new_cursor_line
].data
) + 1,
570 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
571 State
.screen_top1
= {
572 line
=State
.cursor1
.line
,
573 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
575 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
579 function Text
.right(State
)
580 Text
.right_without_scroll(State
)
581 if Text
.cursor_out_of_screen(State
) then
582 Text
.snap_cursor_to_bottom_of_screen(State
)
586 function Text
.right_without_scroll(State
)
587 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
588 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
589 State
.cursor1
.pos
= State
.cursor1
.pos
+1
591 local new_cursor_line
= State
.cursor1
.line
592 while new_cursor_line
<= #State
.lines
-1 do
593 new_cursor_line
= new_cursor_line
+1
594 if State
.lines
[new_cursor_line
].mode
== 'text' then
595 State
.cursor1
= {line
=new_cursor_line
, pos
=1}
602 -- result: pos, index of screen line
603 function Text
.pos_at_start_of_screen_line(State
, loc1
)
604 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
605 local line_cache
= State
.line_cache
[loc1
.line
]
606 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
607 local spos
= line_cache
.screen_line_starting_pos
[i
]
608 if spos
<= loc1
.pos
then
615 function Text
.pos_at_end_of_screen_line(State
, loc1
)
616 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
617 local line_cache
= State
.line_cache
[loc1
.line
]
618 local most_recent_final_pos
= utf8
.len(State
.lines
[loc1
.line
].data
)+1
619 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
620 local spos
= line_cache
.screen_line_starting_pos
[i
]
621 if spos
<= loc1
.pos
then
622 return most_recent_final_pos
624 most_recent_final_pos
= spos
-1
629 function Text
.cursor_at_final_screen_line(State
)
630 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
631 local screen_lines
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
632 --? print(screen_lines[#screen_lines], State.cursor1.pos)
633 return screen_lines
[#screen_lines
] <= State
.cursor1
.pos
636 function Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
638 while State
.cursor1
.line
<= #State
.lines
do
639 if State
.lines
[State
.cursor1
.line
].mode
== 'text' then
642 --? print('cursor skips', State.cursor1.line)
643 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[State
.cursor1
.line
].h
, State
.width
)
644 State
.cursor1
.line
= State
.cursor1
.line
+ 1
646 if State
.cursor1
.pos
== nil then
647 State
.cursor1
.pos
= 1
649 -- hack: insert a text line at bottom of file if necessary
650 if State
.cursor1
.line
> #State
.lines
then
651 assert(State
.cursor1
.line
== #State
.lines
+1)
652 table.insert(State
.lines
, {mode
='text', data
=''})
653 table.insert(State
.line_cache
, {})
655 --? print(y, App.screen.height, App.screen.height-State.line_height)
656 if y
> App
.screen
.height
- State
.line_height
then
657 --? print('scroll up')
658 Text
.snap_cursor_to_bottom_of_screen(State
)
662 -- should never modify State.cursor1
663 function Text
.snap_cursor_to_bottom_of_screen(State
)
664 --? print('to2:', State.cursor1.line, State.cursor1.pos)
665 local top2
= Text
.to2(State
, State
.cursor1
)
666 --? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
667 -- slide to start of screen line
668 top2
.screen_pos
= 1 -- start of screen line
669 --? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
670 --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
671 local y
= App
.screen
.height
- State
.line_height
672 -- duplicate some logic from love.draw
674 --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
675 if top2
.line
== 1 and top2
.screen_line
== 1 then break end
676 if top2
.screen_line
> 1 or State
.lines
[top2
.line
-1].mode
== 'text' then
677 local h
= State
.line_height
678 if y
- h
< State
.top
then
683 assert(top2
.line
> 1)
684 assert(State
.lines
[top2
.line
-1].mode
== 'drawing')
685 -- We currently can't draw partial drawings, so either skip it entirely
687 local h
= Drawing_padding_height
+ Drawing
.pixels(State
.lines
[top2
.line
-1].h
, State
.width
)
688 if y
- h
< State
.top
then
691 --? print('skipping drawing of height', h)
694 top2
= Text
.previous_screen_line(State
, top2
)
696 --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
697 State
.screen_top1
= Text
.to1(State
, top2
)
698 --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
699 --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
700 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
703 function Text
.in_line(State
, line_index
, x
,y
)
704 local line
= State
.lines
[line_index
]
705 local line_cache
= State
.line_cache
[line_index
]
706 if line_cache
.starty
== nil then return false end -- outside current page
707 if y
< line_cache
.starty
then return false end
708 Text
.populate_screen_line_starting_pos(State
, line_index
)
709 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)
712 -- convert mx,my in pixels to schema-1 coordinates
713 function Text
.to_pos_on_line(State
, line_index
, mx
, my
)
714 local line
= State
.lines
[line_index
]
715 local line_cache
= State
.line_cache
[line_index
]
716 assert(my
>= line_cache
.starty
)
717 -- duplicate some logic from Text.draw
718 local y
= line_cache
.starty
719 local start_screen_line_index
= Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
)
720 for screen_line_index
= start_screen_line_index
,#line_cache
.screen_line_starting_pos
do
721 local screen_line_starting_pos
= line_cache
.screen_line_starting_pos
[screen_line_index
]
722 local screen_line_starting_byte_offset
= Text
.offset(line
.data
, screen_line_starting_pos
)
723 --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
724 local nexty
= y
+ State
.line_height
726 -- On all wrapped screen lines but the final one, clicks past end of
727 -- line position cursor on final character of screen line.
728 -- (The final screen line positions past end of screen line as always.)
729 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
730 --? print('past end of non-final line; return')
731 return line_cache
.screen_line_starting_pos
[screen_line_index
+1]-1
733 local s
= string.sub(line
.data
, screen_line_starting_byte_offset
)
734 --? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
735 return screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, mx
, State
.left
) - 1
742 function Text
.screen_line_width(State
, line_index
, i
)
743 local line
= State
.lines
[line_index
]
744 local line_cache
= State
.line_cache
[line_index
]
745 local start_pos
= line_cache
.screen_line_starting_pos
[i
]
746 local start_offset
= Text
.offset(line
.data
, start_pos
)
748 if i
< #line_cache
.screen_line_starting_pos
then
749 local past_end_pos
= line_cache
.screen_line_starting_pos
[i
+1]
750 local past_end_offset
= Text
.offset(line
.data
, past_end_pos
)
751 screen_line
= string.sub(line
.data
, start_offset
, past_end_offset
-1)
753 screen_line
= string.sub(line
.data
, start_pos
)
755 return App
.width(screen_line
)
758 function Text
.screen_line_index(screen_line_starting_pos
, pos
)
759 for i
= #screen_line_starting_pos
,1,-1 do
760 if screen_line_starting_pos
[i
] <= pos
then
766 -- convert x pixel coordinate to pos
767 -- oblivious to wrapping
768 -- result: 1 to len+1
769 function Text
.nearest_cursor_pos(line
, x
, left
)
773 local len
= utf8
.len(line
)
774 local max_x
= left
+Text
.x(line
, len
+1)
778 local leftpos
, rightpos
= 1, len
+1
779 --? print('-- nearest', x)
781 --? print('nearest', x, '^'..line..'$', leftpos, rightpos)
782 if leftpos
== rightpos
then
785 local curr
= math
.floor((leftpos
+rightpos
)/2)
786 local currxmin
= left
+Text
.x(line
, curr
)
787 local currxmax
= left
+Text
.x(line
, curr
+1)
788 --? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
789 if currxmin
<= x
and x
< currxmax
then
790 if x
-currxmin
< currxmax
-x
then
796 if leftpos
>= rightpos
-1 then
808 -- return the nearest index of line (in utf8 code points) which lies entirely
809 -- within x pixels of the left margin
810 -- result: 0 to len+1
811 function Text
.nearest_pos_less_than(line
, x
)
812 --? print('', '-- nearest_pos_less_than', line, x)
813 local len
= utf8
.len(line
)
814 local max_x
= Text
.x_after(line
, len
)
818 local left
, right
= 0, len
+1
820 local curr
= math
.floor((left
+right
)/2)
821 local currxmin
= Text
.x_after(line
, curr
+1)
822 local currxmax
= Text
.x_after(line
, curr
+2)
823 --? print('', x, left, right, curr, currxmin, currxmax)
824 if currxmin
<= x
and x
< currxmax
then
827 if left
>= right
-1 then
839 function Text
.x_after(s
, pos
)
840 local offset
= Text
.offset(s
, math
.min(pos
+1, #s
+1))
841 local s_before
= s
:sub(1, offset
-1)
842 --? print('^'..s_before..'$')
843 return App
.width(s_before
)
846 function Text
.x(s
, pos
)
847 local offset
= Text
.offset(s
, pos
)
848 local s_before
= s
:sub(1, offset
-1)
849 return App
.width(s_before
)
852 function Text
.to2(State
, loc1
)
853 if State
.lines
[loc1
.line
].mode
== 'drawing' then
854 return {line
=loc1
.line
, screen_line
=1, screen_pos
=1}
856 local result
= {line
=loc1
.line
}
857 local line_cache
= State
.line_cache
[loc1
.line
]
858 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
859 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
860 local spos
= line_cache
.screen_line_starting_pos
[i
]
861 if spos
<= loc1
.pos
then
862 result
.screen_line
= i
863 result
.screen_pos
= loc1
.pos
- spos
+ 1
867 assert(result
.screen_pos
)
871 function Text
.to1(State
, loc2
)
872 local result
= {line
=loc2
.line
, pos
=loc2
.screen_pos
}
873 if loc2
.screen_line
> 1 then
874 result
.pos
= State
.line_cache
[loc2
.line
].screen_line_starting_pos
[loc2
.screen_line
] + loc2
.screen_pos
- 1
879 function Text
.eq1(a
, b
)
880 return a
.line
== b
.line
and a
.pos
== b
.pos
883 function Text
.lt1(a
, b
)
884 if a
.line
< b
.line
then
887 if a
.line
> b
.line
then
893 function Text
.le1(a
, b
)
894 if a
.line
< b
.line
then
897 if a
.line
> b
.line
then
900 return a
.pos
<= b
.pos
903 function Text
.offset(s
, pos1
)
904 if pos1
== 1 then return 1 end
905 local result
= utf8
.offset(s
, pos1
)
906 if result
== nil then
913 function Text
.previous_screen_line(State
, loc2
)
914 if loc2
.screen_line
> 1 then
915 return {line
=loc2
.line
, screen_line
=loc2
.screen_line
-1, screen_pos
=1}
916 elseif loc2
.line
== 1 then
918 elseif State
.lines
[loc2
.line
-1].mode
== 'drawing' then
919 return {line
=loc2
.line
-1, screen_line
=1, screen_pos
=1}
921 local l
= State
.lines
[loc2
.line
-1]
922 Text
.populate_screen_line_starting_pos(State
, loc2
.line
-1)
923 return {line
=loc2
.line
-1, screen_line
=#State
.line_cache
[loc2
.line
-1].screen_line_starting_pos
, screen_pos
=1}
928 function Text
.tweak_screen_top_and_cursor(State
)
929 if State
.screen_top1
.pos
== 1 then return end
930 Text
.populate_screen_line_starting_pos(State
, State
.screen_top1
.line
)
931 local line
= State
.lines
[State
.screen_top1
.line
]
932 local line_cache
= State
.line_cache
[State
.screen_top1
.line
]
933 for i
=2,#line_cache
.screen_line_starting_pos
do
934 local pos
= line_cache
.screen_line_starting_pos
[i
]
935 if pos
== State
.screen_top1
.pos
then
938 if pos
> State
.screen_top1
.pos
then
939 -- make sure screen top is at start of a screen line
940 local prev
= line_cache
.screen_line_starting_pos
[i
-1]
941 if State
.screen_top1
.pos
- prev
< pos
- State
.screen_top1
.pos
then
942 State
.screen_top1
.pos
= prev
944 State
.screen_top1
.pos
= pos
949 -- make sure cursor is on screen
950 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
951 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
952 elseif State
.cursor1
.line
>= State
.screen_bottom1
.line
then
954 if Text
.cursor_out_of_screen(State
) then
957 line
=State
.screen_bottom1
.line
,
958 pos
=Text
.to_pos_on_line(State
, State
.screen_bottom1
.line
, State
.right
-5, App
.screen
.height
-5),
964 -- slightly expensive since it redraws the screen
965 function Text
.cursor_out_of_screen(State
)
967 return State
.cursor_y
== nil
968 -- this approach is cheaper and almost works, except on the final screen
969 -- where file ends above bottom of screen
970 --? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)
971 --? local botline1 = {line=State.cursor1.line, pos=botpos}
972 --? return Text.lt1(State.screen_bottom1, botline1)
975 function Text
.redraw_all(State
)
976 --? print('clearing fragments')
977 State
.line_cache
= {}
978 for i
=1,#State
.lines
do
979 State
.line_cache
[i
] = {}
983 function Text
.clear_screen_line_cache(State
, line_index
)
984 State
.line_cache
[line_index
].screen_line_starting_pos
= nil
988 return s
:gsub('^%s+', ''):gsub('%s+$', '')
992 return s
:gsub('^%s+', '')
996 return s
:gsub('%s+$', '')
999 function starts_with(s
, prefix
)
1000 if #s
< #prefix
then
1004 if s
:sub(i
,i
) ~= prefix
:sub(i
,i
) then
1011 function ends_with(s
, suffix
)
1012 if #s
< #suffix
then
1015 for i
=0,#suffix
-1 do
1016 if s
:sub(#s
-i
,#s
-i
) ~= suffix
:sub(#suffix
-i
,#suffix
-i
) then