1 -- text editor, particularly text drawing, horizontal wrap, vertical scrolling
4 -- draw a line starting from startpos to screen at y between State.left and State.right
5 -- return y for the next line
6 function Text
.draw(State
, line_index
, y
, startpos
, hide_cursor
, show_line_numbers
)
7 local line
= State
.lines
[line_index
]
8 local line_cache
= State
.line_cache
[line_index
]
9 line_cache
.startpos
= startpos
11 Text
.populate_screen_line_starting_pos(State
, line_index
)
12 Text
.populate_link_offsets(State
, line_index
)
13 if show_line_numbers
then
14 App
.color(Line_number_color
)
15 love
.graphics
.print(line_index
, State
.left
-Line_number_width
*State
.font
:getWidth('m')+10,y
)
18 assert(#line_cache
.screen_line_starting_pos
>= 1, 'line cache missing screen line info')
19 for i
=1,#line_cache
.screen_line_starting_pos
do
20 local pos
= line_cache
.screen_line_starting_pos
[i
]
21 if pos
< startpos
then
23 --? print('skipping', screen_line)
25 local screen_line
= Text
.screen_line(line
, line_cache
, i
)
26 --? print('text.draw:', screen_line, 'at', line_index,pos, 'after', x,y)
27 local frag_len
= utf8
.len(screen_line
)
28 -- render any highlights
29 for _
,link_offsets
in ipairs(line_cache
.link_offsets
) do
30 -- render link decorations
31 local s
,e
,filename
= unpack(link_offsets
)
32 local lo
, hi
= Text
.clip_wikiword_with_screen_line(State
.font
, line
, line_cache
, i
, s
, e
)
34 button(State
, 'link', {x
=State
.left
+lo
, y
=y
, w
=hi
-lo
, h
=State
.line_height
,
35 icon
= icon
.hyperlink_decoration
,
37 if file_exists(filename
) then
38 source
.switch_to_file(filename
)
44 if State
.selection1
.line
then
45 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
46 Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, lo
,hi
)
48 if not hide_cursor
and line_index
== State
.cursor1
.line
then
49 -- render search highlight or cursor
50 if State
.search_term
then
51 local data
= State
.lines
[State
.cursor1
.line
].data
52 local cursor_offset
= Text
.offset(data
, State
.cursor1
.pos
)
53 if data
:sub(cursor_offset
, cursor_offset
+#State
.search_term
-1) == State
.search_term
then
54 local save_selection
= State
.selection1
55 State
.selection1
= {line
=line_index
, pos
=State
.cursor1
.pos
+utf8
.len(State
.search_term
)}
56 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
57 Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, lo
,hi
)
58 State
.selection1
= save_selection
60 elseif Focus
== 'edit' then
61 if pos
<= State
.cursor1
.pos
and pos
+ frag_len
> State
.cursor1
.pos
then
62 Text
.draw_cursor(State
, State
.left
+Text
.x(State
.font
, screen_line
, State
.cursor1
.pos
-pos
+1), y
)
63 elseif pos
+ frag_len
== State
.cursor1
.pos
then
64 -- Show cursor at end of line.
65 -- This place also catches end of wrapping screen lines. That doesn't seem worth distinguishing.
66 -- It seems useful to see a cursor whether your eye is on the left or right margin.
67 Text
.draw_cursor(State
, State
.left
+Text
.x(State
.font
, screen_line
, State
.cursor1
.pos
-pos
+1), y
)
71 -- render colorized text
73 for frag
in screen_line
:gmatch('%S*%s*') do
75 App
.screen
.print(frag
, x
,y
)
76 x
= x
+State
.font
:getWidth(frag
)
78 y
= y
+ State
.line_height
79 if y
>= App
.screen
.height
then
87 function Text
.screen_line(line
, line_cache
, i
)
88 local pos
= line_cache
.screen_line_starting_pos
[i
]
89 local offset
= Text
.offset(line
.data
, pos
)
90 if i
>= #line_cache
.screen_line_starting_pos
then
91 return line
.data
:sub(offset
)
93 local endpos
= line_cache
.screen_line_starting_pos
[i
+1]-1
94 local end_offset
= Text
.offset(line
.data
, endpos
)
95 return line
.data
:sub(offset
, end_offset
)
98 function Text
.draw_cursor(State
, x
, y
)
100 if math
.floor(Cursor_time
*2)%2 == 0 then
101 App
.color(Cursor_color
)
102 love
.graphics
.rectangle('fill', x
,y
, 3,State
.line_height
)
105 State
.cursor_y
= y
+State
.line_height
108 function Text
.populate_screen_line_starting_pos(State
, line_index
)
109 local line
= State
.lines
[line_index
]
110 if line
.mode
~= 'text' then return end
111 local line_cache
= State
.line_cache
[line_index
]
112 if line_cache
.screen_line_starting_pos
then
115 line_cache
.screen_line_starting_pos
= {1}
118 -- try to wrap at word boundaries
119 for frag
in line
.data
:gmatch('%S*%s*') do
120 local frag_width
= State
.font
:getWidth(frag
)
121 --? print('-- frag:', frag, pos, x, frag_width, State.width)
122 while x
+ frag_width
> State
.width
do
123 --? print('frag:', frag, pos, x, frag_width, State.width)
124 if x
< 0.8 * State
.width
then
125 -- long word; chop it at some letter
126 -- We're not going to reimplement TeX here.
127 local bpos
= Text
.nearest_pos_less_than(State
.font
, frag
, State
.width
- x
)
128 if x
== 0 and bpos
== 0 then
129 assert(false, ("Infinite loop while line-wrapping. Editor is %dpx wide; window is %dpx wide"):format(State
.width
, App
.screen
.width
))
132 local boffset
= Text
.offset(frag
, bpos
+1) -- byte _after_ bpos
133 frag
= string.sub(frag
, boffset
)
135 --? print('after chop:', frag)
137 frag_width
= State
.font
:getWidth(frag
)
139 --? print('screen line:', pos)
140 table.insert(line_cache
.screen_line_starting_pos
, pos
)
141 x
= 0 -- new screen line
144 pos
= pos
+ utf8
.len(frag
)
148 function Text
.populate_link_offsets(State
, line_index
)
149 local line
= State
.lines
[line_index
]
150 if line
.mode
~= 'text' then return end
151 local line_cache
= State
.line_cache
[line_index
]
152 if line_cache
.link_offsets
then
155 line_cache
.link_offsets
= {}
157 -- try to wrap at word boundaries
159 while s
<= #line
.data
do
160 s
, e
= line
.data
:find('%[%[%S+%]%]', s
)
161 if s
== nil then break end
162 local word
= line
.data
:sub(s
+2, e
-2) -- strip out surrounding '[[..]]'
163 --? print('wikiword:', s, e, word)
164 table.insert(line_cache
.link_offsets
, {s
, e
, word
})
169 -- Intersect the filename between byte offsets s,e with the bounds of screen line i.
170 -- Return the left/right pixel coordinates of of the intersection,
171 -- or nil if it doesn't intersect with screen line i.
172 function Text
.clip_wikiword_with_screen_line(font
, line
, line_cache
, i
, s
, e
)
173 local spos
= line_cache
.screen_line_starting_pos
[i
]
174 local soff
= Text
.offset(line
.data
, spos
)
179 if i
< #line_cache
.screen_line_starting_pos
then
180 local epos
= line_cache
.screen_line_starting_pos
[i
+1]
181 eoff
= Text
.offset(line
.data
, epos
)
186 local loff
= math
.max(s
, soff
)
189 hoff
= math
.min(e
, eoff
)
193 --? print(s, e, soff, eoff, loff, hoff)
194 return font
:getWidth(line
.data
:sub(soff
, loff
-1)), font
:getWidth(line
.data
:sub(soff
, hoff
))
197 function Text
.text_input(State
, t
)
198 if App
.mouse_down(1) then return end
199 if App
.any_modifier_down() then
200 if App
.key_down(t
) then
201 -- The modifiers didn't change the key. Handle it in keychord_pressed.
204 -- Key mutated by the keyboard layout. Continue below.
207 local before
= snapshot(State
, State
.cursor1
.line
)
208 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
209 Text
.insert_at_cursor(State
, t
)
210 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
211 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
212 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
214 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
217 function Text
.insert_at_cursor(State
, t
)
218 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text', 'line is not text')
219 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
220 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
)
221 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
222 State
.cursor1
.pos
= State
.cursor1
.pos
+1
225 -- Don't handle any keys here that would trigger text_input above.
226 function Text
.keychord_press(State
, chord
)
227 --? print('chord', chord, State.selection1.line, State.selection1.pos)
228 --== shortcuts that mutate text
229 if chord
== 'return' then
230 local before_line
= State
.cursor1
.line
231 local before
= snapshot(State
, before_line
)
232 Text
.insert_return(State
)
233 State
.selection1
= {}
234 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
235 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
238 record_undo_event(State
, {before
=before
, after
=snapshot(State
, before_line
, State
.cursor1
.line
)})
239 elseif chord
== 'tab' then
240 local before
= snapshot(State
, State
.cursor1
.line
)
241 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
242 Text
.insert_at_cursor(State
, '\t')
243 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
244 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
245 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
246 --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
249 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
250 elseif chord
== 'backspace' then
251 if State
.selection1
.line
then
252 Text
.delete_selection(State
, State
.left
, State
.right
)
257 if State
.cursor1
.pos
> 1 then
258 before
= snapshot(State
, State
.cursor1
.line
)
259 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1)
260 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
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 State
.cursor1
.pos
= State
.cursor1
.pos
-1
269 elseif State
.cursor1
.line
> 1 then
270 before
= snapshot(State
, State
.cursor1
.line
-1, State
.cursor1
.line
)
271 if State
.lines
[State
.cursor1
.line
-1].mode
== 'drawing' then
272 table.remove(State
.lines
, State
.cursor1
.line
-1)
273 table.remove(State
.line_cache
, State
.cursor1
.line
-1)
276 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
-1].data
)+1
277 State
.lines
[State
.cursor1
.line
-1].data
= State
.lines
[State
.cursor1
.line
-1].data
..State
.lines
[State
.cursor1
.line
].data
278 table.remove(State
.lines
, State
.cursor1
.line
)
279 table.remove(State
.line_cache
, State
.cursor1
.line
)
281 State
.cursor1
.line
= State
.cursor1
.line
-1
283 if State
.screen_top1
.line
> #State
.lines
then
284 Text
.populate_screen_line_starting_pos(State
, #State
.lines
)
285 local line_cache
= State
.line_cache
[#State
.line_cache
]
286 State
.screen_top1
= {line
=#State
.lines
, pos
=line_cache
.screen_line_starting_pos
[#line_cache
.screen_line_starting_pos
]}
287 elseif Text
.lt1(State
.cursor1
, State
.screen_top1
) then
288 State
.screen_top1
= {
289 line
=State
.cursor1
.line
,
290 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
292 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
294 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
295 assert(Text
.le1(State
.screen_top1
, State
.cursor1
), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State
.screen_top1
.line
, State
.screen_top1
.pos
, State
.cursor1
.line
, State
.cursor1
.pos
))
297 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
298 elseif chord
== 'delete' then
299 if State
.selection1
.line
then
300 Text
.delete_selection(State
, State
.left
, State
.right
)
305 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
306 before
= snapshot(State
, State
.cursor1
.line
)
308 before
= snapshot(State
, State
.cursor1
.line
, State
.cursor1
.line
+1)
310 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
311 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
312 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
+1)
315 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
)
317 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
319 -- no change to State.cursor1.pos
321 elseif State
.cursor1
.line
< #State
.lines
then
322 if State
.lines
[State
.cursor1
.line
+1].mode
== 'text' then
324 State
.lines
[State
.cursor1
.line
].data
= State
.lines
[State
.cursor1
.line
].data
..State
.lines
[State
.cursor1
.line
+1].data
326 table.remove(State
.lines
, State
.cursor1
.line
+1)
327 table.remove(State
.line_cache
, State
.cursor1
.line
+1)
329 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
331 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
332 --== shortcuts that move the cursor
333 elseif chord
== 'left' then
335 State
.selection1
= {}
336 elseif chord
== 'right' then
338 State
.selection1
= {}
339 elseif chord
== 'S-left' then
340 if State
.selection1
.line
== nil then
341 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
344 elseif chord
== 'S-right' then
345 if State
.selection1
.line
== nil then
346 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
349 -- C- hotkeys reserved for drawings, so we'll use M-
350 elseif chord
== 'M-left' then
351 Text
.word_left(State
)
352 State
.selection1
= {}
353 elseif chord
== 'M-right' then
354 Text
.word_right(State
)
355 State
.selection1
= {}
356 elseif chord
== 'M-S-left' then
357 if State
.selection1
.line
== nil then
358 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
360 Text
.word_left(State
)
361 elseif chord
== 'M-S-right' then
362 if State
.selection1
.line
== nil then
363 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
365 Text
.word_right(State
)
366 elseif chord
== 'home' then
367 Text
.start_of_line(State
)
368 State
.selection1
= {}
369 elseif chord
== 'end' then
370 Text
.end_of_line(State
)
371 State
.selection1
= {}
372 elseif chord
== 'S-home' then
373 if State
.selection1
.line
== nil then
374 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
376 Text
.start_of_line(State
)
377 elseif chord
== 'S-end' then
378 if State
.selection1
.line
== nil then
379 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
381 Text
.end_of_line(State
)
382 elseif chord
== 'up' then
384 State
.selection1
= {}
385 elseif chord
== 'down' then
387 State
.selection1
= {}
388 elseif chord
== 'S-up' then
389 if State
.selection1
.line
== nil then
390 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
393 elseif chord
== 'S-down' then
394 if State
.selection1
.line
== nil then
395 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
398 elseif chord
== 'pageup' then
400 State
.selection1
= {}
401 elseif chord
== 'pagedown' then
403 State
.selection1
= {}
404 elseif chord
== 'S-pageup' then
405 if State
.selection1
.line
== nil then
406 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
409 elseif chord
== 'S-pagedown' then
410 if State
.selection1
.line
== nil then
411 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
417 function Text
.insert_return(State
)
418 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
419 table.insert(State
.lines
, State
.cursor1
.line
+1, {mode
='text', data
=string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_offset
)})
420 table.insert(State
.line_cache
, State
.cursor1
.line
+1, {})
421 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_offset
-1)
422 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
423 State
.cursor1
= {line
=State
.cursor1
.line
+1, pos
=1}
426 function Text
.pageup(State
)
427 State
.screen_top1
= Text
.previous_screen_top1(State
)
428 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
429 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
430 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
433 -- return the top y coordinate of a given line_index,
434 -- or nil if no part of it is on screen
435 function Text
.starty(State
, line_index
)
436 -- duplicate some logic from love.draw
437 -- does not modify State (except to populate line_cache)
438 if line_index
< State
.screen_top1
.line
then return end
439 local loc2
= Text
.to2(State
, State
.screen_top1
)
442 if loc2
.line
== line_index
then return y
end
443 if State
.lines
[loc2
.line
].mode
== 'text' then
444 y
= y
+ State
.line_height
445 elseif State
.lines
[loc2
.line
].mode
== 'drawing' then
446 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[loc2
.line
].h
, State
.width
)
448 if y
+ State
.line_height
> App
.screen
.height
then break end
449 local next_loc2
= Text
.next_screen_line(State
, loc2
)
450 if Text
.eq2(next_loc2
, loc2
) then break end -- end of file
455 function Text
.previous_screen_top1(State
)
456 -- duplicate some logic from love.draw
457 -- does not modify State (except to populate line_cache)
458 local loc2
= Text
.to2(State
, State
.screen_top1
)
459 local y
= App
.screen
.height
- State
.line_height
460 while y
>= State
.top
do
461 if loc2
.line
== 1 and loc2
.screen_line
== 1 and loc2
.screen_pos
== 1 then break end
462 if State
.lines
[loc2
.line
].mode
== 'text' then
463 y
= y
- State
.line_height
464 elseif State
.lines
[loc2
.line
].mode
== 'drawing' then
465 y
= y
- Drawing_padding_height
- Drawing
.pixels(State
.lines
[loc2
.line
].h
, State
.width
)
467 loc2
= Text
.previous_screen_line(State
, loc2
)
469 return Text
.to1(State
, loc2
)
472 function Text
.pagedown(State
)
473 State
.screen_top1
= Text
.screen_bottom1(State
)
474 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
475 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
476 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
479 -- return the location of the start of the bottom-most line on screen
480 function Text
.screen_bottom1(State
)
481 -- duplicate some logic from love.draw
482 -- does not modify State (except to populate line_cache)
483 local loc2
= Text
.to2(State
, State
.screen_top1
)
486 if State
.lines
[loc2
.line
].mode
== 'text' then
487 y
= y
+ State
.line_height
488 elseif State
.lines
[loc2
.line
].mode
== 'drawing' then
489 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[loc2
.line
].h
, State
.width
)
491 if y
+ State
.line_height
> App
.screen
.height
then break end
492 local next_loc2
= Text
.next_screen_line(State
, loc2
)
493 if Text
.eq2(next_loc2
, loc2
) then break end
496 return Text
.to1(State
, loc2
)
499 function Text
.up(State
)
500 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text', 'line is not text')
501 --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
502 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
503 if screen_line_starting_pos
== 1 then
504 --? print('cursor is at first screen line of its line')
505 -- line is done; skip to previous text line
506 local new_cursor_line
= State
.cursor1
.line
507 while new_cursor_line
> 1 do
508 new_cursor_line
= new_cursor_line
-1
509 if State
.lines
[new_cursor_line
].mode
== 'text' then
510 --? print('found previous text line')
511 State
.cursor1
= {line
=new_cursor_line
, pos
=nil}
512 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
513 -- previous text line found, pick its final screen line
514 --? print('has multiple screen lines')
515 local screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
516 --? print(#screen_line_starting_pos)
517 screen_line_starting_pos
= screen_line_starting_pos
[#screen_line_starting_pos
]
518 local screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_pos
)
519 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_byte_offset
)
520 State
.cursor1
.pos
= screen_line_starting_pos
+ Text
.nearest_cursor_pos(State
.font
, s
, State
.cursor_x
, State
.left
) - 1
525 -- move up one screen line in current line
526 assert(screen_line_index
> 1, 'bumped up against top screen line in line')
527 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
-1]
528 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
529 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
530 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(State
.font
, s
, State
.cursor_x
, State
.left
) - 1
531 --? print('cursor pos is now '..tostring(State.cursor1.pos))
533 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
534 State
.screen_top1
= {
535 line
=State
.cursor1
.line
,
536 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
538 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
542 function Text
.down(State
)
543 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text', 'line is not text')
544 --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
545 assert(State
.cursor1
.pos
, 'cursor has no pos')
546 if Text
.cursor_at_final_screen_line(State
) then
547 -- line is done, skip to next text line
548 --? print('cursor at final screen line of its line')
549 local new_cursor_line
= State
.cursor1
.line
550 while new_cursor_line
< #State
.lines
do
551 new_cursor_line
= new_cursor_line
+1
552 if State
.lines
[new_cursor_line
].mode
== 'text' then
554 line
= new_cursor_line
,
555 pos
= Text
.nearest_cursor_pos(State
.font
, State
.lines
[new_cursor_line
].data
, State
.cursor_x
, State
.left
),
557 --? print(State.cursor1.pos)
561 local screen_bottom1
= Text
.screen_bottom1(State
)
562 --? print('down 2', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, screen_bottom1.line, screen_bottom1.pos)
563 if State
.cursor1
.line
> screen_bottom1
.line
then
564 --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
565 --? print('scroll up preserving cursor')
566 Text
.snap_cursor_to_bottom_of_screen(State
)
567 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
570 -- move down one screen line in current line
571 local screen_bottom1
= Text
.screen_bottom1(State
)
572 local scroll_down
= Text
.le1(screen_bottom1
, State
.cursor1
)
573 --? print('cursor is NOT at final screen line of its line')
574 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
575 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
576 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
+1]
577 --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
578 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
579 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
580 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(State
.font
, s
, State
.cursor_x
, State
.left
) - 1
581 --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
583 --? print('scroll up preserving cursor')
584 Text
.snap_cursor_to_bottom_of_screen(State
)
585 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
588 --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
591 function Text
.start_of_line(State
)
592 State
.cursor1
.pos
= 1
593 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
594 State
.screen_top1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
} -- copy
598 function Text
.end_of_line(State
)
599 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) + 1
600 if Text
.cursor_out_of_screen(State
) then
601 Text
.snap_cursor_to_bottom_of_screen(State
)
605 function Text
.word_left(State
)
606 -- skip some whitespace
608 if State
.cursor1
.pos
== 1 then
611 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%S') then
616 -- skip some non-whitespace
619 if State
.cursor1
.pos
== 1 then
622 assert(State
.cursor1
.pos
> 1, 'bumped up against start of line')
623 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%s') then
629 function Text
.word_right(State
)
630 -- skip some whitespace
632 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
635 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%S') then
638 Text
.right_without_scroll(State
)
641 Text
.right_without_scroll(State
)
642 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
645 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%s') then
649 if Text
.cursor_out_of_screen(State
) then
650 Text
.snap_cursor_to_bottom_of_screen(State
)
654 function Text
.match(s
, pos
, pat
)
655 local start_offset
= Text
.offset(s
, pos
)
656 local end_offset
= Text
.offset(s
, pos
+1)
657 assert(end_offset
> start_offset
, ('end_offset %d not > start_offset %d'):format(end_offset
, start_offset
))
658 local curr
= s
:sub(start_offset
, end_offset
-1)
659 return curr
:match(pat
)
662 function Text
.left(State
)
663 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text', 'line is not text')
664 if State
.cursor1
.pos
> 1 then
665 State
.cursor1
.pos
= State
.cursor1
.pos
-1
667 local new_cursor_line
= State
.cursor1
.line
668 while new_cursor_line
> 1 do
669 new_cursor_line
= new_cursor_line
-1
670 if State
.lines
[new_cursor_line
].mode
== 'text' then
672 line
= new_cursor_line
,
673 pos
= utf8
.len(State
.lines
[new_cursor_line
].data
) + 1,
679 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
680 State
.screen_top1
= {
681 line
=State
.cursor1
.line
,
682 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
684 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
688 function Text
.right(State
)
689 Text
.right_without_scroll(State
)
690 if Text
.cursor_out_of_screen(State
) then
691 Text
.snap_cursor_to_bottom_of_screen(State
)
695 function Text
.right_without_scroll(State
)
696 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text', 'line is not text')
697 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
698 State
.cursor1
.pos
= State
.cursor1
.pos
+1
700 local new_cursor_line
= State
.cursor1
.line
701 while new_cursor_line
<= #State
.lines
-1 do
702 new_cursor_line
= new_cursor_line
+1
703 if State
.lines
[new_cursor_line
].mode
== 'text' then
704 State
.cursor1
= {line
=new_cursor_line
, pos
=1}
711 -- result: pos, index of screen line
712 function Text
.pos_at_start_of_screen_line(State
, loc1
)
713 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
714 local line_cache
= State
.line_cache
[loc1
.line
]
715 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
716 local spos
= line_cache
.screen_line_starting_pos
[i
]
717 if spos
<= loc1
.pos
then
721 assert(false, ('invalid pos %d'):format(loc1
.pos
))
724 function Text
.pos_at_end_of_screen_line(State
, loc1
)
725 assert(State
.lines
[loc1
.line
].mode
== 'text')
726 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
727 local line_cache
= State
.line_cache
[loc1
.line
]
728 local most_recent_final_pos
= utf8
.len(State
.lines
[loc1
.line
].data
)+1
729 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
730 local spos
= line_cache
.screen_line_starting_pos
[i
]
731 if spos
<= loc1
.pos
then
732 return most_recent_final_pos
734 most_recent_final_pos
= spos
-1
736 assert(false, ('invalid pos %d'):format(loc1
.pos
))
739 function Text
.final_text_loc_on_screen(State
)
740 local screen_bottom1
= Text
.screen_bottom1(State
)
741 if State
.lines
[screen_bottom1
.line
].mode
== 'text' then
743 line
=screen_bottom1
.line
,
744 pos
=Text
.pos_at_end_of_screen_line(State
, screen_bottom1
),
747 local loc2
= Text
.to2(State
, screen_bottom1
)
749 if State
.lines
[loc2
.line
].mode
== 'text' then break end
750 assert(loc2
.line
> 1 or loc2
.screen_line
> 1 and loc2
.screen_pos
> 1) -- elsewhere we're making sure there's always at least one text line on screen
751 loc2
= Text
.previous_screen_line(State
, loc2
)
753 local result
= Text
.to1(State
, loc2
)
754 result
.pos
= Text
.pos_at_end_of_screen_line(State
, result
)
758 function Text
.cursor_at_final_screen_line(State
)
759 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
760 local screen_lines
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
761 --? print(screen_lines[#screen_lines], State.cursor1.pos)
762 return screen_lines
[#screen_lines
] <= State
.cursor1
.pos
765 function Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
767 while State
.cursor1
.line
<= #State
.lines
do
768 if State
.lines
[State
.cursor1
.line
].mode
== 'text' then
771 --? print('cursor skips', State.cursor1.line)
772 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[State
.cursor1
.line
].h
, State
.width
)
773 State
.cursor1
.line
= State
.cursor1
.line
+ 1
775 if State
.cursor1
.pos
== nil then
776 State
.cursor1
.pos
= 1
778 -- hack: insert a text line at bottom of file if necessary
779 if State
.cursor1
.line
> #State
.lines
then
780 assert(State
.cursor1
.line
== #State
.lines
+1, 'tried to ensure bottom line of file is text, but failed')
781 table.insert(State
.lines
, {mode
='text', data
=''})
782 table.insert(State
.line_cache
, {})
784 --? print(y, App.screen.height, App.screen.height-State.line_height)
785 if y
> App
.screen
.height
- State
.line_height
then
786 --? print('scroll up')
787 Text
.snap_cursor_to_bottom_of_screen(State
)
791 -- should never modify State.cursor1
792 function Text
.snap_cursor_to_bottom_of_screen(State
)
793 --? print('to2:', State.cursor1.line, State.cursor1.pos)
794 local top2
= Text
.to2(State
, State
.cursor1
)
795 --? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
796 -- slide to start of screen line
797 top2
.screen_pos
= 1 -- start of screen line
798 --? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
799 --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
800 local y
= App
.screen
.height
- State
.line_height
801 -- duplicate some logic from love.draw
803 --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
804 if top2
.line
== 1 and top2
.screen_line
== 1 then break end
805 if top2
.screen_line
> 1 or State
.lines
[top2
.line
-1].mode
== 'text' then
806 local h
= State
.line_height
807 if y
- h
< State
.top
then
812 assert(top2
.line
> 1, 'tried to snap cursor to buttom of screen but failed')
813 assert(State
.lines
[top2
.line
-1].mode
== 'drawing', "expected a drawing but it's not")
814 -- We currently can't draw partial drawings, so either skip it entirely
816 local h
= Drawing_padding_height
+ Drawing
.pixels(State
.lines
[top2
.line
-1].h
, State
.width
)
817 if y
- h
< State
.top
then
820 --? print('skipping drawing of height', h)
823 top2
= Text
.previous_screen_line(State
, top2
)
825 --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
826 State
.screen_top1
= Text
.to1(State
, top2
)
827 --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
828 --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
829 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
832 function Text
.in_line(State
, line_index
, x
,y
)
833 local line
= State
.lines
[line_index
]
834 local line_cache
= State
.line_cache
[line_index
]
835 local starty
= Text
.starty(State
, line_index
)
836 if starty
== nil then return false end -- outside current page
837 if y
< starty
then return false end
838 Text
.populate_screen_line_starting_pos(State
, line_index
)
839 return y
< starty
+ State
.line_height
*(#line_cache
.screen_line_starting_pos
- Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
) + 1)
842 -- convert mx,my in pixels to schema-1 coordinates
843 function Text
.to_pos_on_line(State
, line_index
, mx
, my
)
844 local line
= State
.lines
[line_index
]
845 local line_cache
= State
.line_cache
[line_index
]
846 local starty
= Text
.starty(State
, line_index
)
847 assert(my
>= starty
, 'failed to map y pixel to line')
848 -- duplicate some logic from Text.draw
850 local start_screen_line_index
= Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
)
851 for screen_line_index
= start_screen_line_index
,#line_cache
.screen_line_starting_pos
do
852 local screen_line_starting_pos
= line_cache
.screen_line_starting_pos
[screen_line_index
]
853 local screen_line_starting_byte_offset
= Text
.offset(line
.data
, screen_line_starting_pos
)
854 --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
855 local nexty
= y
+ State
.line_height
857 -- On all wrapped screen lines but the final one, clicks past end of
858 -- line position cursor on final character of screen line.
859 -- (The final screen line positions past end of screen line as always.)
860 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
861 --? print('past end of non-final line; return')
862 return line_cache
.screen_line_starting_pos
[screen_line_index
+1]
864 local s
= string.sub(line
.data
, screen_line_starting_byte_offset
)
865 --? print('return', mx, Text.nearest_cursor_pos(State.font, s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, mx, State.left) - 1)
866 return screen_line_starting_pos
+ Text
.nearest_cursor_pos(State
.font
, s
, mx
, State
.left
) - 1
870 assert(false, 'failed to map y pixel to line')
873 function Text
.screen_line_width(State
, line_index
, i
)
874 local line
= State
.lines
[line_index
]
875 local line_cache
= State
.line_cache
[line_index
]
876 local start_pos
= line_cache
.screen_line_starting_pos
[i
]
877 local start_offset
= Text
.offset(line
.data
, start_pos
)
879 if i
< #line_cache
.screen_line_starting_pos
then
880 local past_end_pos
= line_cache
.screen_line_starting_pos
[i
+1]
881 local past_end_offset
= Text
.offset(line
.data
, past_end_pos
)
882 screen_line
= string.sub(line
.data
, start_offset
, past_end_offset
-1)
884 screen_line
= string.sub(line
.data
, start_pos
)
886 return State
.font
:getWidth(screen_line
)
889 function Text
.screen_line_index(screen_line_starting_pos
, pos
)
890 for i
= #screen_line_starting_pos
,1,-1 do
891 if screen_line_starting_pos
[i
] <= pos
then
897 -- convert x pixel coordinate to pos
898 -- oblivious to wrapping
899 -- result: 1 to len+1
900 function Text
.nearest_cursor_pos(font
, line
, x
, left
)
904 local len
= utf8
.len(line
)
905 local max_x
= left
+Text
.x(font
, line
, len
+1)
909 local leftpos
, rightpos
= 1, len
+1
910 --? print('-- nearest', x)
912 --? print('nearest', x, '^'..line..'$', leftpos, rightpos)
913 if leftpos
== rightpos
then
916 local curr
= math
.floor((leftpos
+rightpos
)/2)
917 local currxmin
= left
+Text
.x(font
, line
, curr
)
918 local currxmax
= left
+Text
.x(font
, line
, curr
+1)
919 --? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
920 if currxmin
<= x
and x
< currxmax
then
921 if x
-currxmin
< currxmax
-x
then
927 if leftpos
>= rightpos
-1 then
936 assert(false, 'failed to map x pixel to pos')
939 -- return the nearest index of line (in utf8 code points) which lies entirely
940 -- within x pixels of the left margin
941 -- result: 0 to len+1
942 function Text
.nearest_pos_less_than(font
, line
, x
)
943 --? print('', '-- nearest_pos_less_than', line, x)
944 local len
= utf8
.len(line
)
945 local max_x
= Text
.x_after(font
, line
, len
)
949 local left
, right
= 0, len
+1
951 local curr
= math
.floor((left
+right
)/2)
952 local currxmin
= Text
.x_after(font
, line
, curr
+1)
953 local currxmax
= Text
.x_after(font
, line
, curr
+2)
954 --? print('', x, left, right, curr, currxmin, currxmax)
955 if currxmin
<= x
and x
< currxmax
then
958 if left
>= right
-1 then
967 assert(false, 'failed to map x pixel to pos')
970 function Text
.x_after(font
, s
, pos
)
971 local len
= utf8
.len(s
)
972 local offset
= Text
.offset(s
, math
.min(pos
+1, len
+1))
973 local s_before
= s
:sub(1, offset
-1)
974 --? print('^'..s_before..'$')
975 return font
:getWidth(s_before
)
978 function Text
.x(font
, s
, pos
)
979 local offset
= Text
.offset(s
, pos
)
980 local s_before
= s
:sub(1, offset
-1)
981 return font
:getWidth(s_before
)
984 function Text
.to2(State
, loc1
)
985 if State
.lines
[loc1
.line
].mode
== 'drawing' then
986 return {line
=loc1
.line
, screen_line
=1, screen_pos
=1}
988 local result
= {line
=loc1
.line
}
989 local line_cache
= State
.line_cache
[loc1
.line
]
990 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
991 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
992 local spos
= line_cache
.screen_line_starting_pos
[i
]
993 if spos
<= loc1
.pos
then
994 result
.screen_line
= i
995 result
.screen_pos
= loc1
.pos
- spos
+ 1
999 assert(result
.screen_pos
, 'failed to convert schema-1 coordinate to schema-2')
1003 function Text
.to1(State
, loc2
)
1004 local result
= {line
=loc2
.line
, pos
=loc2
.screen_pos
}
1005 if loc2
.screen_line
> 1 then
1006 result
.pos
= State
.line_cache
[loc2
.line
].screen_line_starting_pos
[loc2
.screen_line
] + loc2
.screen_pos
- 1
1011 function Text
.eq1(a
, b
)
1012 return a
.line
== b
.line
and a
.pos
== b
.pos
1015 function Text
.lt1(a
, b
)
1016 if a
.line
< b
.line
then
1019 if a
.line
> b
.line
then
1022 return a
.pos
< b
.pos
1025 function Text
.le1(a
, b
)
1026 if a
.line
< b
.line
then
1029 if a
.line
> b
.line
then
1032 return a
.pos
<= b
.pos
1035 function Text
.eq2(a
, b
)
1036 return a
.line
== b
.line
and a
.screen_line
== b
.screen_line
and a
.screen_pos
== b
.screen_pos
1039 function Text
.offset(s
, pos1
)
1040 if pos1
== 1 then return 1 end
1041 local result
= utf8
.offset(s
, pos1
)
1042 if result
== nil then
1043 assert(false, ('Text.offset(%d) called on a string of length %d (byte size %d); this is likely a failure to handle utf8\n\n^%s$\n'):format(pos1
, utf8
.len(s
), #s
, s
))
1048 function Text
.previous_screen_line(State
, loc2
)
1049 if loc2
.screen_line
> 1 then
1050 return {line
=loc2
.line
, screen_line
=loc2
.screen_line
-1, screen_pos
=1}
1051 elseif loc2
.line
== 1 then
1053 elseif State
.lines
[loc2
.line
-1].mode
== 'drawing' then
1054 return {line
=loc2
.line
-1, screen_line
=1, screen_pos
=1}
1056 local l
= State
.lines
[loc2
.line
-1]
1057 Text
.populate_screen_line_starting_pos(State
, loc2
.line
-1)
1058 return {line
=loc2
.line
-1, screen_line
=#State
.line_cache
[loc2
.line
-1].screen_line_starting_pos
, screen_pos
=1}
1062 function Text
.next_screen_line(State
, loc2
)
1063 if State
.lines
[loc2
.line
].mode
== 'drawing' then
1064 return {line
=loc2
.line
+1, screen_line
=1, screen_pos
=1}
1066 Text
.populate_screen_line_starting_pos(State
, loc2
.line
)
1067 if loc2
.screen_line
>= #State
.line_cache
[loc2
.line
].screen_line_starting_pos
then
1068 if loc2
.line
< #State
.lines
then
1069 return {line
=loc2
.line
+1, screen_line
=1, screen_pos
=1}
1074 return {line
=loc2
.line
, screen_line
=loc2
.screen_line
+1, screen_pos
=1}
1079 function Text
.tweak_screen_top_and_cursor(State
)
1080 if State
.screen_top1
.pos
== 1 then return end
1081 Text
.populate_screen_line_starting_pos(State
, State
.screen_top1
.line
)
1082 local line
= State
.lines
[State
.screen_top1
.line
]
1083 local line_cache
= State
.line_cache
[State
.screen_top1
.line
]
1084 for i
=2,#line_cache
.screen_line_starting_pos
do
1085 local pos
= line_cache
.screen_line_starting_pos
[i
]
1086 if pos
== State
.screen_top1
.pos
then
1089 if pos
> State
.screen_top1
.pos
then
1090 -- make sure screen top is at start of a screen line
1091 local prev
= line_cache
.screen_line_starting_pos
[i
-1]
1092 if State
.screen_top1
.pos
- prev
< pos
- State
.screen_top1
.pos
then
1093 State
.screen_top1
.pos
= prev
1095 State
.screen_top1
.pos
= pos
1100 -- make sure cursor is on screen
1101 local screen_bottom1
= Text
.screen_bottom1(State
)
1102 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
1103 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
1104 elseif State
.cursor1
.line
>= screen_bottom1
.line
then
1105 --? print('too low')
1106 if Text
.cursor_out_of_screen(State
) then
1109 line
=screen_bottom1
.line
,
1110 pos
=Text
.to_pos_on_line(State
, screen_bottom1
.line
, State
.right
-5, App
.screen
.height
-5),
1116 -- slightly expensive since it redraws the screen
1117 function Text
.cursor_out_of_screen(State
)
1119 return State
.cursor_y
== nil
1122 function Text
.redraw_all(State
)
1123 --? print('clearing fragments')
1124 -- Perform some early sanity checking here, in hopes that we correctly call
1125 -- this whenever we change editor state.
1126 if State
.right
<= State
.left
then
1127 assert(false, ('Right margin %d must be to the right of the left margin %d'):format(State
.right
, State
.left
))
1130 State
.line_cache
= {}
1131 for i
=1,#State
.lines
do
1132 State
.line_cache
[i
] = {}
1136 function Text
.clear_screen_line_cache(State
, line_index
)
1137 State
.line_cache
[line_index
].screen_line_starting_pos
= nil
1138 State
.line_cache
[line_index
].link_offsets
= nil
1142 return s
:gsub('^%s+', ''):gsub('%s+$', '')
1146 return s
:gsub('^%s+', '')
1150 return s
:gsub('%s+$', '')
1153 function starts_with(s
, prefix
)
1154 if #s
< #prefix
then
1158 if s
:sub(i
,i
) ~= prefix
:sub(i
,i
) then
1165 function ends_with(s
, suffix
)
1166 if #s
< #suffix
then
1169 for i
=0,#suffix
-1 do
1170 if s
:sub(#s
-i
,#s
-i
) ~= suffix
:sub(#suffix
-i
,#suffix
-i
) then