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
)
15 App
.color(Line_number_color
)
16 love
.graphics
.print(line_index
, State
.left
-Line_number_width
*App
.width('m')+10,y
)
18 assert(#line_cache
.screen_line_starting_pos
>= 1)
19 for i
=1,#line_cache
.screen_line_starting_pos
do
20 local pos
= line_cache
.screen_line_starting_pos
[i
]
21 if pos
< startpos
then
23 --? print('skipping', screen_line)
25 final_screen_line_starting_pos
= pos
26 local screen_line
= Text
.screen_line(line
, line_cache
, i
)
27 --? print('text.draw:', screen_line, 'at', line_index,pos, 'after', x,y)
28 local frag_len
= utf8
.len(screen_line
)
29 -- render any highlights
30 for _
,link_offsets
in ipairs(line_cache
.link_offsets
) do
31 -- render link decorations
32 local s
,e
,filename
= unpack(link_offsets
)
33 local lo
, hi
= Text
.clip_wikiword_with_screen_line(line
, line_cache
, i
, s
, e
)
35 button(State
, 'link', {x
=State
.left
+lo
, y
=y
, w
=hi
-lo
, h
=State
.line_height
, color
={1,1,1},
36 icon
= icon
.hyperlink_decoration
,
38 if file_exists(filename
) then
39 source
.switch_to_file(filename
)
45 if State
.selection1
.line
then
46 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
47 Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, lo
,hi
)
49 if not hide_cursor
and line_index
== State
.cursor1
.line
then
50 -- render search highlight or cursor
51 if State
.search_term
then
52 local data
= State
.lines
[State
.cursor1
.line
].data
53 local cursor_offset
= Text
.offset(data
, State
.cursor1
.pos
)
54 if data
:sub(cursor_offset
, cursor_offset
+#State
.search_term
-1) == State
.search_term
then
55 local save_selection
= State
.selection1
56 State
.selection1
= {line
=line_index
, pos
=State
.cursor1
.pos
+utf8
.len(State
.search_term
)}
57 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
58 Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, lo
,hi
)
59 State
.selection1
= save_selection
61 elseif Focus
== 'edit' then
62 if pos
<= State
.cursor1
.pos
and pos
+ frag_len
> State
.cursor1
.pos
then
63 Text
.draw_cursor(State
, State
.left
+Text
.x(screen_line
, State
.cursor1
.pos
-pos
+1), y
)
64 elseif pos
+ frag_len
== State
.cursor1
.pos
then
65 -- Show cursor at end of line.
66 -- This place also catches end of wrapping screen lines. That doesn't seem worth distinguishing.
67 -- It seems useful to see a cursor whether your eye is on the left or right margin.
68 Text
.draw_cursor(State
, State
.left
+Text
.x(screen_line
, State
.cursor1
.pos
-pos
+1), y
)
72 -- render colorized text
74 for frag
in screen_line
:gmatch('%S*%s*') do
76 App
.screen
.print(frag
, x
,y
)
79 y
= y
+ State
.line_height
80 if y
>= App
.screen
.height
then
85 return y
, final_screen_line_starting_pos
88 function Text
.screen_line(line
, line_cache
, i
)
89 local pos
= line_cache
.screen_line_starting_pos
[i
]
90 local offset
= Text
.offset(line
.data
, pos
)
91 if i
>= #line_cache
.screen_line_starting_pos
then
92 return line
.data
:sub(offset
)
94 local endpos
= line_cache
.screen_line_starting_pos
[i
+1]-1
95 local end_offset
= Text
.offset(line
.data
, endpos
)
96 return line
.data
:sub(offset
, end_offset
)
99 function Text
.draw_cursor(State
, x
, y
)
101 if math
.floor(Cursor_time
*2)%2 == 0 then
102 App
.color(Cursor_color
)
103 love
.graphics
.rectangle('fill', x
,y
, 3,State
.line_height
)
106 State
.cursor_y
= y
+State
.line_height
109 function Text
.populate_screen_line_starting_pos(State
, line_index
)
110 local line
= State
.lines
[line_index
]
111 if line
.mode
~= 'text' then return end
112 local line_cache
= State
.line_cache
[line_index
]
113 if line_cache
.screen_line_starting_pos
then
116 line_cache
.screen_line_starting_pos
= {1}
119 -- try to wrap at word boundaries
120 for frag
in line
.data
:gmatch('%S*%s*') do
121 local frag_width
= App
.width(frag
)
122 --? print('-- frag:', frag, pos, x, frag_width, State.width)
123 while x
+ frag_width
> State
.width
do
124 --? print('frag:', frag, pos, x, frag_width, State.width)
125 if x
< 0.8 * State
.width
then
126 -- long word; chop it at some letter
127 -- We're not going to reimplement TeX here.
128 local bpos
= Text
.nearest_pos_less_than(frag
, State
.width
- x
)
129 -- everything works if bpos == 0, but is a little inefficient
131 local boffset
= Text
.offset(frag
, bpos
+1) -- byte _after_ bpos
132 frag
= string.sub(frag
, boffset
)
134 --? print('after chop:', frag)
136 frag_width
= App
.width(frag
)
138 --? print('screen line:', pos)
139 table.insert(line_cache
.screen_line_starting_pos
, pos
)
140 x
= 0 -- new screen line
143 pos
= pos
+ utf8
.len(frag
)
147 function Text
.populate_link_offsets(State
, line_index
)
148 local line
= State
.lines
[line_index
]
149 if line
.mode
~= 'text' then return end
150 local line_cache
= State
.line_cache
[line_index
]
151 if line_cache
.link_offsets
then
154 line_cache
.link_offsets
= {}
156 -- try to wrap at word boundaries
158 while s
<= #line
.data
do
159 s
, e
= line
.data
:find('%[%[%S+%]%]', s
)
160 if s
== nil then break end
161 local word
= line
.data
:sub(s
+2, e
-2) -- strip out surrounding '[[..]]'
162 --? print('wikiword:', s, e, word)
163 table.insert(line_cache
.link_offsets
, {s
, e
, word
})
168 -- Intersect the filename between byte offsets s,e with the bounds of screen line i.
169 -- Return the left/right pixel coordinates of of the intersection,
170 -- or nil if it doesn't intersect with screen line i.
171 function Text
.clip_wikiword_with_screen_line(line
, line_cache
, i
, s
, e
)
172 local spos
= line_cache
.screen_line_starting_pos
[i
]
173 local soff
= Text
.offset(line
.data
, spos
)
178 if i
< #line_cache
.screen_line_starting_pos
then
179 local epos
= line_cache
.screen_line_starting_pos
[i
+1]
180 eoff
= Text
.offset(line
.data
, epos
)
185 local loff
= math
.max(s
, soff
)
188 hoff
= math
.min(e
, eoff
)
192 --? print(s, e, soff, eoff, loff, hoff)
193 return App
.width(line
.data
:sub(soff
, loff
-1)), App
.width(line
.data
:sub(soff
, hoff
))
196 function Text
.text_input(State
, t
)
197 if App
.mouse_down(1) then return end
198 if App
.ctrl_down() or App
.alt_down() or App
.cmd_down() then return end
199 local before
= snapshot(State
, State
.cursor1
.line
)
200 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
201 Text
.insert_at_cursor(State
, t
)
202 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
203 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
204 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
206 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
209 function Text
.insert_at_cursor(State
, t
)
210 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
211 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
212 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
)
213 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
214 State
.cursor1
.pos
= State
.cursor1
.pos
+1
217 -- Don't handle any keys here that would trigger text_input above.
218 function Text
.keychord_press(State
, chord
)
219 --? print('chord', chord, State.selection1.line, State.selection1.pos)
220 --== shortcuts that mutate text
221 if chord
== 'return' then
222 local before_line
= State
.cursor1
.line
223 local before
= snapshot(State
, before_line
)
224 Text
.insert_return(State
)
225 State
.selection1
= {}
226 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
227 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
230 record_undo_event(State
, {before
=before
, after
=snapshot(State
, before_line
, State
.cursor1
.line
)})
231 elseif chord
== 'tab' then
232 local before
= snapshot(State
, State
.cursor1
.line
)
233 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
234 Text
.insert_at_cursor(State
, '\t')
235 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
236 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
237 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
238 --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
241 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
242 elseif chord
== 'backspace' then
243 if State
.selection1
.line
then
244 Text
.delete_selection(State
, State
.left
, State
.right
)
249 if State
.cursor1
.pos
> 1 then
250 before
= snapshot(State
, State
.cursor1
.line
)
251 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1)
252 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
255 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
)
257 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
259 State
.cursor1
.pos
= State
.cursor1
.pos
-1
261 elseif State
.cursor1
.line
> 1 then
262 before
= snapshot(State
, State
.cursor1
.line
-1, State
.cursor1
.line
)
263 if State
.lines
[State
.cursor1
.line
-1].mode
== 'drawing' then
264 table.remove(State
.lines
, State
.cursor1
.line
-1)
265 table.remove(State
.line_cache
, State
.cursor1
.line
-1)
268 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
-1].data
)+1
269 State
.lines
[State
.cursor1
.line
-1].data
= State
.lines
[State
.cursor1
.line
-1].data
..State
.lines
[State
.cursor1
.line
].data
270 table.remove(State
.lines
, State
.cursor1
.line
)
271 table.remove(State
.line_cache
, State
.cursor1
.line
)
273 State
.cursor1
.line
= State
.cursor1
.line
-1
275 if State
.screen_top1
.line
> #State
.lines
then
276 Text
.populate_screen_line_starting_pos(State
, #State
.lines
)
277 local line_cache
= State
.line_cache
[#State
.line_cache
]
278 State
.screen_top1
= {line
=#State
.lines
, pos
=line_cache
.screen_line_starting_pos
[#line_cache
.screen_line_starting_pos
]}
279 elseif Text
.lt1(State
.cursor1
, State
.screen_top1
) then
280 State
.screen_top1
= {
281 line
=State
.cursor1
.line
,
282 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
284 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
286 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
287 assert(Text
.le1(State
.screen_top1
, State
.cursor1
))
289 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
290 elseif chord
== 'delete' then
291 if State
.selection1
.line
then
292 Text
.delete_selection(State
, State
.left
, State
.right
)
297 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
298 before
= snapshot(State
, State
.cursor1
.line
)
300 before
= snapshot(State
, State
.cursor1
.line
, State
.cursor1
.line
+1)
302 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
303 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
304 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
+1)
307 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
)
309 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
311 -- no change to State.cursor1.pos
313 elseif State
.cursor1
.line
< #State
.lines
then
314 if State
.lines
[State
.cursor1
.line
+1].mode
== 'text' then
316 State
.lines
[State
.cursor1
.line
].data
= State
.lines
[State
.cursor1
.line
].data
..State
.lines
[State
.cursor1
.line
+1].data
318 table.remove(State
.lines
, State
.cursor1
.line
+1)
319 table.remove(State
.line_cache
, State
.cursor1
.line
+1)
321 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
323 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
324 --== shortcuts that move the cursor
325 elseif chord
== 'left' then
327 State
.selection1
= {}
328 elseif chord
== 'right' then
330 State
.selection1
= {}
331 elseif chord
== 'S-left' then
332 if State
.selection1
.line
== nil then
333 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
336 elseif chord
== 'S-right' then
337 if State
.selection1
.line
== nil then
338 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
341 -- C- hotkeys reserved for drawings, so we'll use M-
342 elseif chord
== 'M-left' then
343 Text
.word_left(State
)
344 State
.selection1
= {}
345 elseif chord
== 'M-right' then
346 Text
.word_right(State
)
347 State
.selection1
= {}
348 elseif chord
== 'M-S-left' then
349 if State
.selection1
.line
== nil then
350 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
352 Text
.word_left(State
)
353 elseif chord
== 'M-S-right' then
354 if State
.selection1
.line
== nil then
355 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
357 Text
.word_right(State
)
358 elseif chord
== 'home' then
359 Text
.start_of_line(State
)
360 State
.selection1
= {}
361 elseif chord
== 'end' then
362 Text
.end_of_line(State
)
363 State
.selection1
= {}
364 elseif chord
== 'S-home' then
365 if State
.selection1
.line
== nil then
366 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
368 Text
.start_of_line(State
)
369 elseif chord
== 'S-end' then
370 if State
.selection1
.line
== nil then
371 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
373 Text
.end_of_line(State
)
374 elseif chord
== 'up' then
376 State
.selection1
= {}
377 elseif chord
== 'down' then
379 State
.selection1
= {}
380 elseif chord
== 'S-up' then
381 if State
.selection1
.line
== nil then
382 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
385 elseif chord
== 'S-down' then
386 if State
.selection1
.line
== nil then
387 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
390 elseif chord
== 'pageup' then
392 State
.selection1
= {}
393 elseif chord
== 'pagedown' then
395 State
.selection1
= {}
396 elseif chord
== 'S-pageup' then
397 if State
.selection1
.line
== nil then
398 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
401 elseif chord
== 'S-pagedown' then
402 if State
.selection1
.line
== nil then
403 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
409 function Text
.insert_return(State
)
410 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
411 table.insert(State
.lines
, State
.cursor1
.line
+1, {mode
='text', data
=string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_offset
)})
412 table.insert(State
.line_cache
, State
.cursor1
.line
+1, {})
413 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_offset
-1)
414 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
415 State
.cursor1
= {line
=State
.cursor1
.line
+1, pos
=1}
418 function Text
.pageup(State
)
420 -- duplicate some logic from love.draw
421 local top2
= Text
.to2(State
, State
.screen_top1
)
422 --? print(App.screen.height)
423 local y
= App
.screen
.height
- State
.line_height
424 while y
>= State
.top
do
425 --? print(y, top2.line, top2.screen_line, top2.screen_pos)
426 if State
.screen_top1
.line
== 1 and State
.screen_top1
.pos
== 1 then break end
427 if State
.lines
[State
.screen_top1
.line
].mode
== 'text' then
428 y
= y
- State
.line_height
429 elseif State
.lines
[State
.screen_top1
.line
].mode
== 'drawing' then
430 y
= y
- Drawing_padding_height
- Drawing
.pixels(State
.lines
[State
.screen_top1
.line
].h
, State
.width
)
432 top2
= Text
.previous_screen_line(State
, top2
)
434 State
.screen_top1
= Text
.to1(State
, top2
)
435 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
436 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
437 --? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
438 --? print('pageup end')
441 function Text
.pagedown(State
)
442 --? print('pagedown')
443 State
.screen_top1
= {line
=State
.screen_bottom1
.line
, pos
=State
.screen_bottom1
.pos
}
444 --? print('setting top to', State.screen_top1.line, State.screen_top1.pos)
445 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
446 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
447 --? print('top now', State.screen_top1.line)
448 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
449 --? print('pagedown end')
452 function Text
.up(State
)
453 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
454 --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
455 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
456 if screen_line_starting_pos
== 1 then
457 --? print('cursor is at first screen line of its line')
458 -- line is done; skip to previous text line
459 local new_cursor_line
= State
.cursor1
.line
460 while new_cursor_line
> 1 do
461 new_cursor_line
= new_cursor_line
-1
462 if State
.lines
[new_cursor_line
].mode
== 'text' then
463 --? print('found previous text line')
464 State
.cursor1
= {line
=new_cursor_line
, pos
=nil}
465 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
466 -- previous text line found, pick its final screen line
467 --? print('has multiple screen lines')
468 local screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
469 --? print(#screen_line_starting_pos)
470 screen_line_starting_pos
= screen_line_starting_pos
[#screen_line_starting_pos
]
471 local screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_pos
)
472 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_byte_offset
)
473 State
.cursor1
.pos
= screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
478 -- move up one screen line in current line
479 assert(screen_line_index
> 1)
480 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
-1]
481 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
482 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
483 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
484 --? print('cursor pos is now '..tostring(State.cursor1.pos))
486 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
487 State
.screen_top1
= {
488 line
=State
.cursor1
.line
,
489 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
491 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
495 function Text
.down(State
)
496 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
497 --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
498 assert(State
.cursor1
.pos
)
499 if Text
.cursor_at_final_screen_line(State
) then
500 -- line is done, skip to next text line
501 --? print('cursor at final screen line of its line')
502 local new_cursor_line
= State
.cursor1
.line
503 while new_cursor_line
< #State
.lines
do
504 new_cursor_line
= new_cursor_line
+1
505 if State
.lines
[new_cursor_line
].mode
== 'text' then
507 line
= new_cursor_line
,
508 pos
= Text
.nearest_cursor_pos(State
.lines
[new_cursor_line
].data
, State
.cursor_x
, State
.left
),
510 --? print(State.cursor1.pos)
514 if State
.cursor1
.line
> State
.screen_bottom1
.line
then
515 --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
516 --? print('scroll up preserving cursor')
517 Text
.snap_cursor_to_bottom_of_screen(State
)
518 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
521 -- move down one screen line in current line
522 local scroll_down
= Text
.le1(State
.screen_bottom1
, State
.cursor1
)
523 --? print('cursor is NOT at final screen line of its line')
524 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
525 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
526 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
+1]
527 --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
528 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
529 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
530 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
531 --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
533 --? print('scroll up preserving cursor')
534 Text
.snap_cursor_to_bottom_of_screen(State
)
535 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
538 --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
541 function Text
.start_of_line(State
)
542 State
.cursor1
.pos
= 1
543 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
544 State
.screen_top1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
} -- copy
548 function Text
.end_of_line(State
)
549 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) + 1
550 if Text
.cursor_out_of_screen(State
) then
551 Text
.snap_cursor_to_bottom_of_screen(State
)
555 function Text
.word_left(State
)
556 -- skip some whitespace
558 if State
.cursor1
.pos
== 1 then
561 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%S') then
566 -- skip some non-whitespace
569 if State
.cursor1
.pos
== 1 then
572 assert(State
.cursor1
.pos
> 1)
573 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%s') then
579 function Text
.word_right(State
)
580 -- skip some whitespace
582 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
585 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%S') then
588 Text
.right_without_scroll(State
)
591 Text
.right_without_scroll(State
)
592 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
595 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%s') then
599 if Text
.cursor_out_of_screen(State
) then
600 Text
.snap_cursor_to_bottom_of_screen(State
)
604 function Text
.match(s
, pos
, pat
)
605 local start_offset
= Text
.offset(s
, pos
)
607 local end_offset
= Text
.offset(s
, pos
+1)
608 assert(end_offset
> start_offset
)
609 local curr
= s
:sub(start_offset
, end_offset
-1)
610 return curr
:match(pat
)
613 function Text
.left(State
)
614 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
615 if State
.cursor1
.pos
> 1 then
616 State
.cursor1
.pos
= State
.cursor1
.pos
-1
618 local new_cursor_line
= State
.cursor1
.line
619 while new_cursor_line
> 1 do
620 new_cursor_line
= new_cursor_line
-1
621 if State
.lines
[new_cursor_line
].mode
== 'text' then
623 line
= new_cursor_line
,
624 pos
= utf8
.len(State
.lines
[new_cursor_line
].data
) + 1,
630 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
631 State
.screen_top1
= {
632 line
=State
.cursor1
.line
,
633 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
635 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
639 function Text
.right(State
)
640 Text
.right_without_scroll(State
)
641 if Text
.cursor_out_of_screen(State
) then
642 Text
.snap_cursor_to_bottom_of_screen(State
)
646 function Text
.right_without_scroll(State
)
647 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
648 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
649 State
.cursor1
.pos
= State
.cursor1
.pos
+1
651 local new_cursor_line
= State
.cursor1
.line
652 while new_cursor_line
<= #State
.lines
-1 do
653 new_cursor_line
= new_cursor_line
+1
654 if State
.lines
[new_cursor_line
].mode
== 'text' then
655 State
.cursor1
= {line
=new_cursor_line
, pos
=1}
662 -- result: pos, index of screen line
663 function Text
.pos_at_start_of_screen_line(State
, loc1
)
664 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
665 local line_cache
= State
.line_cache
[loc1
.line
]
666 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
667 local spos
= line_cache
.screen_line_starting_pos
[i
]
668 if spos
<= loc1
.pos
then
675 function Text
.pos_at_end_of_screen_line(State
, loc1
)
676 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
677 local line_cache
= State
.line_cache
[loc1
.line
]
678 local most_recent_final_pos
= utf8
.len(State
.lines
[loc1
.line
].data
)+1
679 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
680 local spos
= line_cache
.screen_line_starting_pos
[i
]
681 if spos
<= loc1
.pos
then
682 return most_recent_final_pos
684 most_recent_final_pos
= spos
-1
689 function Text
.cursor_at_final_screen_line(State
)
690 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
691 local screen_lines
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
692 --? print(screen_lines[#screen_lines], State.cursor1.pos)
693 return screen_lines
[#screen_lines
] <= State
.cursor1
.pos
696 function Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
698 while State
.cursor1
.line
<= #State
.lines
do
699 if State
.lines
[State
.cursor1
.line
].mode
== 'text' then
702 --? print('cursor skips', State.cursor1.line)
703 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[State
.cursor1
.line
].h
, State
.width
)
704 State
.cursor1
.line
= State
.cursor1
.line
+ 1
706 if State
.cursor1
.pos
== nil then
707 State
.cursor1
.pos
= 1
709 -- hack: insert a text line at bottom of file if necessary
710 if State
.cursor1
.line
> #State
.lines
then
711 assert(State
.cursor1
.line
== #State
.lines
+1)
712 table.insert(State
.lines
, {mode
='text', data
=''})
713 table.insert(State
.line_cache
, {})
715 --? print(y, App.screen.height, App.screen.height-State.line_height)
716 if y
> App
.screen
.height
- State
.line_height
then
717 --? print('scroll up')
718 Text
.snap_cursor_to_bottom_of_screen(State
)
722 -- should never modify State.cursor1
723 function Text
.snap_cursor_to_bottom_of_screen(State
)
724 --? print('to2:', State.cursor1.line, State.cursor1.pos)
725 local top2
= Text
.to2(State
, State
.cursor1
)
726 --? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
727 -- slide to start of screen line
728 top2
.screen_pos
= 1 -- start of screen line
729 --? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
730 --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
731 local y
= App
.screen
.height
- State
.line_height
732 -- duplicate some logic from love.draw
734 --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
735 if top2
.line
== 1 and top2
.screen_line
== 1 then break end
736 if top2
.screen_line
> 1 or State
.lines
[top2
.line
-1].mode
== 'text' then
737 local h
= State
.line_height
738 if y
- h
< State
.top
then
743 assert(top2
.line
> 1)
744 assert(State
.lines
[top2
.line
-1].mode
== 'drawing')
745 -- We currently can't draw partial drawings, so either skip it entirely
747 local h
= Drawing_padding_height
+ Drawing
.pixels(State
.lines
[top2
.line
-1].h
, State
.width
)
748 if y
- h
< State
.top
then
751 --? print('skipping drawing of height', h)
754 top2
= Text
.previous_screen_line(State
, top2
)
756 --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
757 State
.screen_top1
= Text
.to1(State
, top2
)
758 --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
759 --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
760 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
763 function Text
.in_line(State
, line_index
, x
,y
)
764 local line
= State
.lines
[line_index
]
765 local line_cache
= State
.line_cache
[line_index
]
766 if line_cache
.starty
== nil then return false end -- outside current page
767 if y
< line_cache
.starty
then return false end
768 Text
.populate_screen_line_starting_pos(State
, line_index
)
769 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)
772 -- convert mx,my in pixels to schema-1 coordinates
773 function Text
.to_pos_on_line(State
, line_index
, mx
, my
)
774 local line
= State
.lines
[line_index
]
775 local line_cache
= State
.line_cache
[line_index
]
776 assert(my
>= line_cache
.starty
)
777 -- duplicate some logic from Text.draw
778 local y
= line_cache
.starty
779 local start_screen_line_index
= Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
)
780 for screen_line_index
= start_screen_line_index
,#line_cache
.screen_line_starting_pos
do
781 local screen_line_starting_pos
= line_cache
.screen_line_starting_pos
[screen_line_index
]
782 local screen_line_starting_byte_offset
= Text
.offset(line
.data
, screen_line_starting_pos
)
783 --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
784 local nexty
= y
+ State
.line_height
786 -- On all wrapped screen lines but the final one, clicks past end of
787 -- line position cursor on final character of screen line.
788 -- (The final screen line positions past end of screen line as always.)
789 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
790 --? print('past end of non-final line; return')
791 return line_cache
.screen_line_starting_pos
[screen_line_index
+1]-1
793 local s
= string.sub(line
.data
, screen_line_starting_byte_offset
)
794 --? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
795 return screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, mx
, State
.left
) - 1
802 function Text
.screen_line_width(State
, line_index
, i
)
803 local line
= State
.lines
[line_index
]
804 local line_cache
= State
.line_cache
[line_index
]
805 local start_pos
= line_cache
.screen_line_starting_pos
[i
]
806 local start_offset
= Text
.offset(line
.data
, start_pos
)
808 if i
< #line_cache
.screen_line_starting_pos
then
809 local past_end_pos
= line_cache
.screen_line_starting_pos
[i
+1]
810 local past_end_offset
= Text
.offset(line
.data
, past_end_pos
)
811 screen_line
= string.sub(line
.data
, start_offset
, past_end_offset
-1)
813 screen_line
= string.sub(line
.data
, start_pos
)
815 return App
.width(screen_line
)
818 function Text
.screen_line_index(screen_line_starting_pos
, pos
)
819 for i
= #screen_line_starting_pos
,1,-1 do
820 if screen_line_starting_pos
[i
] <= pos
then
826 -- convert x pixel coordinate to pos
827 -- oblivious to wrapping
828 -- result: 1 to len+1
829 function Text
.nearest_cursor_pos(line
, x
, left
)
833 local len
= utf8
.len(line
)
834 local max_x
= left
+Text
.x(line
, len
+1)
838 local leftpos
, rightpos
= 1, len
+1
839 --? print('-- nearest', x)
841 --? print('nearest', x, '^'..line..'$', leftpos, rightpos)
842 if leftpos
== rightpos
then
845 local curr
= math
.floor((leftpos
+rightpos
)/2)
846 local currxmin
= left
+Text
.x(line
, curr
)
847 local currxmax
= left
+Text
.x(line
, curr
+1)
848 --? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
849 if currxmin
<= x
and x
< currxmax
then
850 if x
-currxmin
< currxmax
-x
then
856 if leftpos
>= rightpos
-1 then
868 -- return the nearest index of line (in utf8 code points) which lies entirely
869 -- within x pixels of the left margin
870 -- result: 0 to len+1
871 function Text
.nearest_pos_less_than(line
, x
)
872 --? print('', '-- nearest_pos_less_than', line, x)
873 local len
= utf8
.len(line
)
874 local max_x
= Text
.x_after(line
, len
)
878 local left
, right
= 0, len
+1
880 local curr
= math
.floor((left
+right
)/2)
881 local currxmin
= Text
.x_after(line
, curr
+1)
882 local currxmax
= Text
.x_after(line
, curr
+2)
883 --? print('', x, left, right, curr, currxmin, currxmax)
884 if currxmin
<= x
and x
< currxmax
then
887 if left
>= right
-1 then
899 function Text
.x_after(s
, pos
)
900 local offset
= Text
.offset(s
, math
.min(pos
+1, #s
+1))
901 local s_before
= s
:sub(1, offset
-1)
902 --? print('^'..s_before..'$')
903 return App
.width(s_before
)
906 function Text
.x(s
, pos
)
907 local offset
= Text
.offset(s
, pos
)
908 local s_before
= s
:sub(1, offset
-1)
909 return App
.width(s_before
)
912 function Text
.to2(State
, loc1
)
913 if State
.lines
[loc1
.line
].mode
== 'drawing' then
914 return {line
=loc1
.line
, screen_line
=1, screen_pos
=1}
916 local result
= {line
=loc1
.line
}
917 local line_cache
= State
.line_cache
[loc1
.line
]
918 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
919 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
920 local spos
= line_cache
.screen_line_starting_pos
[i
]
921 if spos
<= loc1
.pos
then
922 result
.screen_line
= i
923 result
.screen_pos
= loc1
.pos
- spos
+ 1
927 assert(result
.screen_pos
)
931 function Text
.to1(State
, loc2
)
932 local result
= {line
=loc2
.line
, pos
=loc2
.screen_pos
}
933 if loc2
.screen_line
> 1 then
934 result
.pos
= State
.line_cache
[loc2
.line
].screen_line_starting_pos
[loc2
.screen_line
] + loc2
.screen_pos
- 1
939 function Text
.eq1(a
, b
)
940 return a
.line
== b
.line
and a
.pos
== b
.pos
943 function Text
.lt1(a
, b
)
944 if a
.line
< b
.line
then
947 if a
.line
> b
.line
then
953 function Text
.le1(a
, b
)
954 if a
.line
< b
.line
then
957 if a
.line
> b
.line
then
960 return a
.pos
<= b
.pos
963 function Text
.offset(s
, pos1
)
964 if pos1
== 1 then return 1 end
965 local result
= utf8
.offset(s
, pos1
)
966 if result
== nil then
973 function Text
.previous_screen_line(State
, loc2
)
974 if loc2
.screen_line
> 1 then
975 return {line
=loc2
.line
, screen_line
=loc2
.screen_line
-1, screen_pos
=1}
976 elseif loc2
.line
== 1 then
978 elseif State
.lines
[loc2
.line
-1].mode
== 'drawing' then
979 return {line
=loc2
.line
-1, screen_line
=1, screen_pos
=1}
981 local l
= State
.lines
[loc2
.line
-1]
982 Text
.populate_screen_line_starting_pos(State
, loc2
.line
-1)
983 return {line
=loc2
.line
-1, screen_line
=#State
.line_cache
[loc2
.line
-1].screen_line_starting_pos
, screen_pos
=1}
988 function Text
.tweak_screen_top_and_cursor(State
)
989 if State
.screen_top1
.pos
== 1 then return end
990 Text
.populate_screen_line_starting_pos(State
, State
.screen_top1
.line
)
991 local line
= State
.lines
[State
.screen_top1
.line
]
992 local line_cache
= State
.line_cache
[State
.screen_top1
.line
]
993 for i
=2,#line_cache
.screen_line_starting_pos
do
994 local pos
= line_cache
.screen_line_starting_pos
[i
]
995 if pos
== State
.screen_top1
.pos
then
998 if pos
> State
.screen_top1
.pos
then
999 -- make sure screen top is at start of a screen line
1000 local prev
= line_cache
.screen_line_starting_pos
[i
-1]
1001 if State
.screen_top1
.pos
- prev
< pos
- State
.screen_top1
.pos
then
1002 State
.screen_top1
.pos
= prev
1004 State
.screen_top1
.pos
= pos
1009 -- make sure cursor is on screen
1010 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
1011 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
1012 elseif State
.cursor1
.line
>= State
.screen_bottom1
.line
then
1013 --? print('too low')
1014 if Text
.cursor_out_of_screen(State
) then
1017 line
=State
.screen_bottom1
.line
,
1018 pos
=Text
.to_pos_on_line(State
, State
.screen_bottom1
.line
, State
.right
-5, App
.screen
.height
-5),
1024 -- slightly expensive since it redraws the screen
1025 function Text
.cursor_out_of_screen(State
)
1027 return State
.cursor_y
== nil
1028 -- this approach is cheaper and almost works, except on the final screen
1029 -- where file ends above bottom of screen
1030 --? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)
1031 --? local botline1 = {line=State.cursor1.line, pos=botpos}
1032 --? return Text.lt1(State.screen_bottom1, botline1)
1035 function Text
.redraw_all(State
)
1036 --? print('clearing fragments')
1037 State
.line_cache
= {}
1038 for i
=1,#State
.lines
do
1039 State
.line_cache
[i
] = {}
1043 function Text
.clear_screen_line_cache(State
, line_index
)
1044 State
.line_cache
[line_index
].screen_line_starting_pos
= nil
1045 State
.line_cache
[line_index
].link_offsets
= nil
1049 return s
:gsub('^%s+', ''):gsub('%s+$', '')
1053 return s
:gsub('^%s+', '')
1057 return s
:gsub('%s+$', '')
1060 function starts_with(s
, prefix
)
1061 if #s
< #prefix
then
1065 if s
:sub(i
,i
) ~= prefix
:sub(i
,i
) then
1072 function ends_with(s
, suffix
)
1073 if #s
< #suffix
then
1076 for i
=0,#suffix
-1 do
1077 if s
:sub(#s
-i
,#s
-i
) ~= suffix
:sub(#suffix
-i
,#suffix
-i
) then