1 -- text editor, particularly text drawing, horizontal wrap, vertical scrolling
3 AB_padding
= 20 -- space in pixels between A side and B side
5 -- draw a line starting from startpos to screen at y between State.left and State.right
6 -- return the final y, and pos,posB of start of final screen line drawn
7 function Text
.draw(State
, line_index
, y
, startpos
, startposB
, hide_cursor
)
8 local line
= State
.lines
[line_index
]
9 local line_cache
= State
.line_cache
[line_index
]
11 line_cache
.startpos
= startpos
12 line_cache
.startposB
= startposB
14 local overflows_screen
, x
, pos
, screen_line_starting_pos
16 overflows_screen
, x
, y
, pos
, screen_line_starting_pos
= Text
.draw_wrapping_line(State
, line_index
, State
.left
, y
, startpos
)
17 if overflows_screen
then
18 return y
, screen_line_starting_pos
20 if Focus
== 'edit' and State
.cursor1
.pos
then
21 if not hide_cursor
and not State
.search_term
then
22 if line_index
== State
.cursor1
.line
and State
.cursor1
.pos
== pos
then
23 Text
.draw_cursor(State
, x
, y
)
31 --? if line_index == 8 then print('checking for B side') end
32 if line
.dataB
== nil then
34 assert(screen_line_starting_pos
)
35 --? if line_index == 8 then print('return 1') end
36 return y
, screen_line_starting_pos
38 if not State
.expanded
and not line
.expanded
then
40 assert(screen_line_starting_pos
)
41 --? if line_index == 8 then print('return 2') end
42 button(State
, 'expand', {x
=x
+AB_padding
, y
=y
+2, w
=App
.width(State
.em
), h
=State
.line_height
-4, color
={1,1,1},
43 icon
= function(button_params
)
44 App
.color(Fold_background_color
)
45 love
.graphics
.rectangle('fill', button_params
.x
, button_params
.y
, App
.width(State
.em
), State
.line_height
-4, 2,2)
51 return y
, screen_line_starting_pos
54 --? if line_index == 8 then print('drawing B side') end
57 overflows_screen
, x
, y
, pos
, screen_line_starting_pos
= Text
.draw_wrapping_lineB(State
, line_index
, x
,y
, startposB
)
59 overflows_screen
, x
, y
, pos
, screen_line_starting_pos
= Text
.draw_wrapping_lineB(State
, line_index
, x
+AB_padding
,y
, 1)
61 if overflows_screen
then
62 return y
, nil, screen_line_starting_pos
64 --? if line_index == 8 then print('a') end
65 if Focus
== 'edit' and State
.cursor1
.posB
then
66 --? if line_index == 8 then print('b') end
67 if not hide_cursor
and not State
.search_term
then
68 --? if line_index == 8 then print('c', State.cursor1.line, State.cursor1.posB, line_index, pos) end
69 if line_index
== State
.cursor1
.line
and State
.cursor1
.posB
== pos
then
70 Text
.draw_cursor(State
, x
, y
)
74 return y
, nil, screen_line_starting_pos
77 -- Given an array of fragments, draw the subset starting from pos to screen
78 -- starting from (x,y).
80 -- - whether we got to bottom of screen before end of line
83 -- - starting pos of the final screen line drawn
84 function Text
.draw_wrapping_line(State
, line_index
, x
,y
, startpos
)
85 local line
= State
.lines
[line_index
]
86 local line_cache
= State
.line_cache
[line_index
]
87 --? print('== line', line_index, '^'..line.data..'$')
88 local screen_line_starting_pos
= startpos
89 Text
.compute_fragments(State
, line_index
)
92 for _
, f
in ipairs(line_cache
.fragments
) do
94 local frag
, frag_text
= f
.data
, f
.text
96 local frag_len
= utf8
.len(frag
)
97 --? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)
98 if pos
< startpos
then
100 --? print('skipping', frag)
103 local frag_width
= App
.width(frag_text
)
104 if x
+ frag_width
> State
.right
then
105 assert(x
> State
.left
) -- no overfull lines
106 y
= y
+ State
.line_height
107 if y
+ State
.line_height
> App
.screen
.height
then
108 return --[[screen filled]] true, x
,y
, pos
, screen_line_starting_pos
110 screen_line_starting_pos
= pos
113 if State
.selection1
.line
then
114 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
115 Text
.draw_highlight(State
, line
, x
,y
, pos
, lo
,hi
)
117 -- Make [[WikiWords]] (single word, all in one screen line) clickable.
118 local trimmed_word
= rtrim(frag
) -- compute_fragments puts whitespace at the end
119 if starts_with(trimmed_word
, '[[') and ends_with(trimmed_word
, ']]') then
120 local filename
= trimmed_word
:gsub('^..(.*)..$', '%1')
121 if source
.link_exists(State
, filename
) then
122 local filename_text
= App
.newText(love
.graphics
.getFont(), filename
)
123 button(State
, 'link', {x
=x
+App
.width(to_text('[[')), y
=y
, w
=App
.width(filename_text
), h
=State
.line_height
, color
={1,1,1},
124 icon
= icon
.hyperlink_decoration
,
125 onpress1
= function()
126 source
.switch_to_file(filename
)
131 App
.screen
.draw(frag_text
, x
,y
)
132 -- render cursor if necessary
133 if State
.cursor1
.pos
and line_index
== State
.cursor1
.line
then
134 if pos
<= State
.cursor1
.pos
and pos
+ frag_len
> State
.cursor1
.pos
then
135 if State
.search_term
then
136 if State
.lines
[State
.cursor1
.line
].data
:sub(State
.cursor1
.pos
, State
.cursor1
.pos
+utf8
.len(State
.search_term
)-1) == State
.search_term
then
137 local lo_px
= Text
.draw_highlight(State
, line
, x
,y
, pos
, State
.cursor1
.pos
, State
.cursor1
.pos
+utf8
.len(State
.search_term
))
138 App
.color(Text_color
)
139 love
.graphics
.print(State
.search_term
, x
+lo_px
,y
)
141 elseif Focus
== 'edit' then
142 Text
.draw_cursor(State
, x
+Text
.x(frag
, State
.cursor1
.pos
-pos
+1), y
)
143 App
.color(Text_color
)
151 return false, x
,y
, pos
, screen_line_starting_pos
154 function Text
.draw_wrapping_lineB(State
, line_index
, x
,y
, startpos
)
155 local line
= State
.lines
[line_index
]
156 local line_cache
= State
.line_cache
[line_index
]
157 local screen_line_starting_pos
= startpos
158 Text
.compute_fragmentsB(State
, line_index
, x
)
160 for _
, f
in ipairs(line_cache
.fragmentsB
) do
161 local frag
, frag_text
= f
.data
, f
.text
162 local frag_len
= utf8
.len(frag
)
163 --? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)
164 if pos
< startpos
then
166 --? print('skipping', frag)
169 local frag_width
= App
.width(frag_text
)
170 if x
+ frag_width
> State
.right
then
171 assert(x
> State
.left
) -- no overfull lines
172 y
= y
+ State
.line_height
173 if y
+ State
.line_height
> App
.screen
.height
then
174 return --[[screen filled]] true, x
,y
, pos
, screen_line_starting_pos
176 screen_line_starting_pos
= pos
179 if State
.selection1
.line
then
180 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
181 Text
.draw_highlight(State
, line
, x
,y
, pos
, lo
,hi
)
183 App
.screen
.draw(frag_text
, x
,y
)
184 -- render cursor if necessary
185 if State
.cursor1
.posB
and line_index
== State
.cursor1
.line
then
186 if pos
<= State
.cursor1
.posB
and pos
+ frag_len
> State
.cursor1
.posB
then
187 if State
.search_term
then
188 if State
.lines
[State
.cursor1
.line
].dataB
:sub(State
.cursor1
.posB
, State
.cursor1
.posB
+utf8
.len(State
.search_term
)-1) == State
.search_term
then
189 local lo_px
= Text
.draw_highlight(State
, line
, x
,y
, pos
, State
.cursor1
.posB
, State
.cursor1
.posB
+utf8
.len(State
.search_term
))
190 App
.color(Fold_color
)
191 love
.graphics
.print(State
.search_term
, x
+lo_px
,y
)
193 elseif Focus
== 'edit' then
194 Text
.draw_cursor(State
, x
+Text
.x(frag
, State
.cursor1
.posB
-pos
+1), y
)
195 App
.color(Fold_color
)
203 return false, x
,y
, pos
, screen_line_starting_pos
206 function Text
.draw_cursor(State
, x
, y
)
208 if math
.floor(Cursor_time
*2)%2 == 0 then
209 App
.color(Cursor_color
)
210 love
.graphics
.rectangle('fill', x
,y
, 3,State
.line_height
)
213 State
.cursor_y
= y
+State
.line_height
216 function Text
.populate_screen_line_starting_pos(State
, line_index
)
217 local line
= State
.lines
[line_index
]
218 if line
.mode
~= 'text' then return end
219 local line_cache
= State
.line_cache
[line_index
]
220 if line_cache
.screen_line_starting_pos
then
223 -- duplicate some logic from Text.draw
224 Text
.compute_fragments(State
, line_index
)
225 line_cache
.screen_line_starting_pos
= {1}
228 for _
, f
in ipairs(line_cache
.fragments
) do
229 local frag
, frag_text
= f
.data
, f
.text
231 local frag_width
= App
.width(frag_text
)
232 if x
+ frag_width
> State
.right
then
234 table.insert(line_cache
.screen_line_starting_pos
, pos
)
237 local frag_len
= utf8
.len(frag
)
242 function Text
.compute_fragments(State
, line_index
)
243 --? print('compute_fragments', line_index, 'between', State.left, State.right)
244 local line
= State
.lines
[line_index
]
245 if line
.mode
~= 'text' then return end
246 local line_cache
= State
.line_cache
[line_index
]
247 if line_cache
.fragments
then
250 line_cache
.fragments
= {}
252 -- try to wrap at word boundaries
253 for frag
in line
.data
:gmatch('%S*%s*') do
254 local frag_text
= App
.newText(love
.graphics
.getFont(), frag
)
255 local frag_width
= App
.width(frag_text
)
256 --? print('x: '..tostring(x)..'; frag_width: '..tostring(frag_width)..'; '..tostring(State.right-x)..'px to go')
257 while x
+ frag_width
> State
.right
do
258 --? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x))
259 if (x
-State
.left
) < 0.8 * (State
.right
-State
.left
) then
260 --? print('splitting')
261 -- long word; chop it at some letter
262 -- We're not going to reimplement TeX here.
263 local bpos
= Text
.nearest_pos_less_than(frag
, State
.right
- x
)
264 --? print('bpos', bpos)
265 if bpos
== 0 then break end -- avoid infinite loop when window is too narrow
266 local boffset
= Text
.offset(frag
, bpos
+1) -- byte _after_ bpos
267 --? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')
268 local frag1
= string.sub(frag
, 1, boffset
-1)
269 local frag1_text
= App
.newText(love
.graphics
.getFont(), frag1
)
270 local frag1_width
= App
.width(frag1_text
)
271 --? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')
272 assert(x
+ frag1_width
<= State
.right
)
273 table.insert(line_cache
.fragments
, {data
=frag1
, text
=frag1_text
})
274 frag
= string.sub(frag
, boffset
)
275 frag_text
= App
.newText(love
.graphics
.getFont(), frag
)
276 frag_width
= App
.width(frag_text
)
278 x
= State
.left
-- new line
281 --? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')
282 table.insert(line_cache
.fragments
, {data
=frag
, text
=frag_text
})
288 function Text
.populate_screen_line_starting_posB(State
, line_index
, x
)
289 local line
= State
.lines
[line_index
]
290 local line_cache
= State
.line_cache
[line_index
]
291 if line_cache
.screen_line_starting_posB
then
294 -- duplicate some logic from Text.draw
295 Text
.compute_fragmentsB(State
, line_index
, x
)
296 line_cache
.screen_line_starting_posB
= {1}
298 for _
, f
in ipairs(line_cache
.fragmentsB
) do
299 local frag
, frag_text
= f
.data
, f
.text
301 local frag_width
= App
.width(frag_text
)
302 if x
+ frag_width
> State
.right
then
304 table.insert(line_cache
.screen_line_starting_posB
, pos
)
307 local frag_len
= utf8
.len(frag
)
312 function Text
.compute_fragmentsB(State
, line_index
, x
)
313 --? print('compute_fragmentsB', line_index, 'between', x, State.right)
314 local line
= State
.lines
[line_index
]
315 local line_cache
= State
.line_cache
[line_index
]
316 if line_cache
.fragmentsB
then
319 line_cache
.fragmentsB
= {}
320 -- try to wrap at word boundaries
321 for frag
in line
.dataB
:gmatch('%S*%s*') do
322 local frag_text
= App
.newText(love
.graphics
.getFont(), frag
)
323 local frag_width
= App
.width(frag_text
)
324 --? print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go')
325 while x
+ frag_width
> State
.right
do
326 --? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x))
327 if (x
-State
.left
) < 0.8 * (State
.right
-State
.left
) then
328 --? print('splitting')
329 -- long word; chop it at some letter
330 -- We're not going to reimplement TeX here.
331 local bpos
= Text
.nearest_pos_less_than(frag
, State
.right
- x
)
332 --? print('bpos', bpos)
333 if bpos
== 0 then break end -- avoid infinite loop when window is too narrow
334 local boffset
= Text
.offset(frag
, bpos
+1) -- byte _after_ bpos
335 --? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')
336 local frag1
= string.sub(frag
, 1, boffset
-1)
337 local frag1_text
= App
.newText(love
.graphics
.getFont(), frag1
)
338 local frag1_width
= App
.width(frag1_text
)
339 --? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')
340 assert(x
+ frag1_width
<= State
.right
)
341 table.insert(line_cache
.fragmentsB
, {data
=frag1
, text
=frag1_text
})
342 frag
= string.sub(frag
, boffset
)
343 frag_text
= App
.newText(love
.graphics
.getFont(), frag
)
344 frag_width
= App
.width(frag_text
)
346 x
= State
.left
-- new line
349 --? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')
350 table.insert(line_cache
.fragmentsB
, {data
=frag
, text
=frag_text
})
356 function Text
.textinput(State
, t
)
357 if App
.mouse_down(1) then return end
358 if App
.ctrl_down() or App
.alt_down() or App
.cmd_down() then return end
359 local before
= snapshot(State
, State
.cursor1
.line
)
360 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
361 Text
.insert_at_cursor(State
, t
)
362 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
363 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
364 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
366 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
369 function Text
.insert_at_cursor(State
, t
)
370 if State
.cursor1
.pos
then
371 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
372 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
)
373 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
374 State
.cursor1
.pos
= State
.cursor1
.pos
+1
376 assert(State
.cursor1
.posB
)
377 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].dataB
, State
.cursor1
.posB
)
378 State
.lines
[State
.cursor1
.line
].dataB
= string.sub(State
.lines
[State
.cursor1
.line
].dataB
, 1, byte_offset
-1)..t
..string.sub(State
.lines
[State
.cursor1
.line
].dataB
, byte_offset
)
379 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
380 State
.cursor1
.posB
= State
.cursor1
.posB
+1
384 -- Don't handle any keys here that would trigger love.textinput above.
385 function Text
.keychord_pressed(State
, chord
)
386 --? print('chord', chord, State.selection1.line, State.selection1.pos)
387 --== shortcuts that mutate text
388 if chord
== 'return' then
389 local before_line
= State
.cursor1
.line
390 local before
= snapshot(State
, before_line
)
391 Text
.insert_return(State
)
392 State
.selection1
= {}
393 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
394 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
397 record_undo_event(State
, {before
=before
, after
=snapshot(State
, before_line
, State
.cursor1
.line
)})
398 elseif chord
== 'tab' then
399 local before
= snapshot(State
, State
.cursor1
.line
)
400 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
401 Text
.insert_at_cursor(State
, '\t')
402 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
403 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
404 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
405 --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
408 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
409 elseif chord
== 'backspace' then
410 if State
.selection1
.line
then
411 Text
.delete_selection(State
, State
.left
, State
.right
)
416 if State
.cursor1
.pos
and State
.cursor1
.pos
> 1 then
417 before
= snapshot(State
, State
.cursor1
.line
)
418 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1)
419 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
422 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
)
424 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
426 State
.cursor1
.pos
= State
.cursor1
.pos
-1
428 elseif State
.cursor1
.posB
then
429 if State
.cursor1
.posB
> 1 then
430 before
= snapshot(State
, State
.cursor1
.line
)
431 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].dataB
, State
.cursor1
.posB
-1)
432 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].dataB
, State
.cursor1
.posB
)
435 State
.lines
[State
.cursor1
.line
].dataB
= string.sub(State
.lines
[State
.cursor1
.line
].dataB
, 1, byte_start
-1)..string.sub(State
.lines
[State
.cursor1
.line
].dataB
, byte_end
)
437 State
.lines
[State
.cursor1
.line
].dataB
= string.sub(State
.lines
[State
.cursor1
.line
].dataB
, 1, byte_start
-1)
439 State
.cursor1
.posB
= State
.cursor1
.posB
-1
442 -- refuse to delete past beginning of side B
444 elseif State
.cursor1
.line
> 1 then
445 before
= snapshot(State
, State
.cursor1
.line
-1, State
.cursor1
.line
)
446 if State
.lines
[State
.cursor1
.line
-1].mode
== 'drawing' then
447 table.remove(State
.lines
, State
.cursor1
.line
-1)
448 table.remove(State
.line_cache
, State
.cursor1
.line
-1)
451 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
-1].data
)+1
452 State
.lines
[State
.cursor1
.line
-1].data
= State
.lines
[State
.cursor1
.line
-1].data
..State
.lines
[State
.cursor1
.line
].data
453 table.remove(State
.lines
, State
.cursor1
.line
)
454 table.remove(State
.line_cache
, State
.cursor1
.line
)
456 State
.cursor1
.line
= State
.cursor1
.line
-1
458 if State
.screen_top1
.line
> #State
.lines
then
459 Text
.populate_screen_line_starting_pos(State
, #State
.lines
)
460 local line_cache
= State
.line_cache
[#State
.line_cache
]
461 State
.screen_top1
= {line
=#State
.lines
, pos
=line_cache
.screen_line_starting_pos
[#line_cache
.screen_line_starting_pos
]}
462 elseif Text
.lt1(State
.cursor1
, State
.screen_top1
) then
463 local top2
= Text
.to2(State
, State
.screen_top1
)
464 top2
= Text
.previous_screen_line(State
, top2
, State
.left
, State
.right
)
465 State
.screen_top1
= Text
.to1(State
, top2
)
466 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
468 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
469 assert(Text
.le1(State
.screen_top1
, State
.cursor1
))
471 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
472 elseif chord
== 'delete' then
473 if State
.selection1
.line
then
474 Text
.delete_selection(State
, State
.left
, State
.right
)
479 if State
.cursor1
.posB
or State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
480 before
= snapshot(State
, State
.cursor1
.line
)
482 before
= snapshot(State
, State
.cursor1
.line
, State
.cursor1
.line
+1)
484 if State
.cursor1
.pos
and State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
485 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
486 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
+1)
489 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
)
491 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
493 -- no change to State.cursor1.pos
495 elseif State
.cursor1
.posB
then
496 if State
.cursor1
.posB
<= utf8
.len(State
.lines
[State
.cursor1
.line
].dataB
) then
497 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].dataB
, State
.cursor1
.posB
)
498 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].dataB
, State
.cursor1
.posB
+1)
501 State
.lines
[State
.cursor1
.line
].dataB
= string.sub(State
.lines
[State
.cursor1
.line
].dataB
, 1, byte_start
-1)..string.sub(State
.lines
[State
.cursor1
.line
].dataB
, byte_end
)
503 State
.lines
[State
.cursor1
.line
].dataB
= string.sub(State
.lines
[State
.cursor1
.line
].dataB
, 1, byte_start
-1)
505 -- no change to State.cursor1.pos
508 -- refuse to delete past end of side B
510 elseif State
.cursor1
.line
< #State
.lines
then
511 if State
.lines
[State
.cursor1
.line
+1].mode
== 'text' then
513 State
.lines
[State
.cursor1
.line
].data
= State
.lines
[State
.cursor1
.line
].data
..State
.lines
[State
.cursor1
.line
+1].data
514 -- delete side B on first line
515 State
.lines
[State
.cursor1
.line
].dataB
= State
.lines
[State
.cursor1
.line
+1].dataB
517 table.remove(State
.lines
, State
.cursor1
.line
+1)
518 table.remove(State
.line_cache
, State
.cursor1
.line
+1)
520 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
522 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
523 --== shortcuts that move the cursor
524 elseif chord
== 'left' then
526 State
.selection1
= {}
527 elseif chord
== 'right' then
529 State
.selection1
= {}
530 elseif chord
== 'S-left' then
531 if State
.selection1
.line
== nil then
532 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
}
535 elseif chord
== 'S-right' then
536 if State
.selection1
.line
== nil then
537 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
}
540 -- C- hotkeys reserved for drawings, so we'll use M-
541 elseif chord
== 'M-left' then
542 Text
.word_left(State
)
543 State
.selection1
= {}
544 elseif chord
== 'M-right' then
545 Text
.word_right(State
)
546 State
.selection1
= {}
547 elseif chord
== 'M-S-left' then
548 if State
.selection1
.line
== nil then
549 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
}
551 Text
.word_left(State
)
552 elseif chord
== 'M-S-right' then
553 if State
.selection1
.line
== nil then
554 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
}
556 Text
.word_right(State
)
557 elseif chord
== 'home' then
558 Text
.start_of_line(State
)
559 State
.selection1
= {}
560 elseif chord
== 'end' then
561 Text
.end_of_line(State
)
562 State
.selection1
= {}
563 elseif chord
== 'S-home' then
564 if State
.selection1
.line
== nil then
565 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
}
567 Text
.start_of_line(State
)
568 elseif chord
== 'S-end' then
569 if State
.selection1
.line
== nil then
570 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
}
572 Text
.end_of_line(State
)
573 elseif chord
== 'up' then
575 State
.selection1
= {}
576 elseif chord
== 'down' then
578 State
.selection1
= {}
579 elseif chord
== 'S-up' then
580 if State
.selection1
.line
== nil then
581 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
}
584 elseif chord
== 'S-down' then
585 if State
.selection1
.line
== nil then
586 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
}
589 elseif chord
== 'pageup' then
591 State
.selection1
= {}
592 elseif chord
== 'pagedown' then
594 State
.selection1
= {}
595 elseif chord
== 'S-pageup' then
596 if State
.selection1
.line
== nil then
597 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
}
600 elseif chord
== 'S-pagedown' then
601 if State
.selection1
.line
== nil then
602 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
}
608 function Text
.insert_return(State
)
609 if State
.cursor1
.pos
then
610 -- when inserting a newline, move any B side to the new line
611 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
612 table.insert(State
.lines
, State
.cursor1
.line
+1, {mode
='text', data
=string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_offset
), dataB
=State
.lines
[State
.cursor1
.line
].dataB
})
613 table.insert(State
.line_cache
, State
.cursor1
.line
+1, {})
614 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_offset
-1)
615 State
.lines
[State
.cursor1
.line
].dataB
= nil
616 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
617 State
.cursor1
= {line
=State
.cursor1
.line
+1, pos
=1}
619 -- disable enter when cursor is on the B side
623 function Text
.pageup(State
)
625 -- duplicate some logic from love.draw
626 local top2
= Text
.to2(State
, State
.screen_top1
)
627 --? print(App.screen.height)
628 local y
= App
.screen
.height
- State
.line_height
629 while y
>= State
.top
do
630 --? print(y, top2.line, top2.screen_line, top2.screen_pos)
631 if State
.screen_top1
.line
== 1 and State
.screen_top1
.pos
and State
.screen_top1
.pos
== 1 then break end
632 if State
.lines
[State
.screen_top1
.line
].mode
== 'text' then
633 y
= y
- State
.line_height
634 elseif State
.lines
[State
.screen_top1
.line
].mode
== 'drawing' then
635 y
= y
- Drawing_padding_height
- Drawing
.pixels(State
.lines
[State
.screen_top1
.line
].h
, State
.width
)
637 top2
= Text
.previous_screen_line(State
, top2
)
639 State
.screen_top1
= Text
.to1(State
, top2
)
640 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
, posB
=State
.screen_top1
.posB
}
641 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
642 --? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
643 --? print('pageup end')
646 function Text
.pagedown(State
)
647 --? print('pagedown')
648 local bot2
= Text
.to2(State
, State
.screen_bottom1
)
649 local new_top1
= Text
.to1(State
, bot2
)
650 if Text
.lt1(State
.screen_top1
, new_top1
) then
651 State
.screen_top1
= new_top1
653 State
.screen_top1
= {line
=State
.screen_bottom1
.line
, pos
=State
.screen_bottom1
.pos
, posB
=State
.screen_bottom1
.posB
}
655 --? print('setting top to', State.screen_top1.line, State.screen_top1.pos)
656 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
, posB
=State
.screen_top1
.posB
}
657 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
658 --? print('top now', State.screen_top1.line)
659 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
660 --? print('pagedown end')
663 function Text
.up(State
)
664 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
665 if State
.cursor1
.pos
then
672 function Text
.upA(State
)
673 --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
674 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
675 if screen_line_starting_pos
== 1 then
676 --? print('cursor is at first screen line of its line')
677 -- line is done; skip to previous text line
678 local new_cursor_line
= State
.cursor1
.line
679 while new_cursor_line
> 1 do
680 new_cursor_line
= new_cursor_line
-1
681 if State
.lines
[new_cursor_line
].mode
== 'text' then
682 --? print('found previous text line')
683 State
.cursor1
= {line
=State
.cursor1
.line
-1, pos
=nil}
684 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
685 -- previous text line found, pick its final screen line
686 --? print('has multiple screen lines')
687 local screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
688 --? print(#screen_line_starting_pos)
689 screen_line_starting_pos
= screen_line_starting_pos
[#screen_line_starting_pos
]
690 local screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_pos
)
691 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_byte_offset
)
692 State
.cursor1
.pos
= screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
697 -- move up one screen line in current line
698 assert(screen_line_index
> 1)
699 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
-1]
700 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
701 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
702 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
703 --? print('cursor pos is now '..tostring(State.cursor1.pos))
705 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
706 local top2
= Text
.to2(State
, State
.screen_top1
)
707 top2
= Text
.previous_screen_line(State
, top2
)
708 State
.screen_top1
= Text
.to1(State
, top2
)
712 function Text
.upB(State
)
713 local line_cache
= State
.line_cache
[State
.cursor1
.line
]
714 local screen_line_starting_posB
, screen_line_indexB
= Text
.pos_at_start_of_screen_lineB(State
, State
.cursor1
)
715 assert(screen_line_indexB
>= 1)
716 if screen_line_indexB
== 1 then
717 -- move to A side of previous line
718 local new_cursor_line
= State
.cursor1
.line
719 while new_cursor_line
> 1 do
720 new_cursor_line
= new_cursor_line
-1
721 if State
.lines
[new_cursor_line
].mode
== 'text' then
722 State
.cursor1
= {line
=State
.cursor1
.line
-1, posB
=nil}
723 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
724 local prev_line_cache
= State
.line_cache
[State
.cursor1
.line
]
725 local prev_screen_line_starting_pos
= prev_line_cache
.screen_line_starting_pos
[#prev_line_cache
.screen_line_starting_pos
]
726 local prev_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, prev_screen_line_starting_pos
)
727 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, prev_screen_line_starting_byte_offset
)
728 State
.cursor1
.pos
= prev_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
732 elseif screen_line_indexB
== 2 then
733 -- all-B screen-line to potentially A+B screen-line
734 local xA
= Margin_left
+ Text
.screen_line_width(State
, State
.cursor1
.line
, #line_cache
.screen_line_starting_pos
) + AB_padding
735 if State
.cursor_x
< xA
then
736 State
.cursor1
.posB
= nil
737 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
738 local new_screen_line_starting_pos
= line_cache
.screen_line_starting_pos
[#line_cache
.screen_line_starting_pos
]
739 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
740 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
741 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
743 Text
.populate_screen_line_starting_posB(State
, State
.cursor1
.line
)
744 local new_screen_line_starting_posB
= line_cache
.screen_line_starting_posB
[screen_line_indexB
-1]
745 local new_screen_line_starting_byte_offsetB
= Text
.offset(State
.lines
[State
.cursor1
.line
].dataB
, new_screen_line_starting_posB
)
746 local s
= string.sub(State
.lines
[State
.cursor1
.line
].dataB
, new_screen_line_starting_byte_offsetB
)
747 State
.cursor1
.posB
= new_screen_line_starting_posB
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
-xA
, State
.left
) - 1
750 assert(screen_line_indexB
> 2)
751 -- all-B screen-line to all-B screen-line
752 Text
.populate_screen_line_starting_posB(State
, State
.cursor1
.line
)
753 local new_screen_line_starting_posB
= line_cache
.screen_line_starting_posB
[screen_line_indexB
-1]
754 local new_screen_line_starting_byte_offsetB
= Text
.offset(State
.lines
[State
.cursor1
.line
].dataB
, new_screen_line_starting_posB
)
755 local s
= string.sub(State
.lines
[State
.cursor1
.line
].dataB
, new_screen_line_starting_byte_offsetB
)
756 State
.cursor1
.posB
= new_screen_line_starting_posB
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
758 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
759 local top2
= Text
.to2(State
, State
.screen_top1
)
760 top2
= Text
.previous_screen_line(State
, top2
)
761 State
.screen_top1
= Text
.to1(State
, top2
)
765 -- cursor on final screen line (A or B side) => goes to next screen line on A side
766 -- cursor on A side => move down one screen line (A side) in current line
767 -- cursor on B side => move down one screen line (B side) in current line
768 function Text
.down(State
)
769 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
770 --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
771 if Text
.cursor_at_final_screen_line(State
) then
772 -- line is done, skip to next text line
773 --? print('cursor at final screen line of its line')
774 local new_cursor_line
= State
.cursor1
.line
775 while new_cursor_line
< #State
.lines
do
776 new_cursor_line
= new_cursor_line
+1
777 if State
.lines
[new_cursor_line
].mode
== 'text' then
779 line
= new_cursor_line
,
780 pos
= Text
.nearest_cursor_pos(State
.lines
[new_cursor_line
].data
, State
.cursor_x
, State
.left
),
782 --? print(State.cursor1.pos)
786 if State
.cursor1
.line
> State
.screen_bottom1
.line
then
787 --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
788 --? print('scroll up preserving cursor')
789 Text
.snap_cursor_to_bottom_of_screen(State
)
790 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
792 elseif State
.cursor1
.pos
then
793 -- move down one screen line (A side) in current line
794 local scroll_down
= Text
.le1(State
.screen_bottom1
, State
.cursor1
)
795 --? print('cursor is NOT at final screen line of its line')
796 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
797 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
798 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
+1]
799 --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
800 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
801 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
802 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
803 --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
805 --? print('scroll up preserving cursor')
806 Text
.snap_cursor_to_bottom_of_screen(State
)
807 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
810 -- move down one screen line (B side) in current line
811 local scroll_down
= false
812 if Text
.le1(State
.screen_bottom1
, State
.cursor1
) then
815 local cursor_line
= State
.lines
[State
.cursor1
.line
]
816 local cursor_line_cache
= State
.line_cache
[State
.cursor1
.line
]
817 local cursor2
= Text
.to2(State
, State
.cursor1
)
818 assert(cursor2
.screen_lineB
< #cursor_line_cache
.screen_line_starting_posB
)
819 local screen_line_starting_posB
, screen_line_indexB
= Text
.pos_at_start_of_screen_lineB(State
, State
.cursor1
)
820 Text
.populate_screen_line_starting_posB(State
, State
.cursor1
.line
)
821 local new_screen_line_starting_posB
= cursor_line_cache
.screen_line_starting_posB
[screen_line_indexB
+1]
822 local new_screen_line_starting_byte_offsetB
= Text
.offset(cursor_line
.dataB
, new_screen_line_starting_posB
)
823 local s
= string.sub(cursor_line
.dataB
, new_screen_line_starting_byte_offsetB
)
824 State
.cursor1
.posB
= new_screen_line_starting_posB
+ Text
.nearest_cursor_pos(s
, State
.cursor_x
, State
.left
) - 1
826 Text
.snap_cursor_to_bottom_of_screen(State
)
829 --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
832 function Text
.start_of_line(State
)
833 if State
.cursor1
.pos
then
834 State
.cursor1
.pos
= 1
836 State
.cursor1
.posB
= 1
838 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
839 State
.screen_top1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
, posB
=State
.cursor1
.posB
} -- copy
843 function Text
.end_of_line(State
)
844 if State
.cursor1
.pos
then
845 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) + 1
847 State
.cursor1
.posB
= utf8
.len(State
.lines
[State
.cursor1
.line
].dataB
) + 1
849 if Text
.cursor_out_of_screen(State
) then
850 Text
.snap_cursor_to_bottom_of_screen(State
)
854 function Text
.word_left(State
)
855 -- we can cross the fold, so check side A/B one level down
856 Text
.skip_whitespace_left(State
)
858 Text
.skip_non_whitespace_left(State
)
861 function Text
.word_right(State
)
862 -- we can cross the fold, so check side A/B one level down
863 Text
.skip_whitespace_right(State
)
865 Text
.skip_non_whitespace_right(State
)
866 if Text
.cursor_out_of_screen(State
) then
867 Text
.snap_cursor_to_bottom_of_screen(State
)
871 function Text
.skip_whitespace_left(State
)
872 if State
.cursor1
.pos
then
873 Text
.skip_whitespace_leftA(State
)
875 Text
.skip_whitespace_leftB(State
)
879 function Text
.skip_non_whitespace_left(State
)
880 if State
.cursor1
.pos
then
881 Text
.skip_non_whitespace_leftA(State
)
883 Text
.skip_non_whitespace_leftB(State
)
887 function Text
.skip_whitespace_leftA(State
)
889 if State
.cursor1
.pos
== 1 then
892 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%S') then
899 function Text
.skip_whitespace_leftB(State
)
901 if State
.cursor1
.posB
== 1 then
904 if Text
.match(State
.lines
[State
.cursor1
.line
].dataB
, State
.cursor1
.posB
-1, '%S') then
911 function Text
.skip_non_whitespace_leftA(State
)
913 if State
.cursor1
.pos
== 1 then
916 assert(State
.cursor1
.pos
> 1)
917 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%s') then
924 function Text
.skip_non_whitespace_leftB(State
)
926 if State
.cursor1
.posB
== 1 then
929 assert(State
.cursor1
.posB
> 1)
930 if Text
.match(State
.lines
[State
.cursor1
.line
].dataB
, State
.cursor1
.posB
-1, '%s') then
937 function Text
.skip_whitespace_right(State
)
938 if State
.cursor1
.pos
then
939 Text
.skip_whitespace_rightA(State
)
941 Text
.skip_whitespace_rightB(State
)
945 function Text
.skip_non_whitespace_right(State
)
946 if State
.cursor1
.pos
then
947 Text
.skip_non_whitespace_rightA(State
)
949 Text
.skip_non_whitespace_rightB(State
)
953 function Text
.skip_whitespace_rightA(State
)
955 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
958 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%S') then
961 Text
.right_without_scroll(State
)
965 function Text
.skip_whitespace_rightB(State
)
967 if State
.cursor1
.posB
> utf8
.len(State
.lines
[State
.cursor1
.line
].dataB
) then
970 if Text
.match(State
.lines
[State
.cursor1
.line
].dataB
, State
.cursor1
.posB
, '%S') then
973 Text
.right_without_scroll(State
)
977 function Text
.skip_non_whitespace_rightA(State
)
979 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
982 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%s') then
985 Text
.right_without_scroll(State
)
989 function Text
.skip_non_whitespace_rightB(State
)
991 if State
.cursor1
.posB
> utf8
.len(State
.lines
[State
.cursor1
.line
].dataB
) then
994 if Text
.match(State
.lines
[State
.cursor1
.line
].dataB
, State
.cursor1
.posB
, '%s') then
997 Text
.right_without_scroll(State
)
1001 function Text
.match(s
, pos
, pat
)
1002 local start_offset
= Text
.offset(s
, pos
)
1003 assert(start_offset
)
1004 local end_offset
= Text
.offset(s
, pos
+1)
1005 assert(end_offset
> start_offset
)
1006 local curr
= s
:sub(start_offset
, end_offset
-1)
1007 return curr
:match(pat
)
1010 function Text
.left(State
)
1011 if State
.cursor1
.pos
then
1018 function Text
.leftA(State
)
1019 if State
.cursor1
.pos
> 1 then
1020 State
.cursor1
.pos
= State
.cursor1
.pos
-1
1022 local new_cursor_line
= State
.cursor1
.line
1023 while new_cursor_line
> 1 do
1024 new_cursor_line
= new_cursor_line
-1
1025 if State
.lines
[new_cursor_line
].mode
== 'text' then
1027 line
= new_cursor_line
,
1028 pos
= utf8
.len(State
.lines
[new_cursor_line
].data
) + 1,
1034 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
1035 local top2
= Text
.to2(State
, State
.screen_top1
)
1036 top2
= Text
.previous_screen_line(State
, top2
)
1037 State
.screen_top1
= Text
.to1(State
, top2
)
1041 function Text
.leftB(State
)
1042 if State
.cursor1
.posB
> 1 then
1043 State
.cursor1
.posB
= State
.cursor1
.posB
-1
1045 -- overflow back into A side
1046 State
.cursor1
.posB
= nil
1047 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) + 1
1049 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
1050 local top2
= Text
.to2(State
, State
.screen_top1
)
1051 top2
= Text
.previous_screen_line(State
, top2
)
1052 State
.screen_top1
= Text
.to1(State
, top2
)
1056 function Text
.right(State
)
1057 Text
.right_without_scroll(State
)
1058 if Text
.cursor_out_of_screen(State
) then
1059 Text
.snap_cursor_to_bottom_of_screen(State
)
1063 function Text
.right_without_scroll(State
)
1064 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text')
1065 if State
.cursor1
.pos
then
1066 Text
.right_without_scrollA(State
)
1068 Text
.right_without_scrollB(State
)
1072 function Text
.right_without_scrollA(State
)
1073 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
1074 State
.cursor1
.pos
= State
.cursor1
.pos
+1
1076 local new_cursor_line
= State
.cursor1
.line
1077 while new_cursor_line
<= #State
.lines
-1 do
1078 new_cursor_line
= new_cursor_line
+1
1079 if State
.lines
[new_cursor_line
].mode
== 'text' then
1080 State
.cursor1
= {line
=new_cursor_line
, pos
=1}
1087 function Text
.right_without_scrollB(State
)
1088 if State
.cursor1
.posB
<= utf8
.len(State
.lines
[State
.cursor1
.line
].dataB
) then
1089 State
.cursor1
.posB
= State
.cursor1
.posB
+1
1091 -- overflow back into A side
1092 local new_cursor_line
= State
.cursor1
.line
1093 while new_cursor_line
<= #State
.lines
-1 do
1094 new_cursor_line
= new_cursor_line
+1
1095 if State
.lines
[new_cursor_line
].mode
== 'text' then
1096 State
.cursor1
= {line
=new_cursor_line
, pos
=1}
1103 function Text
.pos_at_start_of_screen_line(State
, loc1
)
1104 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
1105 local line_cache
= State
.line_cache
[loc1
.line
]
1106 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
1107 local spos
= line_cache
.screen_line_starting_pos
[i
]
1108 if spos
<= loc1
.pos
then
1115 function Text
.pos_at_start_of_screen_lineB(State
, loc1
)
1116 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
1117 local line_cache
= State
.line_cache
[loc1
.line
]
1118 local x
= Margin_left
+ Text
.screen_line_width(State
, loc1
.line
, #line_cache
.screen_line_starting_pos
) + AB_padding
1119 Text
.populate_screen_line_starting_posB(State
, loc1
.line
, x
)
1120 for i
=#line_cache
.screen_line_starting_posB
,1,-1 do
1121 local sposB
= line_cache
.screen_line_starting_posB
[i
]
1122 if sposB
<= loc1
.posB
then
1129 function Text
.cursor_at_final_screen_line(State
)
1130 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
1131 local line
= State
.lines
[State
.cursor1
.line
]
1132 local screen_lines
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
1133 --? print(screen_lines[#screen_lines], State.cursor1.pos)
1134 if (not State
.expanded
and not line
.expanded
) or
1135 line
.dataB
== nil then
1136 return screen_lines
[#screen_lines
] <= State
.cursor1
.pos
1138 if State
.cursor1
.pos
then
1140 return screen_lines
[#screen_lines
] <= State
.cursor1
.pos
1142 assert(State
.cursor1
.posB
)
1143 local line_cache
= State
.line_cache
[State
.cursor1
.line
]
1144 local x
= Margin_left
+ Text
.screen_line_width(State
, State
.cursor1
.line
, #line_cache
.screen_line_starting_pos
) + AB_padding
1145 Text
.populate_screen_line_starting_posB(State
, State
.cursor1
.line
, x
)
1146 local screen_lines
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_posB
1147 return screen_lines
[#screen_lines
] <= State
.cursor1
.posB
1150 function Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
1152 while State
.cursor1
.line
<= #State
.lines
do
1153 if State
.lines
[State
.cursor1
.line
].mode
== 'text' then
1156 --? print('cursor skips', State.cursor1.line)
1157 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[State
.cursor1
.line
].h
, State
.width
)
1158 State
.cursor1
.line
= State
.cursor1
.line
+ 1
1160 -- hack: insert a text line at bottom of file if necessary
1161 if State
.cursor1
.line
> #State
.lines
then
1162 assert(State
.cursor1
.line
== #State
.lines
+1)
1163 table.insert(State
.lines
, {mode
='text', data
=''})
1164 table.insert(State
.line_cache
, {})
1166 --? print(y, App.screen.height, App.screen.height-State.line_height)
1167 if y
> App
.screen
.height
- State
.line_height
then
1168 --? print('scroll up')
1169 Text
.snap_cursor_to_bottom_of_screen(State
)
1173 -- should never modify State.cursor1
1174 function Text
.snap_cursor_to_bottom_of_screen(State
)
1175 --? print('to2:', State.cursor1.line, State.cursor1.pos, State.cursor1.posB)
1176 local top2
= Text
.to2(State
, State
.cursor1
)
1177 --? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB)
1178 -- slide to start of screen line
1179 if top2
.screen_pos
then
1182 assert(top2
.screen_posB
)
1183 top2
.screen_posB
= 1
1185 --? print('snap', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)
1186 --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
1187 local y
= App
.screen
.height
- State
.line_height
1188 -- duplicate some logic from love.draw
1190 --? print(y, 'top2:', State.lines[top2.line].data, top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB)
1191 if top2
.line
== 1 and top2
.screen_line
== 1 then break end
1192 if top2
.screen_line
> 1 or State
.lines
[top2
.line
-1].mode
== 'text' then
1193 local h
= State
.line_height
1194 if y
- h
< State
.top
then
1199 assert(top2
.line
> 1)
1200 assert(State
.lines
[top2
.line
-1].mode
== 'drawing')
1201 -- We currently can't draw partial drawings, so either skip it entirely
1203 local h
= Drawing_padding_height
+ Drawing
.pixels(State
.lines
[top2
.line
-1].h
, State
.width
)
1204 if y
- h
< State
.top
then
1207 --? print('skipping drawing of height', h)
1210 top2
= Text
.previous_screen_line(State
, top2
)
1212 --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
1213 State
.screen_top1
= Text
.to1(State
, top2
)
1214 --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
1215 --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)
1216 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
1219 function Text
.in_line(State
, line_index
, x
,y
)
1220 local line
= State
.lines
[line_index
]
1221 local line_cache
= State
.line_cache
[line_index
]
1222 if line_cache
.starty
== nil then return false end -- outside current page
1223 if y
< line_cache
.starty
then return false end
1224 local num_screen_lines
= 0
1225 if line_cache
.startpos
then
1226 Text
.populate_screen_line_starting_pos(State
, line_index
)
1227 num_screen_lines
= num_screen_lines
+ #line_cache
.screen_line_starting_pos
- Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
) + 1
1229 --? print('#screenlines after A', num_screen_lines)
1230 if line
.dataB
and (State
.expanded
or line
.expanded
) then
1231 local x
= Margin_left
+ Text
.screen_line_width(State
, line_index
, #line_cache
.screen_line_starting_pos
) + AB_padding
1232 Text
.populate_screen_line_starting_posB(State
, line_index
, x
)
1233 --? print('B:', x, #line_cache.screen_line_starting_posB)
1234 if line_cache
.startposB
then
1235 num_screen_lines
= num_screen_lines
+ #line_cache
.screen_line_starting_posB
- Text
.screen_line_indexB(line_cache
.screen_line_starting_posB
, line_cache
.startposB
) -- no +1; first screen line of B side overlaps with A side
1237 num_screen_lines
= num_screen_lines
+ #line_cache
.screen_line_starting_posB
- Text
.screen_line_indexB(line_cache
.screen_line_starting_posB
, 1) -- no +1; first screen line of B side overlaps with A side
1240 --? print('#screenlines after B', num_screen_lines)
1241 return y
< line_cache
.starty
+ State
.line_height
*num_screen_lines
1244 -- convert mx,my in pixels to schema-1 coordinates
1245 -- returns: pos, posB
1247 -- line without B side
1248 -- line with B side collapsed
1249 -- line with B side expanded
1250 -- line starting rendering in A side (startpos ~= nil)
1251 -- line starting rendering in B side (startposB ~= nil)
1252 -- my on final screen line of A side
1253 -- mx to right of A side with no B side
1254 -- mx to right of A side but left of B side
1255 -- mx to right of B side
1257 -- startpos xor startposB
1258 -- expanded -> dataB
1259 function Text
.to_pos_on_line(State
, line_index
, mx
, my
)
1260 local line
= State
.lines
[line_index
]
1261 local line_cache
= State
.line_cache
[line_index
]
1262 assert(my
>= line_cache
.starty
)
1263 -- duplicate some logic from Text.draw
1264 local y
= line_cache
.starty
1265 --? print('click', line_index, my, 'with line starting at', y, #line_cache.screen_line_starting_pos) -- , #line_cache.screen_line_starting_posB)
1266 if line_cache
.startpos
then
1267 local start_screen_line_index
= Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
)
1268 for screen_line_index
= start_screen_line_index
,#line_cache
.screen_line_starting_pos
do
1269 local screen_line_starting_pos
= line_cache
.screen_line_starting_pos
[screen_line_index
]
1270 local screen_line_starting_byte_offset
= Text
.offset(line
.data
, screen_line_starting_pos
)
1271 --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
1272 local nexty
= y
+ State
.line_height
1274 -- On all wrapped screen lines but the final one, clicks past end of
1275 -- line position cursor on final character of screen line.
1276 -- (The final screen line positions past end of screen line as always.)
1277 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
1278 --? print('past end of non-final line; return')
1279 return line_cache
.screen_line_starting_pos
[screen_line_index
+1]-1
1281 local s
= string.sub(line
.data
, screen_line_starting_byte_offset
)
1282 --? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
1283 local screen_line_posA
= Text
.nearest_cursor_pos(s
, mx
, State
.left
)
1284 if line
.dataB
== nil then
1286 return screen_line_starting_pos
+ screen_line_posA
- 1
1288 if not State
.expanded
and not line
.expanded
then
1289 -- B side is not expanded
1290 return screen_line_starting_pos
+ screen_line_posA
- 1
1292 local lenA
= utf8
.len(s
)
1293 if screen_line_posA
< lenA
then
1294 -- mx is within A side
1295 return screen_line_starting_pos
+ screen_line_posA
- 1
1297 local max_xA
= State
.left
+Text
.x(s
, lenA
+1)
1298 if mx
< max_xA
+ AB_padding
then
1299 -- mx is in the space between A and B side
1300 return screen_line_starting_pos
+ screen_line_posA
- 1
1302 mx
= mx
- max_xA
- AB_padding
1303 local screen_line_posB
= Text
.nearest_cursor_pos(line
.dataB
, mx
, --[[no left margin]] 0)
1304 return nil, screen_line_posB
1309 -- look in screen lines composed entirely of the B side
1310 assert(State
.expanded
or line
.expanded
)
1311 local start_screen_line_indexB
1312 if line_cache
.startposB
then
1313 start_screen_line_indexB
= Text
.screen_line_indexB(line_cache
.screen_line_starting_posB
, line_cache
.startposB
)
1315 start_screen_line_indexB
= 2 -- skip the first line of side B, which we checked above
1317 for screen_line_indexB
= start_screen_line_indexB
,#line_cache
.screen_line_starting_posB
do
1318 local screen_line_starting_posB
= line_cache
.screen_line_starting_posB
[screen_line_indexB
]
1319 local screen_line_starting_byte_offsetB
= Text
.offset(line
.dataB
, screen_line_starting_posB
)
1320 --? print('iter2', y, screen_line_indexB, screen_line_starting_posB, string.sub(line.dataB, screen_line_starting_byte_offsetB))
1321 local nexty
= y
+ State
.line_height
1323 -- On all wrapped screen lines but the final one, clicks past end of
1324 -- line position cursor on final character of screen line.
1325 -- (The final screen line positions past end of screen line as always.)
1326 --? print('aa', mx, State.left, Text.screen_line_widthB(State, line_index, screen_line_indexB))
1327 if screen_line_indexB
< #line_cache
.screen_line_starting_posB
and mx
> State
.left
+ Text
.screen_line_widthB(State
, line_index
, screen_line_indexB
) then
1328 --? print('past end of non-final line; return')
1329 return nil, line_cache
.screen_line_starting_posB
[screen_line_indexB
+1]-1
1331 local s
= string.sub(line
.dataB
, screen_line_starting_byte_offsetB
)
1332 --? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1)
1333 return nil, screen_line_starting_posB
+ Text
.nearest_cursor_pos(s
, mx
, State
.left
) - 1
1340 function Text
.screen_line_width(State
, line_index
, i
)
1341 local line
= State
.lines
[line_index
]
1342 local line_cache
= State
.line_cache
[line_index
]
1343 local start_pos
= line_cache
.screen_line_starting_pos
[i
]
1344 local start_offset
= Text
.offset(line
.data
, start_pos
)
1346 if i
< #line_cache
.screen_line_starting_pos
then
1347 local past_end_pos
= line_cache
.screen_line_starting_pos
[i
+1]
1348 local past_end_offset
= Text
.offset(line
.data
, past_end_pos
)
1349 screen_line
= string.sub(line
.data
, start_offset
, past_end_offset
-1)
1351 screen_line
= string.sub(line
.data
, start_pos
)
1353 local screen_line_text
= App
.newText(love
.graphics
.getFont(), screen_line
)
1354 return App
.width(screen_line_text
)
1357 function Text
.screen_line_widthB(State
, line_index
, i
)
1358 local line
= State
.lines
[line_index
]
1359 local line_cache
= State
.line_cache
[line_index
]
1360 local start_posB
= line_cache
.screen_line_starting_posB
[i
]
1361 local start_offsetB
= Text
.offset(line
.dataB
, start_posB
)
1363 if i
< #line_cache
.screen_line_starting_posB
then
1364 --? print('non-final', i)
1365 local past_end_posB
= line_cache
.screen_line_starting_posB
[i
+1]
1366 local past_end_offsetB
= Text
.offset(line
.dataB
, past_end_posB
)
1367 --? print('between', start_offsetB, past_end_offsetB)
1368 screen_line
= string.sub(line
.dataB
, start_offsetB
, past_end_offsetB
-1)
1370 --? print('final', i)
1371 --? print('after', start_offsetB)
1372 screen_line
= string.sub(line
.dataB
, start_offsetB
)
1374 local screen_line_text
= App
.newText(love
.graphics
.getFont(), screen_line
)
1375 --? local result = App.width(screen_line_text)
1376 --? print('=>', result)
1378 return App
.width(screen_line_text
)
1381 function Text
.screen_line_index(screen_line_starting_pos
, pos
)
1382 for i
= #screen_line_starting_pos
,1,-1 do
1383 if screen_line_starting_pos
[i
] <= pos
then
1389 function Text
.screen_line_indexB(screen_line_starting_posB
, posB
)
1393 assert(screen_line_starting_posB
)
1394 for i
= #screen_line_starting_posB
,1,-1 do
1395 if screen_line_starting_posB
[i
] <= posB
then
1401 -- convert x pixel coordinate to pos
1402 -- oblivious to wrapping
1403 -- result: 1 to len+1
1404 function Text
.nearest_cursor_pos(line
, x
, left
)
1408 local len
= utf8
.len(line
)
1409 local max_x
= left
+Text
.x(line
, len
+1)
1413 local leftpos
, rightpos
= 1, len
+1
1414 --? print('-- nearest', x)
1416 --? print('nearest', x, '^'..line..'$', leftpos, rightpos)
1417 if leftpos
== rightpos
then
1420 local curr
= math
.floor((leftpos
+rightpos
)/2)
1421 local currxmin
= left
+Text
.x(line
, curr
)
1422 local currxmax
= left
+Text
.x(line
, curr
+1)
1423 --? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
1424 if currxmin
<= x
and x
< currxmax
then
1425 if x
-currxmin
< currxmax
-x
then
1431 if leftpos
>= rightpos
-1 then
1434 if currxmin
> x
then
1443 -- return the nearest index of line (in utf8 code points) which lies entirely
1444 -- within x pixels of the left margin
1445 -- result: 0 to len+1
1446 function Text
.nearest_pos_less_than(line
, x
)
1447 --? print('', '-- nearest_pos_less_than', line, x)
1448 local len
= utf8
.len(line
)
1449 local max_x
= Text
.x_after(line
, len
)
1453 local left
, right
= 0, len
+1
1455 local curr
= math
.floor((left
+right
)/2)
1456 local currxmin
= Text
.x_after(line
, curr
+1)
1457 local currxmax
= Text
.x_after(line
, curr
+2)
1458 --? print('', x, left, right, curr, currxmin, currxmax)
1459 if currxmin
<= x
and x
< currxmax
then
1462 if left
>= right
-1 then
1465 if currxmin
> x
then
1474 function Text
.x_after(s
, pos
)
1475 local offset
= Text
.offset(s
, math
.min(pos
+1, #s
+1))
1476 local s_before
= s
:sub(1, offset
-1)
1477 --? print('^'..s_before..'$')
1478 local text_before
= App
.newText(love
.graphics
.getFont(), s_before
)
1479 return App
.width(text_before
)
1482 function Text
.x(s
, pos
)
1483 local offset
= Text
.offset(s
, pos
)
1484 local s_before
= s
:sub(1, offset
-1)
1485 local text_before
= App
.newText(love
.graphics
.getFont(), s_before
)
1486 return App
.width(text_before
)
1489 function Text
.to2(State
, loc1
)
1490 if State
.lines
[loc1
.line
].mode
== 'drawing' then
1491 return {line
=loc1
.line
, screen_line
=1, screen_pos
=1}
1494 return Text
.to2A(State
, loc1
)
1496 return Text
.to2B(State
, loc1
)
1500 function Text
.to2A(State
, loc1
)
1501 local result
= {line
=loc1
.line
}
1502 local line_cache
= State
.line_cache
[loc1
.line
]
1503 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
1504 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
1505 local spos
= line_cache
.screen_line_starting_pos
[i
]
1506 if spos
<= loc1
.pos
then
1507 result
.screen_line
= i
1508 result
.screen_pos
= loc1
.pos
- spos
+ 1
1512 assert(result
.screen_pos
)
1516 function Text
.to2B(State
, loc1
)
1517 local result
= {line
=loc1
.line
}
1518 local line_cache
= State
.line_cache
[loc1
.line
]
1519 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
1520 local x
= Margin_left
+ Text
.screen_line_width(State
, loc1
.line
, #line_cache
.screen_line_starting_pos
) + AB_padding
1521 Text
.populate_screen_line_starting_posB(State
, loc1
.line
, x
)
1522 for i
=#line_cache
.screen_line_starting_posB
,1,-1 do
1523 local sposB
= line_cache
.screen_line_starting_posB
[i
]
1524 if sposB
<= loc1
.posB
then
1525 result
.screen_lineB
= i
1526 result
.screen_posB
= loc1
.posB
- sposB
+ 1
1530 assert(result
.screen_posB
)
1534 function Text
.to1(State
, loc2
)
1535 if loc2
.screen_pos
then
1536 return Text
.to1A(State
, loc2
)
1538 return Text
.to1B(State
, loc2
)
1542 function Text
.to1A(State
, loc2
)
1543 local result
= {line
=loc2
.line
, pos
=loc2
.screen_pos
}
1544 if loc2
.screen_line
> 1 then
1545 result
.pos
= State
.line_cache
[loc2
.line
].screen_line_starting_pos
[loc2
.screen_line
] + loc2
.screen_pos
- 1
1550 function Text
.to1B(State
, loc2
)
1551 local result
= {line
=loc2
.line
, posB
=loc2
.screen_posB
}
1552 if loc2
.screen_lineB
> 1 then
1553 result
.posB
= State
.line_cache
[loc2
.line
].screen_line_starting_posB
[loc2
.screen_lineB
] + loc2
.screen_posB
- 1
1558 function Text
.lt1(a
, b
)
1559 if a
.line
< b
.line
then
1562 if a
.line
> b
.line
then
1566 if a
.pos
and not b
.pos
then
1569 if not a
.pos
and b
.pos
then
1573 return a
.pos
< b
.pos
1575 return a
.posB
< b
.posB
1579 function Text
.le1(a
, b
)
1580 return eq(a
, b
) or Text
.lt1(a
, b
)
1583 function Text
.offset(s
, pos1
)
1584 if pos1
== 1 then return 1 end
1585 local result
= utf8
.offset(s
, pos1
)
1586 if result
== nil then
1593 function Text
.previous_screen_line(State
, loc2
)
1594 if loc2
.screen_pos
then
1595 return Text
.previous_screen_lineA(State
, loc2
)
1597 return Text
.previous_screen_lineB(State
, loc2
)
1601 function Text
.previous_screen_lineA(State
, loc2
)
1602 if loc2
.screen_line
> 1 then
1603 return {line
=loc2
.line
, screen_line
=loc2
.screen_line
-1, screen_pos
=1}
1604 elseif loc2
.line
== 1 then
1607 Text
.populate_screen_line_starting_pos(State
, loc2
.line
-1)
1608 if State
.lines
[loc2
.line
-1].dataB
== nil or
1609 (not State
.expanded
and not State
.lines
[loc2
.line
-1].expanded
) then
1610 --? print('c1', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, State.line_cache[loc2.line-1].fragmentsB)
1611 return {line
=loc2
.line
-1, screen_line
=#State
.line_cache
[loc2
.line
-1].screen_line_starting_pos
, screen_pos
=1}
1613 -- try to switch to B
1614 local prev_line_cache
= State
.line_cache
[loc2
.line
-1]
1615 local x
= Margin_left
+ Text
.screen_line_width(State
, loc2
.line
-1, #prev_line_cache
.screen_line_starting_pos
) + AB_padding
1616 Text
.populate_screen_line_starting_posB(State
, loc2
.line
-1, x
)
1617 local screen_line_starting_posB
= State
.line_cache
[loc2
.line
-1].screen_line_starting_posB
1618 --? print('c', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, '==', #screen_line_starting_posB, 'starting from x', x)
1619 if #screen_line_starting_posB
> 1 then
1621 return {line
=loc2
.line
-1, screen_lineB
=#State
.line_cache
[loc2
.line
-1].screen_line_starting_posB
, screen_posB
=1}
1624 -- if there's only one screen line, assume it overlaps with A, so remain in A
1625 return {line
=loc2
.line
-1, screen_line
=#State
.line_cache
[loc2
.line
-1].screen_line_starting_pos
, screen_pos
=1}
1630 function Text
.previous_screen_lineB(State
, loc2
)
1631 if loc2
.screen_lineB
> 2 then -- first screen line of B side overlaps with A side
1632 return {line
=loc2
.line
, screen_lineB
=loc2
.screen_lineB
-1, screen_posB
=1}
1635 -- TODO: handle case where fold lands precisely at end of a new screen-line
1636 return {line
=loc2
.line
, screen_line
=#State
.line_cache
[loc2
.line
].screen_line_starting_pos
, screen_pos
=1}
1641 function Text
.tweak_screen_top_and_cursor(State
)
1642 if State
.screen_top1
.pos
== 1 then return end
1643 Text
.populate_screen_line_starting_pos(State
, State
.screen_top1
.line
)
1644 local line
= State
.lines
[State
.screen_top1
.line
]
1645 local line_cache
= State
.line_cache
[State
.screen_top1
.line
]
1646 for i
=2,#line_cache
.screen_line_starting_pos
do
1647 local pos
= line_cache
.screen_line_starting_pos
[i
]
1648 if pos
== State
.screen_top1
.pos
then
1651 if pos
> State
.screen_top1
.pos
then
1652 -- make sure screen top is at start of a screen line
1653 local prev
= line_cache
.screen_line_starting_pos
[i
-1]
1654 if State
.screen_top1
.pos
- prev
< pos
- State
.screen_top1
.pos
then
1655 State
.screen_top1
.pos
= prev
1657 State
.screen_top1
.pos
= pos
1662 -- make sure cursor is on screen
1663 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
1664 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
1665 elseif State
.cursor1
.line
>= State
.screen_bottom1
.line
then
1666 --? print('too low')
1667 if Text
.cursor_out_of_screen(State
) then
1669 local pos
,posB
= Text
.to_pos_on_line(State
, State
.screen_bottom1
.line
, State
.right
-5, App
.screen
.height
-5)
1670 State
.cursor1
= {line
=State
.screen_bottom1
.line
, pos
=pos
, posB
=posB
}
1675 -- slightly expensive since it redraws the screen
1676 function Text
.cursor_out_of_screen(State
)
1678 return State
.cursor_y
== nil
1679 -- this approach is cheaper and almost works, except on the final screen
1680 -- where file ends above bottom of screen
1681 --? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)
1682 --? local botline1 = {line=State.cursor1.line, pos=botpos}
1683 --? return Text.lt1(State.screen_bottom1, botline1)
1686 function source
.link_exists(State
, filename
)
1687 if State
.link_cache
== nil then
1688 State
.link_cache
= {}
1690 if State
.link_cache
[filename
] == nil then
1691 State
.link_cache
[filename
] = file_exists(filename
)
1693 return State
.link_cache
[filename
]
1696 function Text
.redraw_all(State
)
1697 --? print('clearing fragments')
1698 State
.line_cache
= {}
1699 for i
=1,#State
.lines
do
1700 State
.line_cache
[i
] = {}
1702 State
.link_cache
= {}
1705 function Text
.clear_screen_line_cache(State
, line_index
)
1706 State
.line_cache
[line_index
].fragments
= nil
1707 State
.line_cache
[line_index
].fragmentsB
= nil
1708 State
.line_cache
[line_index
].screen_line_starting_pos
= nil
1709 State
.line_cache
[line_index
].screen_line_starting_posB
= nil
1713 return s
:gsub('^%s+', ''):gsub('%s+$', '')
1717 return s
:gsub('^%s+', '')
1721 return s
:gsub('%s+$', '')
1724 function starts_with(s
, sub
)
1725 return s
:find(sub
, 1, --[[no escapes]] true) == 1
1728 function ends_with(s
, sub
)
1729 return s
:reverse():find(sub
:reverse(), 1, --[[no escapes]] true) == 1