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 the final y, 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
14 local screen_line_starting_pos
= startpos
15 Text
.populate_screen_line_starting_pos(State
, line_index
)
18 for _
, f
in ipairs(line_cache
.fragments
) do
21 local frag_len
= utf8
.len(f
)
22 --? print('text.draw:', f, 'at', line_index,pos, 'after', x,y)
23 if pos
< startpos
then
25 --? print('skipping', f)
28 local frag_width
= App
.width(f
)
29 if x
+ frag_width
> State
.right
then
30 assert(x
> State
.left
) -- no overfull lines
31 y
= y
+ State
.line_height
32 if y
+ State
.line_height
> App
.screen
.height
then
33 return y
, screen_line_starting_pos
35 screen_line_starting_pos
= pos
38 if State
.selection1
.line
then
39 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
40 Text
.draw_highlight(State
, line
, x
,y
, pos
, lo
,hi
)
42 -- Make [[WikiWords]] (single word, all in one screen line) clickable.
43 local trimmed_word
= rtrim(f
) -- compute_fragments puts whitespace at the end
44 if starts_with(trimmed_word
, '[[') and ends_with(trimmed_word
, ']]') then
45 local filename
= trimmed_word
:gsub('^..(.*)..$', '%1')
46 if source
.link_exists(State
, filename
) then
47 button(State
, 'link', {x
=x
+App
.width('[['), y
=y
, w
=App
.width(filename
), h
=State
.line_height
, color
={1,1,1},
48 icon
= icon
.hyperlink_decoration
,
50 source
.switch_to_file(filename
)
55 App
.screen
.print(f
, x
,y
)
56 -- render cursor if necessary
57 if line_index
== State
.cursor1
.line
then
58 if pos
<= State
.cursor1
.pos
and pos
+ frag_len
> State
.cursor1
.pos
then
59 if State
.search_term
then
60 if State
.lines
[State
.cursor1
.line
].data
:sub(State
.cursor1
.pos
, State
.cursor1
.pos
+utf8
.len(State
.search_term
)-1) == State
.search_term
then
61 local lo_px
= Text
.draw_highlight(State
, line
, x
,y
, pos
, State
.cursor1
.pos
, State
.cursor1
.pos
+utf8
.len(State
.search_term
))
63 love
.graphics
.print(State
.search_term
, x
+lo_px
,y
)
65 elseif Focus
== 'edit' then
66 Text
.draw_cursor(State
, x
+Text
.x(f
, State
.cursor1
.pos
-pos
+1), y
)
75 if Focus
== 'edit' and not hide_cursor
and State
.search_term
== nil then
76 if line_index
== State
.cursor1
.line
and State
.cursor1
.pos
== pos
then
77 Text
.draw_cursor(State
, x
, y
)
80 return y
, screen_line_starting_pos
83 function Text
.draw_cursor(State
, x
, y
)
85 if math
.floor(Cursor_time
*2)%2 == 0 then
86 App
.color(Cursor_color
)
87 love
.graphics
.rectangle('fill', x
,y
, 3,State
.line_height
)
90 State
.cursor_y
= y
+State
.line_height
93 function Text
.populate_screen_line_starting_pos(State
, line_index
)
94 local line
= State
.lines
[line_index
]
95 if line
.mode
~= 'text' then return end
96 local line_cache
= State
.line_cache
[line_index
]
97 if line_cache
.screen_line_starting_pos
then
100 -- duplicate some logic from Text.draw
101 Text
.compute_fragments(State
, line_index
)
102 line_cache
.screen_line_starting_pos
= {1}
105 for _
, f
in ipairs(line_cache
.fragments
) do
107 local frag_width
= App
.width(f
)
108 if x
+ frag_width
> State
.right
then
110 table.insert(line_cache
.screen_line_starting_pos
, pos
)
113 pos
= pos
+ utf8
.len(f
)
117 function Text
.compute_fragments(State
, line_index
)
118 local line
= State
.lines
[line_index
]
119 if line
.mode
~= 'text' then return end
120 local line_cache
= State
.line_cache
[line_index
]
121 if line_cache
.fragments
then
124 line_cache
.fragments
= {}
126 -- try to wrap at word boundaries
127 for frag
in line
.data
:gmatch('%S*%s*') do
128 local frag_width
= App
.width(frag
)
129 while x
+ frag_width
> State
.right
do
130 if (x
-State
.left
) < 0.8 * (State
.right
-State
.left
) then
131 -- long word; chop it at some letter
132 -- We're not going to reimplement TeX here.
133 local bpos
= Text
.nearest_pos_less_than(frag
, State
.right
- x
)
134 if bpos
== 0 then break end -- avoid infinite loop when window is too narrow
135 local boffset
= Text
.offset(frag
, bpos
+1) -- byte _after_ bpos
136 local frag1
= string.sub(frag
, 1, boffset
-1)
137 local frag1_width
= App
.width(frag1
)
138 assert(x
+ frag1_width
<= State
.right
)
139 table.insert(line_cache
.fragments
, frag1
)
140 frag
= string.sub(frag
, boffset
)
141 frag_width
= App
.width(frag
)
143 x
= State
.left
-- new line
146 table.insert(line_cache
.fragments
, frag
)
152 function Text
.text_input(State
, t
)
153 if App
.mouse_down(1) then return end
154 if App
.ctrl_down() or App
.alt_down() or App
.cmd_down() then return end
155 local before
= snapshot(State
, State
.cursor1
.line
)
156 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
157 Text
.insert_at_cursor(State
, t
)
158 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
159 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
160 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
162 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
165 function Text
.insert_at_cursor(State
, t
)
166 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
167 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
168 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
)
169 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
170 State
.cursor1
.pos
= State
.cursor1
.pos
+1
173 -- Don't handle any keys here that would trigger text_input above.
174 function Text
.keychord_press(State
, chord
)
175 --? print('chord', chord, State.selection1.line, State.selection1.pos)
176 --== shortcuts that mutate text
177 if chord
== 'return' then
178 local before_line
= State
.cursor1
.line
179 local before
= snapshot(State
, before_line
)
180 Text
.insert_return(State
)
181 State
.selection1
= {}
182 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
183 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
186 record_undo_event(State
, {before
=before
, after
=snapshot(State
, before_line
, State
.cursor1
.line
)})
187 elseif chord
== 'tab' then
188 local before
= snapshot(State
, State
.cursor1
.line
)
189 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
190 Text
.insert_at_cursor(State
, '\t')
191 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
192 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
193 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
194 --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
197 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
198 elseif chord
== 'backspace' then
199 if State
.selection1
.line
then
200 Text
.delete_selection(State
, State
.left
, State
.right
)
205 if State
.cursor1
.pos
> 1 then
206 before
= snapshot(State
, State
.cursor1
.line
)
207 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1)
208 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
211 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
)
213 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
215 State
.cursor1
.pos
= State
.cursor1
.pos
-1
217 elseif State
.cursor1
.line
> 1 then
218 before
= snapshot(State
, State
.cursor1
.line
-1, State
.cursor1
.line
)
219 if State
.lines
[State
.cursor1
.line
-1].mode
== 'drawing' then
220 table.remove(State
.lines
, State
.cursor1
.line
-1)
221 table.remove(State
.line_cache
, State
.cursor1
.line
-1)
224 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
-1].data
)+1
225 State
.lines
[State
.cursor1
.line
-1].data
= State
.lines
[State
.cursor1
.line
-1].data
..State
.lines
[State
.cursor1
.line
].data
226 table.remove(State
.lines
, State
.cursor1
.line
)
227 table.remove(State
.line_cache
, State
.cursor1
.line
)
229 State
.cursor1
.line
= State
.cursor1
.line
-1
231 if State
.screen_top1
.line
> #State
.lines
then
232 Text
.populate_screen_line_starting_pos(State
, #State
.lines
)
233 local line_cache
= State
.line_cache
[#State
.line_cache
]
234 State
.screen_top1
= {line
=#State
.lines
, pos
=line_cache
.screen_line_starting_pos
[#line_cache
.screen_line_starting_pos
]}
235 elseif Text
.lt1(State
.cursor1
, State
.screen_top1
) then
236 State
.screen_top1
= {
237 line
=State
.cursor1
.line
,
238 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
240 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
242 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
243 assert(Text
.le1(State
.screen_top1
, State
.cursor1
))
245 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
246 elseif chord
== 'delete' then
247 if State
.selection1
.line
then
248 Text
.delete_selection(State
, State
.left
, State
.right
)
253 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
254 before
= snapshot(State
, State
.cursor1
.line
)
256 before
= snapshot(State
, State
.cursor1
.line
, State
.cursor1
.line
+1)
258 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
259 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
260 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
+1)
263 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
)
265 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
267 -- no change to State.cursor1.pos
269 elseif State
.cursor1
.line
< #State
.lines
then
270 if State
.lines
[State
.cursor1
.line
+1].mode
== 'text' then
272 State
.lines
[State
.cursor1
.line
].data
= State
.lines
[State
.cursor1
.line
].data
..State
.lines
[State
.cursor1
.line
+1].data
274 table.remove(State
.lines
, State
.cursor1
.line
+1)
275 table.remove(State
.line_cache
, State
.cursor1
.line
+1)
277 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
279 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
280 --== shortcuts that move the cursor
281 elseif chord
== 'left' then
283 State
.selection1
= {}
284 elseif chord
== 'right' then
286 State
.selection1
= {}
287 elseif chord
== 'S-left' then
288 if State
.selection1
.line
== nil then
289 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
292 elseif chord
== 'S-right' then
293 if State
.selection1
.line
== nil then
294 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
297 -- C- hotkeys reserved for drawings, so we'll use M-
298 elseif chord
== 'M-left' then
299 Text
.word_left(State
)
300 State
.selection1
= {}
301 elseif chord
== 'M-right' then
302 Text
.word_right(State
)
303 State
.selection1
= {}
304 elseif chord
== 'M-S-left' then
305 if State
.selection1
.line
== nil then
306 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
308 Text
.word_left(State
)
309 elseif chord
== 'M-S-right' then
310 if State
.selection1
.line
== nil then
311 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
313 Text
.word_right(State
)
314 elseif chord
== 'home' then
315 Text
.start_of_line(State
)
316 State
.selection1
= {}
317 elseif chord
== 'end' then
318 Text
.end_of_line(State
)
319 State
.selection1
= {}
320 elseif chord
== 'S-home' then
321 if State
.selection1
.line
== nil then
322 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
324 Text
.start_of_line(State
)
325 elseif chord
== 'S-end' then
326 if State
.selection1
.line
== nil then
327 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
329 Text
.end_of_line(State
)
330 elseif chord
== 'up' then
332 State
.selection1
= {}
333 elseif chord
== 'down' then
335 State
.selection1
= {}
336 elseif chord
== 'S-up' then
337 if State
.selection1
.line
== nil then
338 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
341 elseif chord
== 'S-down' then
342 if State
.selection1
.line
== nil then
343 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
346 elseif chord
== 'pageup' then
348 State
.selection1
= {}
349 elseif chord
== 'pagedown' then
351 State
.selection1
= {}
352 elseif chord
== 'S-pageup' then
353 if State
.selection1
.line
== nil then
354 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
357 elseif chord
== 'S-pagedown' then
358 if State
.selection1
.line
== nil then
359 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
365 function Text
.insert_return(State
)
366 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
367 table.insert(State
.lines
, State
.cursor1
.line
+1, {mode
='text', data
=string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_offset
)})
368 table.insert(State
.line_cache
, State
.cursor1
.line
+1, {})
369 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_offset
-1)
370 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
371 State
.cursor1
= {line
=State
.cursor1
.line
+1, pos
=1}
374 function Text
.pageup(State
)
376 -- duplicate some logic from love.draw
377 local top2
= Text
.to2(State
, State
.screen_top1
)
378 --? print(App.screen.height)
379 local y
= App
.screen
.height
- State
.line_height
380 while y
>= State
.top
do
381 --? print(y, top2.line, top2.screen_line, top2.screen_pos)
382 if State
.screen_top1
.line
== 1 and State
.screen_top1
.pos
== 1 then break end
383 if State
.lines
[State
.screen_top1
.line
].mode
== 'text' then
384 y
= y
- State
.line_height
385 elseif State
.lines
[State
.screen_top1
.line
].mode
== 'drawing' then
386 y
= y
- Drawing_padding_height
- Drawing
.pixels(State
.lines
[State
.screen_top1
.line
].h
, State
.width
)
388 top2
= Text
.previous_screen_line(State
, top2
)
390 State
.screen_top1
= Text
.to1(State
, top2
)
391 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
392 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
393 --? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
394 --? print('pageup end')
397 function Text
.pagedown(State
)
398 --? print('pagedown')
399 -- If a line/paragraph gets to a page boundary, I often want to scroll
400 -- before I get to the bottom.
401 -- However, only do this if it makes forward progress.
402 local bot2
= Text
.to2(State
, State
.screen_bottom1
)
403 if bot2
.screen_line
> 1 then
404 bot2
.screen_line
= math
.max(bot2
.screen_line
-10, 1)
406 local new_top1
= Text
.to1(State
, bot2
)
407 if Text
.lt1(State
.screen_top1
, new_top1
) then
408 State
.screen_top1
= new_top1
410 State
.screen_top1
= {line
=State
.screen_bottom1
.line
, pos
=State
.screen_bottom1
.pos
}
412 --? print('setting top to', State.screen_top1.line, State.screen_top1.pos)
413 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
414 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
415 --? print('top now', State.screen_top1.line)
416 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
417 --? print('pagedown end')
420 function Text
.up(State
)
421 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
422 --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
423 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
424 if screen_line_starting_pos
== 1 then
425 --? print('cursor is at first screen line of its line')
426 -- line is done; skip to previous text line
427 local new_cursor_line
= State
.cursor1
.line
428 while new_cursor_line
> 1 do
429 new_cursor_line
= new_cursor_line
-1
430 if State
.lines
[new_cursor_line
].mode
== 'text' then
431 --? print('found previous text line')
432 State
.cursor1
= {line
=new_cursor_line
, pos
=nil}
433 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
434 -- previous text line found, pick its final screen line
435 --? print('has multiple screen lines')
436 local screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
437 --? print(#screen_line_starting_pos)
438 screen_line_starting_pos
= screen_line_starting_pos
[#screen_line_starting_pos
]
439 local screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_pos
)
440 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_byte_offset
)
441 State
.cursor1
.pos
= screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
446 -- move up one screen line in current line
447 assert(screen_line_index
> 1)
448 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
-1]
449 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
450 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
451 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
452 --? print('cursor pos is now '..tostring(State.cursor1.pos))
454 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
455 State
.screen_top1
= {
456 line
=State
.cursor1
.line
,
457 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
459 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
463 function Text
.down(State
)
464 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
465 --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
466 if Text
.cursor_at_final_screen_line(State
) then
467 -- line is done, skip to next text line
468 --? print('cursor at final screen line of its line')
469 local new_cursor_line
= State
.cursor1
.line
470 while new_cursor_line
< #State
.lines
do
471 new_cursor_line
= new_cursor_line
+1
472 if State
.lines
[new_cursor_line
].mode
== 'text' then
474 line
= new_cursor_line
,
475 pos
= Text
.nearest_cursor_pos(State
.lines
[new_cursor_line
].data
, State
.cursor_x
, State
.left
),
477 --? print(State.cursor1.pos)
481 if State
.cursor1
.line
> State
.screen_bottom1
.line
then
482 --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
483 --? print('scroll up preserving cursor')
484 Text
.snap_cursor_to_bottom_of_screen(State
)
485 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
488 -- move down one screen line in current line
489 local scroll_down
= Text
.le1(State
.screen_bottom1
, State
.cursor1
)
490 --? print('cursor is NOT at final screen line of its line')
491 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
492 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
493 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
+1]
494 --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
495 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
496 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
497 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
498 --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
500 --? print('scroll up preserving cursor')
501 Text
.snap_cursor_to_bottom_of_screen(State
)
502 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
505 --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
508 function Text
.start_of_line(State
)
509 State
.cursor1
.pos
= 1
510 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
511 State
.screen_top1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
} -- copy
515 function Text
.end_of_line(State
)
516 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) + 1
517 if Text
.cursor_out_of_screen(State
) then
518 Text
.snap_cursor_to_bottom_of_screen(State
)
522 function Text
.word_left(State
)
523 -- skip some whitespace
525 if State
.cursor1
.pos
== 1 then
528 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%S') then
533 -- skip some non-whitespace
536 if State
.cursor1
.pos
== 1 then
539 assert(State
.cursor1
.pos
> 1)
540 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%s') then
546 function Text
.word_right(State
)
547 -- skip some whitespace
549 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
552 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%S') then
555 Text
.right_without_scroll(State
)
558 Text
.right_without_scroll(State
)
559 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
562 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%s') then
566 if Text
.cursor_out_of_screen(State
) then
567 Text
.snap_cursor_to_bottom_of_screen(State
)
571 function Text
.match(s
, pos
, pat
)
572 local start_offset
= Text
.offset(s
, pos
)
574 local end_offset
= Text
.offset(s
, pos
+1)
575 assert(end_offset
> start_offset
)
576 local curr
= s
:sub(start_offset
, end_offset
-1)
577 return curr
:match(pat
)
580 function Text
.left(State
)
581 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
582 if State
.cursor1
.pos
> 1 then
583 State
.cursor1
.pos
= State
.cursor1
.pos
-1
585 local new_cursor_line
= State
.cursor1
.line
586 while new_cursor_line
> 1 do
587 new_cursor_line
= new_cursor_line
-1
588 if State
.lines
[new_cursor_line
].mode
== 'text' then
590 line
= new_cursor_line
,
591 pos
= utf8
.len(State
.lines
[new_cursor_line
].data
) + 1,
597 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
598 State
.screen_top1
= {
599 line
=State
.cursor1
.line
,
600 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
602 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
606 function Text
.right(State
)
607 Text
.right_without_scroll(State
)
608 if Text
.cursor_out_of_screen(State
) then
609 Text
.snap_cursor_to_bottom_of_screen(State
)
613 function Text
.right_without_scroll(State
)
614 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
615 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
616 State
.cursor1
.pos
= State
.cursor1
.pos
+1
618 local new_cursor_line
= State
.cursor1
.line
619 while new_cursor_line
<= #State
.lines
-1 do
620 new_cursor_line
= new_cursor_line
+1
621 if State
.lines
[new_cursor_line
].mode
== 'text' then
622 State
.cursor1
= {line
=new_cursor_line
, pos
=1}
629 function Text
.pos_at_start_of_screen_line(State
, loc1
)
630 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
631 local line_cache
= State
.line_cache
[loc1
.line
]
632 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
633 local spos
= line_cache
.screen_line_starting_pos
[i
]
634 if spos
<= loc1
.pos
then
641 function Text
.cursor_at_final_screen_line(State
)
642 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
643 local screen_lines
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
644 --? print(screen_lines[#screen_lines], State.cursor1.pos)
645 return screen_lines
[#screen_lines
] <= State
.cursor1
.pos
648 function Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
650 while State
.cursor1
.line
<= #State
.lines
do
651 if State
.lines
[State
.cursor1
.line
].mode
== 'text' then
654 --? print('cursor skips', State.cursor1.line)
655 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[State
.cursor1
.line
].h
, State
.width
)
656 State
.cursor1
.line
= State
.cursor1
.line
+ 1
658 -- hack: insert a text line at bottom of file if necessary
659 if State
.cursor1
.line
> #State
.lines
then
660 assert(State
.cursor1
.line
== #State
.lines
+1)
661 table.insert(State
.lines
, {mode
='text', data
=''})
662 table.insert(State
.line_cache
, {})
664 --? print(y, App.screen.height, App.screen.height-State.line_height)
665 if y
> App
.screen
.height
- State
.line_height
then
666 --? print('scroll up')
667 Text
.snap_cursor_to_bottom_of_screen(State
)
671 -- should never modify State.cursor1
672 function Text
.snap_cursor_to_bottom_of_screen(State
)
673 --? print('to2:', State.cursor1.line, State.cursor1.pos)
674 local top2
= Text
.to2(State
, State
.cursor1
)
675 --? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
676 -- slide to start of screen line
677 top2
.screen_pos
= 1 -- start of screen line
678 --? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
679 --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
680 local y
= App
.screen
.height
- State
.line_height
681 -- duplicate some logic from love.draw
683 --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
684 if top2
.line
== 1 and top2
.screen_line
== 1 then break end
685 if top2
.screen_line
> 1 or State
.lines
[top2
.line
-1].mode
== 'text' then
686 local h
= State
.line_height
687 if y
- h
< State
.top
then
692 assert(top2
.line
> 1)
693 assert(State
.lines
[top2
.line
-1].mode
== 'drawing')
694 -- We currently can't draw partial drawings, so either skip it entirely
696 local h
= Drawing_padding_height
+ Drawing
.pixels(State
.lines
[top2
.line
-1].h
, State
.width
)
697 if y
- h
< State
.top
then
700 --? print('skipping drawing of height', h)
703 top2
= Text
.previous_screen_line(State
, top2
)
705 --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
706 State
.screen_top1
= Text
.to1(State
, top2
)
707 --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
708 --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
709 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
712 function Text
.in_line(State
, line_index
, x
,y
)
713 local line
= State
.lines
[line_index
]
714 local line_cache
= State
.line_cache
[line_index
]
715 if line_cache
.starty
== nil then return false end -- outside current page
716 if y
< line_cache
.starty
then return false end
717 Text
.populate_screen_line_starting_pos(State
, line_index
)
718 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)
721 -- convert mx,my in pixels to schema-1 coordinates
722 function Text
.to_pos_on_line(State
, line_index
, mx
, my
)
723 local line
= State
.lines
[line_index
]
724 local line_cache
= State
.line_cache
[line_index
]
725 assert(my
>= line_cache
.starty
)
726 -- duplicate some logic from Text.draw
727 local y
= line_cache
.starty
728 local start_screen_line_index
= Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
)
729 for screen_line_index
= start_screen_line_index
,#line_cache
.screen_line_starting_pos
do
730 local screen_line_starting_pos
= line_cache
.screen_line_starting_pos
[screen_line_index
]
731 local screen_line_starting_byte_offset
= Text
.offset(line
.data
, screen_line_starting_pos
)
732 --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
733 local nexty
= y
+ State
.line_height
735 -- On all wrapped screen lines but the final one, clicks past end of
736 -- line position cursor on final character of screen line.
737 -- (The final screen line positions past end of screen line as always.)
738 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
739 --? print('past end of non-final line; return')
740 return line_cache
.screen_line_starting_pos
[screen_line_index
+1]-1
742 local s
= string.sub(line
.data
, screen_line_starting_byte_offset
)
743 --? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
744 return screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, mx
, State
.left
) - 1
751 function Text
.screen_line_width(State
, line_index
, i
)
752 local line
= State
.lines
[line_index
]
753 local line_cache
= State
.line_cache
[line_index
]
754 local start_pos
= line_cache
.screen_line_starting_pos
[i
]
755 local start_offset
= Text
.offset(line
.data
, start_pos
)
757 if i
< #line_cache
.screen_line_starting_pos
then
758 local past_end_pos
= line_cache
.screen_line_starting_pos
[i
+1]
759 local past_end_offset
= Text
.offset(line
.data
, past_end_pos
)
760 screen_line
= string.sub(line
.data
, start_offset
, past_end_offset
-1)
762 screen_line
= string.sub(line
.data
, start_pos
)
764 return App
.width(screen_line
)
767 function Text
.screen_line_index(screen_line_starting_pos
, pos
)
768 for i
= #screen_line_starting_pos
,1,-1 do
769 if screen_line_starting_pos
[i
] <= pos
then
775 -- convert x pixel coordinate to pos
776 -- oblivious to wrapping
777 -- result: 1 to len+1
778 function Text
.nearest_cursor_pos(line
, x
, left
)
782 local len
= utf8
.len(line
)
783 local max_x
= left
+Text
.x(line
, len
+1)
787 local leftpos
, rightpos
= 1, len
+1
788 --? print('-- nearest', x)
790 --? print('nearest', x, '^'..line..'$', leftpos, rightpos)
791 if leftpos
== rightpos
then
794 local curr
= math
.floor((leftpos
+rightpos
)/2)
795 local currxmin
= left
+Text
.x(line
, curr
)
796 local currxmax
= left
+Text
.x(line
, curr
+1)
797 --? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
798 if currxmin
<= x
and x
< currxmax
then
799 if x
-currxmin
< currxmax
-x
then
805 if leftpos
>= rightpos
-1 then
817 -- return the nearest index of line (in utf8 code points) which lies entirely
818 -- within x pixels of the left margin
819 -- result: 0 to len+1
820 function Text
.nearest_pos_less_than(line
, x
)
821 --? print('', '-- nearest_pos_less_than', line, x)
822 local len
= utf8
.len(line
)
823 local max_x
= Text
.x_after(line
, len
)
827 local left
, right
= 0, len
+1
829 local curr
= math
.floor((left
+right
)/2)
830 local currxmin
= Text
.x_after(line
, curr
+1)
831 local currxmax
= Text
.x_after(line
, curr
+2)
832 --? print('', x, left, right, curr, currxmin, currxmax)
833 if currxmin
<= x
and x
< currxmax
then
836 if left
>= right
-1 then
848 function Text
.x_after(s
, pos
)
849 local offset
= Text
.offset(s
, math
.min(pos
+1, #s
+1))
850 local s_before
= s
:sub(1, offset
-1)
851 --? print('^'..s_before..'$')
852 return App
.width(s_before
)
855 function Text
.x(s
, pos
)
856 local offset
= Text
.offset(s
, pos
)
857 local s_before
= s
:sub(1, offset
-1)
858 return App
.width(s_before
)
861 function Text
.to2(State
, loc1
)
862 if State
.lines
[loc1
.line
].mode
== 'drawing' then
863 return {line
=loc1
.line
, screen_line
=1, screen_pos
=1}
865 local result
= {line
=loc1
.line
}
866 local line_cache
= State
.line_cache
[loc1
.line
]
867 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
868 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
869 local spos
= line_cache
.screen_line_starting_pos
[i
]
870 if spos
<= loc1
.pos
then
871 result
.screen_line
= i
872 result
.screen_pos
= loc1
.pos
- spos
+ 1
876 assert(result
.screen_pos
)
880 function Text
.to1(State
, loc2
)
881 local result
= {line
=loc2
.line
, pos
=loc2
.screen_pos
}
882 if loc2
.screen_line
> 1 then
883 result
.pos
= State
.line_cache
[loc2
.line
].screen_line_starting_pos
[loc2
.screen_line
] + loc2
.screen_pos
- 1
888 function Text
.eq1(a
, b
)
889 return a
.line
== b
.line
and a
.pos
== b
.pos
892 function Text
.lt1(a
, b
)
893 if a
.line
< b
.line
then
896 if a
.line
> b
.line
then
902 function Text
.le1(a
, b
)
903 if a
.line
< b
.line
then
906 if a
.line
> b
.line
then
909 return a
.pos
<= b
.pos
912 function Text
.offset(s
, pos1
)
913 if pos1
== 1 then return 1 end
914 local result
= utf8
.offset(s
, pos1
)
915 if result
== nil then
922 function Text
.previous_screen_line(State
, loc2
)
923 if loc2
.screen_line
> 1 then
924 return {line
=loc2
.line
, screen_line
=loc2
.screen_line
-1, screen_pos
=1}
925 elseif loc2
.line
== 1 then
927 elseif State
.lines
[loc2
.line
-1].mode
== 'drawing' then
928 return {line
=loc2
.line
-1, screen_line
=1, screen_pos
=1}
930 local l
= State
.lines
[loc2
.line
-1]
931 Text
.populate_screen_line_starting_pos(State
, loc2
.line
-1)
932 return {line
=loc2
.line
-1, screen_line
=#State
.line_cache
[loc2
.line
-1].screen_line_starting_pos
, screen_pos
=1}
937 function Text
.tweak_screen_top_and_cursor(State
)
938 if State
.screen_top1
.pos
== 1 then return end
939 Text
.populate_screen_line_starting_pos(State
, State
.screen_top1
.line
)
940 local line
= State
.lines
[State
.screen_top1
.line
]
941 local line_cache
= State
.line_cache
[State
.screen_top1
.line
]
942 for i
=2,#line_cache
.screen_line_starting_pos
do
943 local pos
= line_cache
.screen_line_starting_pos
[i
]
944 if pos
== State
.screen_top1
.pos
then
947 if pos
> State
.screen_top1
.pos
then
948 -- make sure screen top is at start of a screen line
949 local prev
= line_cache
.screen_line_starting_pos
[i
-1]
950 if State
.screen_top1
.pos
- prev
< pos
- State
.screen_top1
.pos
then
951 State
.screen_top1
.pos
= prev
953 State
.screen_top1
.pos
= pos
958 -- make sure cursor is on screen
959 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
960 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
961 elseif State
.cursor1
.line
>= State
.screen_bottom1
.line
then
963 if Text
.cursor_out_of_screen(State
) then
966 line
=State
.screen_bottom1
.line
,
967 pos
=Text
.to_pos_on_line(State
, State
.screen_bottom1
.line
, State
.right
-5, App
.screen
.height
-5),
973 -- slightly expensive since it redraws the screen
974 function Text
.cursor_out_of_screen(State
)
976 return State
.cursor_y
== nil
977 -- this approach is cheaper and almost works, except on the final screen
978 -- where file ends above bottom of screen
979 --? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)
980 --? local botline1 = {line=State.cursor1.line, pos=botpos}
981 --? return Text.lt1(State.screen_bottom1, botline1)
984 function source
.link_exists(State
, filename
)
985 if State
.link_cache
== nil then
986 State
.link_cache
= {}
988 if State
.link_cache
[filename
] == nil then
989 State
.link_cache
[filename
] = file_exists(filename
)
991 return State
.link_cache
[filename
]
994 function Text
.redraw_all(State
)
995 --? print('clearing fragments')
996 State
.line_cache
= {}
997 for i
=1,#State
.lines
do
998 State
.line_cache
[i
] = {}
1000 State
.link_cache
= {}
1003 function Text
.clear_screen_line_cache(State
, line_index
)
1004 State
.line_cache
[line_index
].fragments
= nil
1005 State
.line_cache
[line_index
].screen_line_starting_pos
= nil
1009 return s
:gsub('^%s+', ''):gsub('%s+$', '')
1013 return s
:gsub('^%s+', '')
1017 return s
:gsub('%s+$', '')
1020 function starts_with(s
, prefix
)
1021 if #s
< #prefix
then
1025 if s
:sub(i
,i
) ~= prefix
:sub(i
,i
) then
1032 function ends_with(s
, suffix
)
1033 if #s
< #suffix
then
1036 for i
=0,#suffix
-1 do
1037 if s
:sub(#s
-i
,#s
-i
) ~= suffix
:sub(#suffix
-i
,#suffix
-i
) then