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
)
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
)
31 App
.screen
.print(screen_line
, State
.left
,y
)
32 -- render cursor if necessary
33 if line_index
== State
.cursor1
.line
then
34 if pos
<= State
.cursor1
.pos
and pos
+ frag_len
>= State
.cursor1
.pos
then
35 if State
.search_term
then
36 local data
= State
.lines
[State
.cursor1
.line
].data
37 local cursor_offset
= Text
.offset(data
, State
.cursor1
.pos
)
38 if data
:sub(cursor_offset
, cursor_offset
+#State
.search_term
-1) == State
.search_term
then
39 local lo_px
= Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, State
.cursor1
.pos
, State
.cursor1
.pos
+utf8
.len(State
.search_term
))
41 love
.graphics
.print(State
.search_term
, State
.left
+lo_px
,y
)
44 Text
.draw_cursor(State
, State
.left
+Text
.x(screen_line
, State
.cursor1
.pos
-pos
+1), y
)
48 y
= y
+ State
.line_height
49 if y
>= App
.screen
.height
then
54 return y
, final_screen_line_starting_pos
57 function Text
.screen_line(line
, line_cache
, i
)
58 local pos
= line_cache
.screen_line_starting_pos
[i
]
59 local offset
= Text
.offset(line
.data
, pos
)
60 if i
>= #line_cache
.screen_line_starting_pos
then
61 return line
.data
:sub(offset
)
63 local endpos
= line_cache
.screen_line_starting_pos
[i
+1]-1
64 local end_offset
= Text
.offset(line
.data
, endpos
)
65 return line
.data
:sub(offset
, end_offset
)
68 function Text
.draw_cursor(State
, x
, y
)
70 if math
.floor(Cursor_time
*2)%2 == 0 then
71 App
.color(Cursor_color
)
72 love
.graphics
.rectangle('fill', x
,y
, 3,State
.line_height
)
75 State
.cursor_y
= y
+State
.line_height
78 function Text
.populate_screen_line_starting_pos(State
, line_index
)
79 local line
= State
.lines
[line_index
]
80 if line
.mode
~= 'text' then return end
81 local line_cache
= State
.line_cache
[line_index
]
82 if line_cache
.screen_line_starting_pos
then
85 line_cache
.screen_line_starting_pos
= {1}
88 -- try to wrap at word boundaries
89 for frag
in line
.data
:gmatch('%S*%s*') do
90 local frag_width
= App
.width(frag
)
91 --? print('-- frag:', frag, pos, x, frag_width, State.width)
92 while x
+ frag_width
> State
.width
do
93 --? print('frag:', frag, pos, x, frag_width, State.width)
94 if x
< 0.8 * State
.width
then
95 -- long word; chop it at some letter
96 -- We're not going to reimplement TeX here.
97 local bpos
= Text
.nearest_pos_less_than(frag
, State
.width
- x
)
98 -- everything works if bpos == 0, but is a little inefficient
100 local boffset
= Text
.offset(frag
, bpos
+1) -- byte _after_ bpos
101 frag
= string.sub(frag
, boffset
)
103 --? print('after chop:', frag)
105 frag_width
= App
.width(frag
)
107 --? print('screen line:', pos)
108 table.insert(line_cache
.screen_line_starting_pos
, pos
)
109 x
= 0 -- new screen line
112 pos
= pos
+ utf8
.len(frag
)
116 function Text
.text_input(State
, t
)
117 if App
.mouse_down(1) then return end
118 if App
.ctrl_down() or App
.alt_down() or App
.cmd_down() then return end
119 local before
= snapshot(State
, State
.cursor1
.line
)
120 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
121 Text
.insert_at_cursor(State
, t
)
122 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
123 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
124 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
126 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
129 function Text
.insert_at_cursor(State
, t
)
130 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
131 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
132 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
)
133 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
134 State
.cursor1
.pos
= State
.cursor1
.pos
+1
137 -- Don't handle any keys here that would trigger text_input above.
138 function Text
.keychord_press(State
, chord
)
139 --? print('chord', chord, State.selection1.line, State.selection1.pos)
140 --== shortcuts that mutate text
141 if chord
== 'return' then
142 local before_line
= State
.cursor1
.line
143 local before
= snapshot(State
, before_line
)
144 Text
.insert_return(State
)
145 State
.selection1
= {}
146 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
147 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
150 record_undo_event(State
, {before
=before
, after
=snapshot(State
, before_line
, State
.cursor1
.line
)})
151 elseif chord
== 'tab' then
152 local before
= snapshot(State
, State
.cursor1
.line
)
153 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
154 Text
.insert_at_cursor(State
, '\t')
155 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
156 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
157 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
158 --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
161 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
162 elseif chord
== 'backspace' then
163 if State
.selection1
.line
then
164 Text
.delete_selection(State
, State
.left
, State
.right
)
169 if State
.cursor1
.pos
> 1 then
170 before
= snapshot(State
, State
.cursor1
.line
)
171 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1)
172 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
175 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
)
177 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
179 State
.cursor1
.pos
= State
.cursor1
.pos
-1
181 elseif State
.cursor1
.line
> 1 then
182 before
= snapshot(State
, State
.cursor1
.line
-1, State
.cursor1
.line
)
183 if State
.lines
[State
.cursor1
.line
-1].mode
== 'drawing' then
184 table.remove(State
.lines
, State
.cursor1
.line
-1)
185 table.remove(State
.line_cache
, State
.cursor1
.line
-1)
188 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
-1].data
)+1
189 State
.lines
[State
.cursor1
.line
-1].data
= State
.lines
[State
.cursor1
.line
-1].data
..State
.lines
[State
.cursor1
.line
].data
190 table.remove(State
.lines
, State
.cursor1
.line
)
191 table.remove(State
.line_cache
, State
.cursor1
.line
)
193 State
.cursor1
.line
= State
.cursor1
.line
-1
195 if State
.screen_top1
.line
> #State
.lines
then
196 Text
.populate_screen_line_starting_pos(State
, #State
.lines
)
197 local line_cache
= State
.line_cache
[#State
.line_cache
]
198 State
.screen_top1
= {line
=#State
.lines
, pos
=line_cache
.screen_line_starting_pos
[#line_cache
.screen_line_starting_pos
]}
199 elseif Text
.lt1(State
.cursor1
, State
.screen_top1
) then
200 State
.screen_top1
= {
201 line
=State
.cursor1
.line
,
202 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
204 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
206 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
207 assert(Text
.le1(State
.screen_top1
, State
.cursor1
))
209 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
210 elseif chord
== 'delete' then
211 if State
.selection1
.line
then
212 Text
.delete_selection(State
, State
.left
, State
.right
)
217 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
218 before
= snapshot(State
, State
.cursor1
.line
)
220 before
= snapshot(State
, State
.cursor1
.line
, State
.cursor1
.line
+1)
222 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
223 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
224 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
+1)
227 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
)
229 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
231 -- no change to State.cursor1.pos
233 elseif State
.cursor1
.line
< #State
.lines
then
234 if State
.lines
[State
.cursor1
.line
+1].mode
== 'text' then
236 State
.lines
[State
.cursor1
.line
].data
= State
.lines
[State
.cursor1
.line
].data
..State
.lines
[State
.cursor1
.line
+1].data
238 table.remove(State
.lines
, State
.cursor1
.line
+1)
239 table.remove(State
.line_cache
, State
.cursor1
.line
+1)
241 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
243 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
244 --== shortcuts that move the cursor
245 elseif chord
== 'left' then
247 State
.selection1
= {}
248 elseif chord
== 'right' then
250 State
.selection1
= {}
251 elseif chord
== 'S-left' then
252 if State
.selection1
.line
== nil then
253 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
256 elseif chord
== 'S-right' then
257 if State
.selection1
.line
== nil then
258 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
261 -- C- hotkeys reserved for drawings, so we'll use M-
262 elseif chord
== 'M-left' then
263 Text
.word_left(State
)
264 State
.selection1
= {}
265 elseif chord
== 'M-right' then
266 Text
.word_right(State
)
267 State
.selection1
= {}
268 elseif chord
== 'M-S-left' then
269 if State
.selection1
.line
== nil then
270 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
272 Text
.word_left(State
)
273 elseif chord
== 'M-S-right' then
274 if State
.selection1
.line
== nil then
275 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
277 Text
.word_right(State
)
278 elseif chord
== 'home' then
279 Text
.start_of_line(State
)
280 State
.selection1
= {}
281 elseif chord
== 'end' then
282 Text
.end_of_line(State
)
283 State
.selection1
= {}
284 elseif chord
== 'S-home' then
285 if State
.selection1
.line
== nil then
286 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
288 Text
.start_of_line(State
)
289 elseif chord
== 'S-end' then
290 if State
.selection1
.line
== nil then
291 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
293 Text
.end_of_line(State
)
294 elseif chord
== 'up' then
296 State
.selection1
= {}
297 elseif chord
== 'down' then
299 State
.selection1
= {}
300 elseif chord
== 'S-up' then
301 if State
.selection1
.line
== nil then
302 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
305 elseif chord
== 'S-down' then
306 if State
.selection1
.line
== nil then
307 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
310 elseif chord
== 'pageup' then
312 State
.selection1
= {}
313 elseif chord
== 'pagedown' then
315 State
.selection1
= {}
316 elseif chord
== 'S-pageup' then
317 if State
.selection1
.line
== nil then
318 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
321 elseif chord
== 'S-pagedown' then
322 if State
.selection1
.line
== nil then
323 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
329 function Text
.insert_return(State
)
330 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
331 table.insert(State
.lines
, State
.cursor1
.line
+1, {mode
='text', data
=string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_offset
)})
332 table.insert(State
.line_cache
, State
.cursor1
.line
+1, {})
333 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_offset
-1)
334 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
335 State
.cursor1
= {line
=State
.cursor1
.line
+1, pos
=1}
338 function Text
.pageup(State
)
340 -- duplicate some logic from love.draw
341 local top2
= Text
.to2(State
, State
.screen_top1
)
342 --? print(App.screen.height)
343 local y
= App
.screen
.height
- State
.line_height
344 while y
>= State
.top
do
345 --? print(y, top2.line, top2.screen_line, top2.screen_pos)
346 if State
.screen_top1
.line
== 1 and State
.screen_top1
.pos
== 1 then break end
347 if State
.lines
[State
.screen_top1
.line
].mode
== 'text' then
348 y
= y
- State
.line_height
349 elseif State
.lines
[State
.screen_top1
.line
].mode
== 'drawing' then
350 y
= y
- Drawing_padding_height
- Drawing
.pixels(State
.lines
[State
.screen_top1
.line
].h
, State
.width
)
352 top2
= Text
.previous_screen_line(State
, top2
)
354 State
.screen_top1
= Text
.to1(State
, top2
)
355 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
356 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
357 --? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
358 --? print('pageup end')
361 function Text
.pagedown(State
)
362 --? print('pagedown')
363 -- If a line/paragraph gets to a page boundary, I often want to scroll
364 -- before I get to the bottom.
365 -- However, only do this if it makes forward progress.
366 local bot2
= Text
.to2(State
, State
.screen_bottom1
)
367 if bot2
.screen_line
> 1 then
368 bot2
.screen_line
= math
.max(bot2
.screen_line
-10, 1)
370 local new_top1
= Text
.to1(State
, bot2
)
371 if Text
.lt1(State
.screen_top1
, new_top1
) then
372 State
.screen_top1
= new_top1
374 State
.screen_top1
= {line
=State
.screen_bottom1
.line
, pos
=State
.screen_bottom1
.pos
}
376 --? print('setting top to', State.screen_top1.line, State.screen_top1.pos)
377 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
378 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
379 --? print('top now', State.screen_top1.line)
380 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
381 --? print('pagedown end')
384 function Text
.up(State
)
385 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
386 --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
387 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
388 if screen_line_starting_pos
== 1 then
389 --? print('cursor is at first screen line of its line')
390 -- line is done; skip to previous text line
391 local new_cursor_line
= State
.cursor1
.line
392 while new_cursor_line
> 1 do
393 new_cursor_line
= new_cursor_line
-1
394 if State
.lines
[new_cursor_line
].mode
== 'text' then
395 --? print('found previous text line')
396 State
.cursor1
= {line
=new_cursor_line
, pos
=nil}
397 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
398 -- previous text line found, pick its final screen line
399 --? print('has multiple screen lines')
400 local screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
401 --? print(#screen_line_starting_pos)
402 screen_line_starting_pos
= screen_line_starting_pos
[#screen_line_starting_pos
]
403 local screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_pos
)
404 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_byte_offset
)
405 State
.cursor1
.pos
= screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
410 -- move up one screen line in current line
411 assert(screen_line_index
> 1)
412 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
-1]
413 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
414 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
415 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
416 --? print('cursor pos is now '..tostring(State.cursor1.pos))
418 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
419 State
.screen_top1
= {
420 line
=State
.cursor1
.line
,
421 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
423 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
427 function Text
.down(State
)
428 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
429 --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
430 assert(State
.cursor1
.pos
)
431 if Text
.cursor_at_final_screen_line(State
) then
432 -- line is done, skip to next text line
433 --? print('cursor at final screen line of its line')
434 local new_cursor_line
= State
.cursor1
.line
435 while new_cursor_line
< #State
.lines
do
436 new_cursor_line
= new_cursor_line
+1
437 if State
.lines
[new_cursor_line
].mode
== 'text' then
439 line
= new_cursor_line
,
440 pos
= Text
.nearest_cursor_pos(State
.lines
[new_cursor_line
].data
, State
.cursor_x
, State
.left
),
442 --? print(State.cursor1.pos)
446 if State
.cursor1
.line
> State
.screen_bottom1
.line
then
447 --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
448 --? print('scroll up preserving cursor')
449 Text
.snap_cursor_to_bottom_of_screen(State
)
450 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
453 -- move down one screen line in current line
454 local scroll_down
= Text
.le1(State
.screen_bottom1
, State
.cursor1
)
455 --? print('cursor is NOT at final screen line of its line')
456 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
457 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
458 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
+1]
459 --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
460 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
461 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
462 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
463 --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
465 --? print('scroll up preserving cursor')
466 Text
.snap_cursor_to_bottom_of_screen(State
)
467 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
470 --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
473 function Text
.start_of_line(State
)
474 State
.cursor1
.pos
= 1
475 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
476 State
.screen_top1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
} -- copy
480 function Text
.end_of_line(State
)
481 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) + 1
482 if Text
.cursor_out_of_screen(State
) then
483 Text
.snap_cursor_to_bottom_of_screen(State
)
487 function Text
.word_left(State
)
488 -- skip some whitespace
490 if State
.cursor1
.pos
== 1 then
493 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%S') then
498 -- skip some non-whitespace
501 if State
.cursor1
.pos
== 1 then
504 assert(State
.cursor1
.pos
> 1)
505 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%s') then
511 function Text
.word_right(State
)
512 -- skip some whitespace
514 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
517 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%S') then
520 Text
.right_without_scroll(State
)
523 Text
.right_without_scroll(State
)
524 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
527 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%s') then
531 if Text
.cursor_out_of_screen(State
) then
532 Text
.snap_cursor_to_bottom_of_screen(State
)
536 function Text
.match(s
, pos
, pat
)
537 local start_offset
= Text
.offset(s
, pos
)
539 local end_offset
= Text
.offset(s
, pos
+1)
540 assert(end_offset
> start_offset
)
541 local curr
= s
:sub(start_offset
, end_offset
-1)
542 return curr
:match(pat
)
545 function Text
.left(State
)
546 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
547 if State
.cursor1
.pos
> 1 then
548 State
.cursor1
.pos
= State
.cursor1
.pos
-1
550 local new_cursor_line
= State
.cursor1
.line
551 while new_cursor_line
> 1 do
552 new_cursor_line
= new_cursor_line
-1
553 if State
.lines
[new_cursor_line
].mode
== 'text' then
555 line
= new_cursor_line
,
556 pos
= utf8
.len(State
.lines
[new_cursor_line
].data
) + 1,
562 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
563 State
.screen_top1
= {
564 line
=State
.cursor1
.line
,
565 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
567 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
571 function Text
.right(State
)
572 Text
.right_without_scroll(State
)
573 if Text
.cursor_out_of_screen(State
) then
574 Text
.snap_cursor_to_bottom_of_screen(State
)
578 function Text
.right_without_scroll(State
)
579 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
580 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
581 State
.cursor1
.pos
= State
.cursor1
.pos
+1
583 local new_cursor_line
= State
.cursor1
.line
584 while new_cursor_line
<= #State
.lines
-1 do
585 new_cursor_line
= new_cursor_line
+1
586 if State
.lines
[new_cursor_line
].mode
== 'text' then
587 State
.cursor1
= {line
=new_cursor_line
, pos
=1}
594 function Text
.pos_at_start_of_screen_line(State
, loc1
)
595 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
596 local line_cache
= State
.line_cache
[loc1
.line
]
597 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
598 local spos
= line_cache
.screen_line_starting_pos
[i
]
599 if spos
<= loc1
.pos
then
606 function Text
.cursor_at_final_screen_line(State
)
607 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
608 local screen_lines
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
609 --? print(screen_lines[#screen_lines], State.cursor1.pos)
610 return screen_lines
[#screen_lines
] <= State
.cursor1
.pos
613 function Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
615 while State
.cursor1
.line
<= #State
.lines
do
616 if State
.lines
[State
.cursor1
.line
].mode
== 'text' then
619 --? print('cursor skips', State.cursor1.line)
620 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[State
.cursor1
.line
].h
, State
.width
)
621 State
.cursor1
.line
= State
.cursor1
.line
+ 1
623 if State
.cursor1
.pos
== nil then
624 State
.cursor1
.pos
= 1
626 -- hack: insert a text line at bottom of file if necessary
627 if State
.cursor1
.line
> #State
.lines
then
628 assert(State
.cursor1
.line
== #State
.lines
+1)
629 table.insert(State
.lines
, {mode
='text', data
=''})
630 table.insert(State
.line_cache
, {})
632 --? print(y, App.screen.height, App.screen.height-State.line_height)
633 if y
> App
.screen
.height
- State
.line_height
then
634 --? print('scroll up')
635 Text
.snap_cursor_to_bottom_of_screen(State
)
639 -- should never modify State.cursor1
640 function Text
.snap_cursor_to_bottom_of_screen(State
)
641 --? print('to2:', State.cursor1.line, State.cursor1.pos)
642 local top2
= Text
.to2(State
, State
.cursor1
)
643 --? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
644 -- slide to start of screen line
645 top2
.screen_pos
= 1 -- start of screen line
646 --? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
647 --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
648 local y
= App
.screen
.height
- State
.line_height
649 -- duplicate some logic from love.draw
651 --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
652 if top2
.line
== 1 and top2
.screen_line
== 1 then break end
653 if top2
.screen_line
> 1 or State
.lines
[top2
.line
-1].mode
== 'text' then
654 local h
= State
.line_height
655 if y
- h
< State
.top
then
660 assert(top2
.line
> 1)
661 assert(State
.lines
[top2
.line
-1].mode
== 'drawing')
662 -- We currently can't draw partial drawings, so either skip it entirely
664 local h
= Drawing_padding_height
+ Drawing
.pixels(State
.lines
[top2
.line
-1].h
, State
.width
)
665 if y
- h
< State
.top
then
668 --? print('skipping drawing of height', h)
671 top2
= Text
.previous_screen_line(State
, top2
)
673 --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
674 State
.screen_top1
= Text
.to1(State
, top2
)
675 --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
676 --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
677 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
680 function Text
.in_line(State
, line_index
, x
,y
)
681 local line
= State
.lines
[line_index
]
682 local line_cache
= State
.line_cache
[line_index
]
683 if line_cache
.starty
== nil then return false end -- outside current page
684 if y
< line_cache
.starty
then return false end
685 Text
.populate_screen_line_starting_pos(State
, line_index
)
686 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)
689 -- convert mx,my in pixels to schema-1 coordinates
690 function Text
.to_pos_on_line(State
, line_index
, mx
, my
)
691 local line
= State
.lines
[line_index
]
692 local line_cache
= State
.line_cache
[line_index
]
693 assert(my
>= line_cache
.starty
)
694 -- duplicate some logic from Text.draw
695 local y
= line_cache
.starty
696 local start_screen_line_index
= Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
)
697 for screen_line_index
= start_screen_line_index
,#line_cache
.screen_line_starting_pos
do
698 local screen_line_starting_pos
= line_cache
.screen_line_starting_pos
[screen_line_index
]
699 local screen_line_starting_byte_offset
= Text
.offset(line
.data
, screen_line_starting_pos
)
700 --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
701 local nexty
= y
+ State
.line_height
703 -- On all wrapped screen lines but the final one, clicks past end of
704 -- line position cursor on final character of screen line.
705 -- (The final screen line positions past end of screen line as always.)
706 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
707 --? print('past end of non-final line; return')
708 return line_cache
.screen_line_starting_pos
[screen_line_index
+1]-1
710 local s
= string.sub(line
.data
, screen_line_starting_byte_offset
)
711 --? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
712 return screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, mx
, State
.left
) - 1
719 function Text
.screen_line_width(State
, line_index
, i
)
720 local line
= State
.lines
[line_index
]
721 local line_cache
= State
.line_cache
[line_index
]
722 local start_pos
= line_cache
.screen_line_starting_pos
[i
]
723 local start_offset
= Text
.offset(line
.data
, start_pos
)
725 if i
< #line_cache
.screen_line_starting_pos
then
726 local past_end_pos
= line_cache
.screen_line_starting_pos
[i
+1]
727 local past_end_offset
= Text
.offset(line
.data
, past_end_pos
)
728 screen_line
= string.sub(line
.data
, start_offset
, past_end_offset
-1)
730 screen_line
= string.sub(line
.data
, start_pos
)
732 return App
.width(screen_line
)
735 function Text
.screen_line_index(screen_line_starting_pos
, pos
)
736 for i
= #screen_line_starting_pos
,1,-1 do
737 if screen_line_starting_pos
[i
] <= pos
then
743 -- convert x pixel coordinate to pos
744 -- oblivious to wrapping
745 -- result: 1 to len+1
746 function Text
.nearest_cursor_pos(line
, x
, left
)
750 local len
= utf8
.len(line
)
751 local max_x
= left
+Text
.x(line
, len
+1)
755 local leftpos
, rightpos
= 1, len
+1
756 --? print('-- nearest', x)
758 --? print('nearest', x, '^'..line..'$', leftpos, rightpos)
759 if leftpos
== rightpos
then
762 local curr
= math
.floor((leftpos
+rightpos
)/2)
763 local currxmin
= left
+Text
.x(line
, curr
)
764 local currxmax
= left
+Text
.x(line
, curr
+1)
765 --? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
766 if currxmin
<= x
and x
< currxmax
then
767 if x
-currxmin
< currxmax
-x
then
773 if leftpos
>= rightpos
-1 then
785 -- return the nearest index of line (in utf8 code points) which lies entirely
786 -- within x pixels of the left margin
787 -- result: 0 to len+1
788 function Text
.nearest_pos_less_than(line
, x
)
789 --? print('', '-- nearest_pos_less_than', line, x)
790 local len
= utf8
.len(line
)
791 local max_x
= Text
.x_after(line
, len
)
795 local left
, right
= 0, len
+1
797 local curr
= math
.floor((left
+right
)/2)
798 local currxmin
= Text
.x_after(line
, curr
+1)
799 local currxmax
= Text
.x_after(line
, curr
+2)
800 --? print('', x, left, right, curr, currxmin, currxmax)
801 if currxmin
<= x
and x
< currxmax
then
804 if left
>= right
-1 then
816 function Text
.x_after(s
, pos
)
817 local offset
= Text
.offset(s
, math
.min(pos
+1, #s
+1))
818 local s_before
= s
:sub(1, offset
-1)
819 --? print('^'..s_before..'$')
820 return App
.width(s_before
)
823 function Text
.x(s
, pos
)
824 local offset
= Text
.offset(s
, pos
)
825 local s_before
= s
:sub(1, offset
-1)
826 return App
.width(s_before
)
829 function Text
.to2(State
, loc1
)
830 if State
.lines
[loc1
.line
].mode
== 'drawing' then
831 return {line
=loc1
.line
, screen_line
=1, screen_pos
=1}
833 local result
= {line
=loc1
.line
}
834 local line_cache
= State
.line_cache
[loc1
.line
]
835 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
836 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
837 local spos
= line_cache
.screen_line_starting_pos
[i
]
838 if spos
<= loc1
.pos
then
839 result
.screen_line
= i
840 result
.screen_pos
= loc1
.pos
- spos
+ 1
844 assert(result
.screen_pos
)
848 function Text
.to1(State
, loc2
)
849 local result
= {line
=loc2
.line
, pos
=loc2
.screen_pos
}
850 if loc2
.screen_line
> 1 then
851 result
.pos
= State
.line_cache
[loc2
.line
].screen_line_starting_pos
[loc2
.screen_line
] + loc2
.screen_pos
- 1
856 function Text
.eq1(a
, b
)
857 return a
.line
== b
.line
and a
.pos
== b
.pos
860 function Text
.lt1(a
, b
)
861 if a
.line
< b
.line
then
864 if a
.line
> b
.line
then
870 function Text
.le1(a
, b
)
871 if a
.line
< b
.line
then
874 if a
.line
> b
.line
then
877 return a
.pos
<= b
.pos
880 function Text
.offset(s
, pos1
)
881 if pos1
== 1 then return 1 end
882 local result
= utf8
.offset(s
, pos1
)
883 if result
== nil then
890 function Text
.previous_screen_line(State
, loc2
)
891 if loc2
.screen_line
> 1 then
892 return {line
=loc2
.line
, screen_line
=loc2
.screen_line
-1, screen_pos
=1}
893 elseif loc2
.line
== 1 then
895 elseif State
.lines
[loc2
.line
-1].mode
== 'drawing' then
896 return {line
=loc2
.line
-1, screen_line
=1, screen_pos
=1}
898 local l
= State
.lines
[loc2
.line
-1]
899 Text
.populate_screen_line_starting_pos(State
, loc2
.line
-1)
900 return {line
=loc2
.line
-1, screen_line
=#State
.line_cache
[loc2
.line
-1].screen_line_starting_pos
, screen_pos
=1}
905 function Text
.tweak_screen_top_and_cursor(State
)
906 if State
.screen_top1
.pos
== 1 then return end
907 Text
.populate_screen_line_starting_pos(State
, State
.screen_top1
.line
)
908 local line
= State
.lines
[State
.screen_top1
.line
]
909 local line_cache
= State
.line_cache
[State
.screen_top1
.line
]
910 for i
=2,#line_cache
.screen_line_starting_pos
do
911 local pos
= line_cache
.screen_line_starting_pos
[i
]
912 if pos
== State
.screen_top1
.pos
then
915 if pos
> State
.screen_top1
.pos
then
916 -- make sure screen top is at start of a screen line
917 local prev
= line_cache
.screen_line_starting_pos
[i
-1]
918 if State
.screen_top1
.pos
- prev
< pos
- State
.screen_top1
.pos
then
919 State
.screen_top1
.pos
= prev
921 State
.screen_top1
.pos
= pos
926 -- make sure cursor is on screen
927 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
928 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
929 elseif State
.cursor1
.line
>= State
.screen_bottom1
.line
then
931 if Text
.cursor_out_of_screen(State
) then
934 line
=State
.screen_bottom1
.line
,
935 pos
=Text
.to_pos_on_line(State
, State
.screen_bottom1
.line
, State
.right
-5, App
.screen
.height
-5),
941 -- slightly expensive since it redraws the screen
942 function Text
.cursor_out_of_screen(State
)
944 return State
.cursor_y
== nil
945 -- this approach is cheaper and almost works, except on the final screen
946 -- where file ends above bottom of screen
947 --? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)
948 --? local botline1 = {line=State.cursor1.line, pos=botpos}
949 --? return Text.lt1(State.screen_bottom1, botline1)
952 function Text
.redraw_all(State
)
953 --? print('clearing fragments')
954 State
.line_cache
= {}
955 for i
=1,#State
.lines
do
956 State
.line_cache
[i
] = {}
960 function Text
.clear_screen_line_cache(State
, line_index
)
961 State
.line_cache
[line_index
].screen_line_starting_pos
= nil
965 return s
:gsub('^%s+', ''):gsub('%s+$', '')
969 return s
:gsub('^%s+', '')
973 return s
:gsub('%s+$', '')
976 function starts_with(s
, prefix
)
981 if s
:sub(i
,i
) ~= prefix
:sub(i
,i
) then
988 function ends_with(s
, suffix
)
993 if s
:sub(#s
-i
,#s
-i
) ~= suffix
:sub(#suffix
-i
,#suffix
-i
) then