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
)
7 --? print('text.draw', line_index, y)
8 local line
= State
.lines
[line_index
]
9 local line_cache
= State
.line_cache
[line_index
]
10 line_cache
.startpos
= startpos
12 Text
.populate_screen_line_starting_pos(State
, line_index
)
13 assert(#line_cache
.screen_line_starting_pos
>= 1, 'line cache missing screen line info')
14 for i
=1,#line_cache
.screen_line_starting_pos
do
15 local pos
= line_cache
.screen_line_starting_pos
[i
]
16 if pos
< startpos
then
19 local screen_line
= Text
.screen_line(line
, line_cache
, i
)
20 --? print('text.draw:', screen_line, 'at', line_index,pos, 'after', x,y)
21 local frag_len
= utf8
.len(screen_line
)
22 -- render any highlights
23 if State
.selection1
.line
then
24 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
25 Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, lo
,hi
)
27 if line_index
== State
.cursor1
.line
then
28 -- render search highlight or cursor
29 if State
.search_term
then
30 local data
= State
.lines
[State
.cursor1
.line
].data
31 local cursor_offset
= Text
.offset(data
, State
.cursor1
.pos
)
32 if data
:sub(cursor_offset
, cursor_offset
+#State
.search_term
-1) == State
.search_term
then
33 local save_selection
= State
.selection1
34 State
.selection1
= {line
=line_index
, pos
=State
.cursor1
.pos
+utf8
.len(State
.search_term
)}
35 local lo
, hi
= Text
.clip_selection(State
, line_index
, pos
, pos
+frag_len
)
36 Text
.draw_highlight(State
, line
, State
.left
,y
, pos
, lo
,hi
)
37 State
.selection1
= save_selection
40 if pos
<= State
.cursor1
.pos
and pos
+ frag_len
> State
.cursor1
.pos
then
41 Text
.draw_cursor(State
, State
.left
+Text
.x(State
.font
, screen_line
, State
.cursor1
.pos
-pos
+1), y
)
42 elseif pos
+ frag_len
== State
.cursor1
.pos
then
43 -- Show cursor at end of line.
44 -- This place also catches end of wrapping screen lines. That doesn't seem worth distinguishing.
45 -- It seems useful to see a cursor whether your eye is on the left or right margin.
46 Text
.draw_cursor(State
, State
.left
+Text
.x(State
.font
, screen_line
, State
.cursor1
.pos
-pos
+1), y
)
52 App
.screen
.print(screen_line
, State
.left
,y
)
53 y
= y
+ State
.line_height
54 if y
>= App
.screen
.height
then
62 function Text
.screen_line(line
, line_cache
, i
)
63 local pos
= line_cache
.screen_line_starting_pos
[i
]
64 local offset
= Text
.offset(line
.data
, pos
)
65 if i
>= #line_cache
.screen_line_starting_pos
then
66 return line
.data
:sub(offset
)
68 local endpos
= line_cache
.screen_line_starting_pos
[i
+1]-1
69 local end_offset
= Text
.offset(line
.data
, endpos
)
70 return line
.data
:sub(offset
, end_offset
)
73 function Text
.draw_cursor(State
, x
, y
)
75 if math
.floor(Cursor_time
*2)%2 == 0 then
76 App
.color(Cursor_color
)
77 love
.graphics
.rectangle('fill', x
,y
, 3,State
.line_height
)
80 State
.cursor_y
= y
+State
.line_height
83 function Text
.populate_screen_line_starting_pos(State
, line_index
)
84 local line
= State
.lines
[line_index
]
85 if line
.mode
~= 'text' then return end
86 local line_cache
= State
.line_cache
[line_index
]
87 if line_cache
.screen_line_starting_pos
then
90 line_cache
.screen_line_starting_pos
= {1}
93 -- try to wrap at word boundaries
94 for frag
in line
.data
:gmatch('%S*%s*') do
95 local frag_width
= State
.font
:getWidth(frag
)
96 --? print('-- frag:', frag, pos, x, frag_width, State.width)
97 while x
+ frag_width
> State
.width
do
98 --? print('frag:', frag, pos, x, frag_width, State.width)
99 if x
< 0.8 * State
.width
then
100 -- long word; chop it at some letter
101 -- We're not going to reimplement TeX here.
102 local bpos
= Text
.nearest_pos_less_than(State
.font
, frag
, State
.width
- x
)
103 if x
== 0 and bpos
== 0 then
104 assert(false, ("Infinite loop while line-wrapping. Editor is %dpx wide; window is %dpx wide"):format(State
.width
, App
.screen
.width
))
107 local boffset
= Text
.offset(frag
, bpos
+1) -- byte _after_ bpos
108 frag
= string.sub(frag
, boffset
)
110 --? print('after chop:', frag)
112 frag_width
= State
.font
:getWidth(frag
)
114 --? print('screen line:', pos)
115 table.insert(line_cache
.screen_line_starting_pos
, pos
)
116 x
= 0 -- new screen line
119 pos
= pos
+ utf8
.len(frag
)
123 function Text
.text_input(State
, t
)
124 if App
.mouse_down(1) then return end
125 if App
.any_modifier_down() then
126 if App
.key_down(t
) then
127 -- The modifiers didn't change the key. Handle it in keychord_pressed.
130 -- Key mutated by the keyboard layout. Continue below.
133 local before
= snapshot(State
, State
.cursor1
.line
)
134 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
135 Text
.insert_at_cursor(State
, t
)
136 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
137 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
138 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
140 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
143 function Text
.insert_at_cursor(State
, t
)
144 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text', 'line is not text')
145 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
146 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
)
147 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
148 State
.cursor1
.pos
= State
.cursor1
.pos
+1
151 -- Don't handle any keys here that would trigger text_input above.
152 function Text
.keychord_press(State
, chord
)
153 --? print('chord', chord, State.selection1.line, State.selection1.pos)
154 --== shortcuts that mutate text
155 if chord
== 'return' then
156 local before_line
= State
.cursor1
.line
157 local before
= snapshot(State
, before_line
)
158 Text
.insert_return(State
)
159 State
.selection1
= {}
160 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
161 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
164 record_undo_event(State
, {before
=before
, after
=snapshot(State
, before_line
, State
.cursor1
.line
)})
165 elseif chord
== 'tab' then
166 local before
= snapshot(State
, State
.cursor1
.line
)
167 --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
168 Text
.insert_at_cursor(State
, '\t')
169 if State
.cursor_y
> App
.screen
.height
- State
.line_height
then
170 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
171 Text
.snap_cursor_to_bottom_of_screen(State
, State
.left
, State
.right
)
172 --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
175 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
176 elseif chord
== 'backspace' then
177 if State
.selection1
.line
then
178 Text
.delete_selection(State
, State
.left
, State
.right
)
183 if State
.cursor1
.pos
> 1 then
184 before
= snapshot(State
, State
.cursor1
.line
)
185 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1)
186 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
189 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
)
191 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
193 State
.cursor1
.pos
= State
.cursor1
.pos
-1
195 elseif State
.cursor1
.line
> 1 then
196 before
= snapshot(State
, State
.cursor1
.line
-1, State
.cursor1
.line
)
197 if State
.lines
[State
.cursor1
.line
-1].mode
== 'drawing' then
198 table.remove(State
.lines
, State
.cursor1
.line
-1)
199 table.remove(State
.line_cache
, State
.cursor1
.line
-1)
202 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
-1].data
)+1
203 State
.lines
[State
.cursor1
.line
-1].data
= State
.lines
[State
.cursor1
.line
-1].data
..State
.lines
[State
.cursor1
.line
].data
204 table.remove(State
.lines
, State
.cursor1
.line
)
205 table.remove(State
.line_cache
, State
.cursor1
.line
)
207 State
.cursor1
.line
= State
.cursor1
.line
-1
209 if State
.screen_top1
.line
> #State
.lines
then
210 Text
.populate_screen_line_starting_pos(State
, #State
.lines
)
211 local line_cache
= State
.line_cache
[#State
.line_cache
]
212 State
.screen_top1
= {line
=#State
.lines
, pos
=line_cache
.screen_line_starting_pos
[#line_cache
.screen_line_starting_pos
]}
213 elseif Text
.lt1(State
.cursor1
, State
.screen_top1
) then
214 State
.screen_top1
= {
215 line
=State
.cursor1
.line
,
216 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
218 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
220 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
221 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
))
223 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
224 elseif chord
== 'delete' then
225 if State
.selection1
.line
then
226 Text
.delete_selection(State
, State
.left
, State
.right
)
231 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
232 before
= snapshot(State
, State
.cursor1
.line
)
234 before
= snapshot(State
, State
.cursor1
.line
, State
.cursor1
.line
+1)
236 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
237 local byte_start
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
238 local byte_end
= utf8
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
+1)
241 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
)
243 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_start
-1)
245 -- no change to State.cursor1.pos
247 elseif State
.cursor1
.line
< #State
.lines
then
248 if State
.lines
[State
.cursor1
.line
+1].mode
== 'text' then
250 State
.lines
[State
.cursor1
.line
].data
= State
.lines
[State
.cursor1
.line
].data
..State
.lines
[State
.cursor1
.line
+1].data
252 table.remove(State
.lines
, State
.cursor1
.line
+1)
253 table.remove(State
.line_cache
, State
.cursor1
.line
+1)
255 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
257 record_undo_event(State
, {before
=before
, after
=snapshot(State
, State
.cursor1
.line
)})
258 --== shortcuts that move the cursor
259 elseif chord
== 'left' then
261 State
.selection1
= {}
262 elseif chord
== 'right' then
264 State
.selection1
= {}
265 elseif chord
== 'S-left' then
266 if State
.selection1
.line
== nil then
267 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
270 elseif chord
== 'S-right' then
271 if State
.selection1
.line
== nil then
272 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
275 -- C- hotkeys reserved for drawings, so we'll use M-
276 elseif chord
== 'M-left' then
277 Text
.word_left(State
)
278 State
.selection1
= {}
279 elseif chord
== 'M-right' then
280 Text
.word_right(State
)
281 State
.selection1
= {}
282 elseif chord
== 'M-S-left' then
283 if State
.selection1
.line
== nil then
284 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
286 Text
.word_left(State
)
287 elseif chord
== 'M-S-right' then
288 if State
.selection1
.line
== nil then
289 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
291 Text
.word_right(State
)
292 elseif chord
== 'home' then
293 Text
.start_of_line(State
)
294 State
.selection1
= {}
295 elseif chord
== 'end' then
296 Text
.end_of_line(State
)
297 State
.selection1
= {}
298 elseif chord
== 'S-home' then
299 if State
.selection1
.line
== nil then
300 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
302 Text
.start_of_line(State
)
303 elseif chord
== 'S-end' then
304 if State
.selection1
.line
== nil then
305 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
307 Text
.end_of_line(State
)
308 elseif chord
== 'up' then
310 State
.selection1
= {}
311 elseif chord
== 'down' then
313 State
.selection1
= {}
314 elseif chord
== 'S-up' then
315 if State
.selection1
.line
== nil then
316 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
319 elseif chord
== 'S-down' then
320 if State
.selection1
.line
== nil then
321 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
324 elseif chord
== 'pageup' then
326 State
.selection1
= {}
327 elseif chord
== 'pagedown' then
329 State
.selection1
= {}
330 elseif chord
== 'S-pageup' then
331 if State
.selection1
.line
== nil then
332 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
335 elseif chord
== 'S-pagedown' then
336 if State
.selection1
.line
== nil then
337 State
.selection1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
}
343 function Text
.insert_return(State
)
344 local byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
)
345 table.insert(State
.lines
, State
.cursor1
.line
+1, {mode
='text', data
=string.sub(State
.lines
[State
.cursor1
.line
].data
, byte_offset
)})
346 table.insert(State
.line_cache
, State
.cursor1
.line
+1, {})
347 State
.lines
[State
.cursor1
.line
].data
= string.sub(State
.lines
[State
.cursor1
.line
].data
, 1, byte_offset
-1)
348 Text
.clear_screen_line_cache(State
, State
.cursor1
.line
)
349 State
.cursor1
= {line
=State
.cursor1
.line
+1, pos
=1}
352 function Text
.pageup(State
)
353 State
.screen_top1
= Text
.previous_screen_top1(State
)
354 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
355 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
356 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
359 -- return the top y coordinate of a given line_index,
360 -- or nil if no part of it is on screen
361 function Text
.starty(State
, line_index
)
362 -- duplicate some logic from love.draw
363 -- does not modify State (except to populate line_cache)
364 if line_index
< State
.screen_top1
.line
then return end
365 local loc2
= Text
.to2(State
, State
.screen_top1
)
368 if State
.lines
[loc2
.line
].mode
== 'drawing' then
369 y
= y
+ Drawing_padding_top
371 if loc2
.line
== line_index
then return y
end
372 if State
.lines
[loc2
.line
].mode
== 'text' then
373 y
= y
+ State
.line_height
374 elseif State
.lines
[loc2
.line
].mode
== 'drawing' then
375 y
= y
+ Drawing
.pixels(State
.lines
[loc2
.line
].h
, State
.width
) + Drawing_padding_bottom
377 if y
+ State
.line_height
> App
.screen
.height
then break end
378 local next_loc2
= Text
.next_screen_line(State
, loc2
)
379 if Text
.eq2(next_loc2
, loc2
) then break end -- end of file
384 function Text
.previous_screen_top1(State
)
385 -- duplicate some logic from love.draw
386 -- does not modify State (except to populate line_cache)
387 local loc2
= Text
.to2(State
, State
.screen_top1
)
388 local y
= App
.screen
.height
- State
.line_height
389 while y
>= State
.top
do
390 if loc2
.line
== 1 and loc2
.screen_line
== 1 and loc2
.screen_pos
== 1 then break end
391 if State
.lines
[loc2
.line
].mode
== 'text' then
392 y
= y
- State
.line_height
393 elseif State
.lines
[loc2
.line
].mode
== 'drawing' then
394 y
= y
- Drawing_padding_height
- Drawing
.pixels(State
.lines
[loc2
.line
].h
, State
.width
)
396 loc2
= Text
.previous_screen_line(State
, loc2
)
398 return Text
.to1(State
, loc2
)
401 function Text
.pagedown(State
)
402 State
.screen_top1
= Text
.screen_bottom1(State
)
403 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
404 Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
405 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
408 -- return the location of the start of the bottom-most line on screen
409 function Text
.screen_bottom1(State
)
410 -- duplicate some logic from love.draw
411 -- does not modify State (except to populate line_cache)
412 local loc2
= Text
.to2(State
, State
.screen_top1
)
415 if State
.lines
[loc2
.line
].mode
== 'text' then
416 y
= y
+ State
.line_height
417 elseif State
.lines
[loc2
.line
].mode
== 'drawing' then
418 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[loc2
.line
].h
, State
.width
)
420 if y
+ State
.line_height
> App
.screen
.height
then break end
421 local next_loc2
= Text
.next_screen_line(State
, loc2
)
422 if Text
.eq2(next_loc2
, loc2
) then break end
425 return Text
.to1(State
, loc2
)
428 function Text
.up(State
)
429 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text', 'line is not text')
430 --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
431 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
432 if screen_line_starting_pos
== 1 then
433 --? print('cursor is at first screen line of its line')
434 -- line is done; skip to previous text line
435 local new_cursor_line
= State
.cursor1
.line
436 while new_cursor_line
> 1 do
437 new_cursor_line
= new_cursor_line
-1
438 if State
.lines
[new_cursor_line
].mode
== 'text' then
439 --? print('found previous text line')
440 State
.cursor1
= {line
=new_cursor_line
, pos
=nil}
441 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
442 -- previous text line found, pick its final screen line
443 --? print('has multiple screen lines')
444 local screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
445 --? print(#screen_line_starting_pos)
446 screen_line_starting_pos
= screen_line_starting_pos
[#screen_line_starting_pos
]
447 local screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_pos
)
448 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, screen_line_starting_byte_offset
)
449 State
.cursor1
.pos
= screen_line_starting_pos
+ Text
.nearest_cursor_pos(State
.font
, s
, State
.cursor_x
, State
.left
) - 1
454 -- move up one screen line in current line
455 assert(screen_line_index
> 1, 'bumped up against top screen line in line')
456 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
-1]
457 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
458 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
459 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(State
.font
, s
, State
.cursor_x
, State
.left
) - 1
460 --? print('cursor pos is now '..tostring(State.cursor1.pos))
462 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
463 State
.screen_top1
= {
464 line
=State
.cursor1
.line
,
465 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
467 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
471 function Text
.down(State
)
472 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text', 'line is not text')
473 --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
474 assert(State
.cursor1
.pos
, 'cursor has no pos')
475 if Text
.cursor_at_final_screen_line(State
) then
476 -- line is done, skip to next text line
477 --? print('cursor at final screen line of its line')
478 local new_cursor_line
= State
.cursor1
.line
479 while new_cursor_line
< #State
.lines
do
480 new_cursor_line
= new_cursor_line
+1
481 if State
.lines
[new_cursor_line
].mode
== 'text' then
483 line
= new_cursor_line
,
484 pos
= Text
.nearest_cursor_pos(State
.font
, State
.lines
[new_cursor_line
].data
, State
.cursor_x
, State
.left
),
486 --? print(State.cursor1.pos)
490 local screen_bottom1
= Text
.screen_bottom1(State
)
491 --? print('down 2', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, screen_bottom1.line, screen_bottom1.pos)
492 if State
.cursor1
.line
> screen_bottom1
.line
then
493 --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
494 --? print('scroll up preserving cursor')
495 Text
.snap_cursor_to_bottom_of_screen(State
)
496 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
499 -- move down one screen line in current line
500 local screen_bottom1
= Text
.screen_bottom1(State
)
501 local scroll_down
= Text
.le1(screen_bottom1
, State
.cursor1
)
502 --? print('cursor is NOT at final screen line of its line')
503 local screen_line_starting_pos
, screen_line_index
= Text
.pos_at_start_of_screen_line(State
, State
.cursor1
)
504 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
505 local new_screen_line_starting_pos
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
[screen_line_index
+1]
506 --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
507 local new_screen_line_starting_byte_offset
= Text
.offset(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_pos
)
508 local s
= string.sub(State
.lines
[State
.cursor1
.line
].data
, new_screen_line_starting_byte_offset
)
509 State
.cursor1
.pos
= new_screen_line_starting_pos
+ Text
.nearest_cursor_pos(State
.font
, s
, State
.cursor_x
, State
.left
) - 1
510 --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
512 --? print('scroll up preserving cursor')
513 Text
.snap_cursor_to_bottom_of_screen(State
)
514 --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
517 --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
520 function Text
.start_of_line(State
)
521 State
.cursor1
.pos
= 1
522 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
523 State
.screen_top1
= {line
=State
.cursor1
.line
, pos
=State
.cursor1
.pos
} -- copy
527 function Text
.end_of_line(State
)
528 State
.cursor1
.pos
= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) + 1
529 if Text
.cursor_out_of_screen(State
) then
530 Text
.snap_cursor_to_bottom_of_screen(State
)
534 function Text
.word_left(State
)
535 -- skip some whitespace
537 if State
.cursor1
.pos
== 1 then
540 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%S') then
545 -- skip some non-whitespace
548 if State
.cursor1
.pos
== 1 then
551 assert(State
.cursor1
.pos
> 1, 'bumped up against start of line')
552 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
-1, '%s') then
558 function Text
.word_right(State
)
559 -- skip some whitespace
561 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
564 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%S') then
567 Text
.right_without_scroll(State
)
570 Text
.right_without_scroll(State
)
571 if State
.cursor1
.pos
> utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
574 if Text
.match(State
.lines
[State
.cursor1
.line
].data
, State
.cursor1
.pos
, '%s') then
578 if Text
.cursor_out_of_screen(State
) then
579 Text
.snap_cursor_to_bottom_of_screen(State
)
583 function Text
.match(s
, pos
, pat
)
584 local start_offset
= Text
.offset(s
, pos
)
585 local end_offset
= Text
.offset(s
, pos
+1)
586 assert(end_offset
> start_offset
, ('end_offset %d not > start_offset %d'):format(end_offset
, start_offset
))
587 local curr
= s
:sub(start_offset
, end_offset
-1)
588 return curr
:match(pat
)
591 function Text
.left(State
)
592 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text', 'line is not text')
593 if State
.cursor1
.pos
> 1 then
594 State
.cursor1
.pos
= State
.cursor1
.pos
-1
596 local new_cursor_line
= State
.cursor1
.line
597 while new_cursor_line
> 1 do
598 new_cursor_line
= new_cursor_line
-1
599 if State
.lines
[new_cursor_line
].mode
== 'text' then
601 line
= new_cursor_line
,
602 pos
= utf8
.len(State
.lines
[new_cursor_line
].data
) + 1,
608 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
609 State
.screen_top1
= {
610 line
=State
.cursor1
.line
,
611 pos
=Text
.pos_at_start_of_screen_line(State
, State
.cursor1
),
613 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
617 function Text
.right(State
)
618 Text
.right_without_scroll(State
)
619 if Text
.cursor_out_of_screen(State
) then
620 Text
.snap_cursor_to_bottom_of_screen(State
)
624 function Text
.right_without_scroll(State
)
625 assert(State
.lines
[State
.cursor1
.line
].mode
== 'text', 'line is not text')
626 if State
.cursor1
.pos
<= utf8
.len(State
.lines
[State
.cursor1
.line
].data
) then
627 State
.cursor1
.pos
= State
.cursor1
.pos
+1
629 local new_cursor_line
= State
.cursor1
.line
630 while new_cursor_line
<= #State
.lines
-1 do
631 new_cursor_line
= new_cursor_line
+1
632 if State
.lines
[new_cursor_line
].mode
== 'text' then
633 State
.cursor1
= {line
=new_cursor_line
, pos
=1}
640 -- result: pos, index of screen line
641 function Text
.pos_at_start_of_screen_line(State
, loc1
)
642 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
643 local line_cache
= State
.line_cache
[loc1
.line
]
644 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
645 local spos
= line_cache
.screen_line_starting_pos
[i
]
646 if spos
<= loc1
.pos
then
650 assert(false, ('invalid pos %d'):format(loc1
.pos
))
653 function Text
.pos_at_end_of_screen_line(State
, loc1
)
654 assert(State
.lines
[loc1
.line
].mode
== 'text')
655 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
656 local line_cache
= State
.line_cache
[loc1
.line
]
657 local most_recent_final_pos
= utf8
.len(State
.lines
[loc1
.line
].data
)+1
658 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
659 local spos
= line_cache
.screen_line_starting_pos
[i
]
660 if spos
<= loc1
.pos
then
661 return most_recent_final_pos
663 most_recent_final_pos
= spos
-1
665 assert(false, ('invalid pos %d'):format(loc1
.pos
))
668 function Text
.final_text_loc_on_screen(State
)
669 local screen_bottom1
= Text
.screen_bottom1(State
)
670 if State
.lines
[screen_bottom1
.line
].mode
== 'text' then
672 line
=screen_bottom1
.line
,
673 pos
=Text
.pos_at_end_of_screen_line(State
, screen_bottom1
),
676 local loc2
= Text
.to2(State
, screen_bottom1
)
678 if State
.lines
[loc2
.line
].mode
== 'text' then break end
679 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
680 loc2
= Text
.previous_screen_line(State
, loc2
)
682 local result
= Text
.to1(State
, loc2
)
683 result
.pos
= Text
.pos_at_end_of_screen_line(State
, result
)
687 function Text
.cursor_at_final_screen_line(State
)
688 Text
.populate_screen_line_starting_pos(State
, State
.cursor1
.line
)
689 local screen_lines
= State
.line_cache
[State
.cursor1
.line
].screen_line_starting_pos
690 --? print(screen_lines[#screen_lines], State.cursor1.pos)
691 return screen_lines
[#screen_lines
] <= State
.cursor1
.pos
694 function Text
.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State
)
696 while State
.cursor1
.line
<= #State
.lines
do
697 if State
.lines
[State
.cursor1
.line
].mode
== 'text' then
700 --? print('cursor skips', State.cursor1.line)
701 y
= y
+ Drawing_padding_height
+ Drawing
.pixels(State
.lines
[State
.cursor1
.line
].h
, State
.width
)
702 State
.cursor1
.line
= State
.cursor1
.line
+ 1
704 if State
.cursor1
.pos
== nil then
705 State
.cursor1
.pos
= 1
707 -- hack: insert a text line at bottom of file if necessary
708 if State
.cursor1
.line
> #State
.lines
then
709 assert(State
.cursor1
.line
== #State
.lines
+1, 'tried to ensure bottom line of file is text, but failed')
710 table.insert(State
.lines
, {mode
='text', data
=''})
711 table.insert(State
.line_cache
, {})
713 --? print(y, App.screen.height, App.screen.height-State.line_height)
714 if y
> App
.screen
.height
- State
.line_height
then
715 --? print('scroll up')
716 Text
.snap_cursor_to_bottom_of_screen(State
)
720 -- should never modify State.cursor1
721 function Text
.snap_cursor_to_bottom_of_screen(State
)
722 --? print('to2:', State.cursor1.line, State.cursor1.pos)
723 local top2
= Text
.to2(State
, State
.cursor1
)
724 --? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
725 -- slide to start of screen line
726 top2
.screen_pos
= 1 -- start of screen line
727 --? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
728 --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
729 local y
= App
.screen
.height
- State
.line_height
730 -- duplicate some logic from love.draw
732 --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
733 if top2
.line
== 1 and top2
.screen_line
== 1 then break end
734 if top2
.screen_line
> 1 or State
.lines
[top2
.line
-1].mode
== 'text' then
735 local h
= State
.line_height
736 if y
- h
< State
.top
then
741 assert(top2
.line
> 1, 'tried to snap cursor to buttom of screen but failed')
742 assert(State
.lines
[top2
.line
-1].mode
== 'drawing', "expected a drawing but it's not")
743 -- We currently can't draw partial drawings, so either skip it entirely
745 local h
= Drawing_padding_height
+ Drawing
.pixels(State
.lines
[top2
.line
-1].h
, State
.width
)
746 if y
- h
< State
.top
then
749 --? print('skipping drawing of height', h)
752 top2
= Text
.previous_screen_line(State
, top2
)
754 --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
755 State
.screen_top1
= Text
.to1(State
, top2
)
756 --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
757 --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
758 Text
.redraw_all(State
) -- if we're scrolling, reclaim all fragments to avoid memory leaks
761 function Text
.in_line(State
, line_index
, x
,y
)
762 local line
= State
.lines
[line_index
]
763 local line_cache
= State
.line_cache
[line_index
]
764 local starty
= Text
.starty(State
, line_index
)
765 if starty
== nil then return false end -- outside current page
766 if y
< starty
then return false end
767 Text
.populate_screen_line_starting_pos(State
, line_index
)
768 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)
771 -- convert mx,my in pixels to schema-1 coordinates
772 function Text
.to_pos_on_line(State
, line_index
, mx
, my
)
773 local line
= State
.lines
[line_index
]
774 local line_cache
= State
.line_cache
[line_index
]
775 local starty
= Text
.starty(State
, line_index
)
776 assert(my
>= starty
, 'failed to map y pixel to line')
777 -- duplicate some logic from Text.draw
779 local start_screen_line_index
= Text
.screen_line_index(line_cache
.screen_line_starting_pos
, line_cache
.startpos
)
780 for screen_line_index
= start_screen_line_index
,#line_cache
.screen_line_starting_pos
do
781 local screen_line_starting_pos
= line_cache
.screen_line_starting_pos
[screen_line_index
]
782 local screen_line_starting_byte_offset
= Text
.offset(line
.data
, screen_line_starting_pos
)
783 --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
784 local nexty
= y
+ State
.line_height
786 -- On all wrapped screen lines but the final one, clicks past end of
787 -- line position cursor on final character of screen line.
788 -- (The final screen line positions past end of screen line as always.)
789 if screen_line_index
< #line_cache
.screen_line_starting_pos
and mx
> State
.left
+ Text
.screen_line_width(State
, line_index
, screen_line_index
) then
790 --? print('past end of non-final line; return')
791 return line_cache
.screen_line_starting_pos
[screen_line_index
+1]
793 local s
= string.sub(line
.data
, screen_line_starting_byte_offset
)
794 --? 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)
795 return screen_line_starting_pos
+ Text
.nearest_cursor_pos(State
.font
, s
, mx
, State
.left
) - 1
799 assert(false, 'failed to map y pixel to line')
802 function Text
.screen_line_width(State
, line_index
, i
)
803 local line
= State
.lines
[line_index
]
804 local line_cache
= State
.line_cache
[line_index
]
805 local start_pos
= line_cache
.screen_line_starting_pos
[i
]
806 local start_offset
= Text
.offset(line
.data
, start_pos
)
808 if i
< #line_cache
.screen_line_starting_pos
then
809 local past_end_pos
= line_cache
.screen_line_starting_pos
[i
+1]
810 local past_end_offset
= Text
.offset(line
.data
, past_end_pos
)
811 screen_line
= string.sub(line
.data
, start_offset
, past_end_offset
-1)
813 screen_line
= string.sub(line
.data
, start_pos
)
815 return State
.font
:getWidth(screen_line
)
818 function Text
.screen_line_index(screen_line_starting_pos
, pos
)
819 for i
= #screen_line_starting_pos
,1,-1 do
820 if screen_line_starting_pos
[i
] <= pos
then
826 -- convert x pixel coordinate to pos
827 -- oblivious to wrapping
828 -- result: 1 to len+1
829 function Text
.nearest_cursor_pos(font
, line
, x
, left
)
833 local len
= utf8
.len(line
)
834 local max_x
= left
+Text
.x(font
, line
, len
+1)
838 local leftpos
, rightpos
= 1, len
+1
839 --? print('-- nearest', x)
841 --? print('nearest', x, '^'..line..'$', leftpos, rightpos)
842 if leftpos
== rightpos
then
845 local curr
= math
.floor((leftpos
+rightpos
)/2)
846 local currxmin
= left
+Text
.x(font
, line
, curr
)
847 local currxmax
= left
+Text
.x(font
, line
, curr
+1)
848 --? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
849 if currxmin
<= x
and x
< currxmax
then
850 if x
-currxmin
< currxmax
-x
then
856 if leftpos
>= rightpos
-1 then
865 assert(false, 'failed to map x pixel to pos')
868 -- return the nearest index of line (in utf8 code points) which lies entirely
869 -- within x pixels of the left margin
870 -- result: 0 to len+1
871 function Text
.nearest_pos_less_than(font
, line
, x
)
872 --? print('', '-- nearest_pos_less_than', line, x)
873 local len
= utf8
.len(line
)
874 local max_x
= Text
.x_after(font
, line
, len
)
878 local left
, right
= 0, len
+1
880 local curr
= math
.floor((left
+right
)/2)
881 local currxmin
= Text
.x_after(font
, line
, curr
+1)
882 local currxmax
= Text
.x_after(font
, line
, curr
+2)
883 --? print('', x, left, right, curr, currxmin, currxmax)
884 if currxmin
<= x
and x
< currxmax
then
887 if left
>= right
-1 then
896 assert(false, 'failed to map x pixel to pos')
899 function Text
.x_after(font
, s
, pos
)
900 local len
= utf8
.len(s
)
901 local offset
= Text
.offset(s
, math
.min(pos
+1, len
+1))
902 local s_before
= s
:sub(1, offset
-1)
903 --? print('^'..s_before..'$')
904 return font
:getWidth(s_before
)
907 function Text
.x(font
, s
, pos
)
908 local offset
= Text
.offset(s
, pos
)
909 local s_before
= s
:sub(1, offset
-1)
910 return font
:getWidth(s_before
)
913 function Text
.to2(State
, loc1
)
914 if State
.lines
[loc1
.line
].mode
== 'drawing' then
915 return {line
=loc1
.line
, screen_line
=1, screen_pos
=1}
917 local result
= {line
=loc1
.line
}
918 local line_cache
= State
.line_cache
[loc1
.line
]
919 Text
.populate_screen_line_starting_pos(State
, loc1
.line
)
920 for i
=#line_cache
.screen_line_starting_pos
,1,-1 do
921 local spos
= line_cache
.screen_line_starting_pos
[i
]
922 if spos
<= loc1
.pos
then
923 result
.screen_line
= i
924 result
.screen_pos
= loc1
.pos
- spos
+ 1
928 assert(result
.screen_pos
, 'failed to convert schema-1 coordinate to schema-2')
932 function Text
.to1(State
, loc2
)
933 local result
= {line
=loc2
.line
, pos
=loc2
.screen_pos
}
934 if loc2
.screen_line
> 1 then
935 result
.pos
= State
.line_cache
[loc2
.line
].screen_line_starting_pos
[loc2
.screen_line
] + loc2
.screen_pos
- 1
940 function Text
.eq1(a
, b
)
941 return a
.line
== b
.line
and a
.pos
== b
.pos
944 function Text
.lt1(a
, b
)
945 if a
.line
< b
.line
then
948 if a
.line
> b
.line
then
954 function Text
.le1(a
, b
)
955 if a
.line
< b
.line
then
958 if a
.line
> b
.line
then
961 return a
.pos
<= b
.pos
964 function Text
.eq2(a
, b
)
965 return a
.line
== b
.line
and a
.screen_line
== b
.screen_line
and a
.screen_pos
== b
.screen_pos
968 function Text
.offset(s
, pos1
)
969 if pos1
== 1 then return 1 end
970 local result
= utf8
.offset(s
, pos1
)
971 if result
== nil then
972 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
))
977 function Text
.previous_screen_line(State
, loc2
)
978 if loc2
.screen_line
> 1 then
979 return {line
=loc2
.line
, screen_line
=loc2
.screen_line
-1, screen_pos
=1}
980 elseif loc2
.line
== 1 then
982 elseif State
.lines
[loc2
.line
-1].mode
== 'drawing' then
983 return {line
=loc2
.line
-1, screen_line
=1, screen_pos
=1}
985 local l
= State
.lines
[loc2
.line
-1]
986 Text
.populate_screen_line_starting_pos(State
, loc2
.line
-1)
987 return {line
=loc2
.line
-1, screen_line
=#State
.line_cache
[loc2
.line
-1].screen_line_starting_pos
, screen_pos
=1}
991 function Text
.next_screen_line(State
, loc2
)
992 if State
.lines
[loc2
.line
].mode
== 'drawing' then
993 return {line
=loc2
.line
+1, screen_line
=1, screen_pos
=1}
995 Text
.populate_screen_line_starting_pos(State
, loc2
.line
)
996 if loc2
.screen_line
>= #State
.line_cache
[loc2
.line
].screen_line_starting_pos
then
997 if loc2
.line
< #State
.lines
then
998 return {line
=loc2
.line
+1, screen_line
=1, screen_pos
=1}
1003 return {line
=loc2
.line
, screen_line
=loc2
.screen_line
+1, screen_pos
=1}
1008 function Text
.tweak_screen_top_and_cursor(State
)
1009 if State
.screen_top1
.pos
== 1 then return end
1010 Text
.populate_screen_line_starting_pos(State
, State
.screen_top1
.line
)
1011 local line
= State
.lines
[State
.screen_top1
.line
]
1012 local line_cache
= State
.line_cache
[State
.screen_top1
.line
]
1013 for i
=2,#line_cache
.screen_line_starting_pos
do
1014 local pos
= line_cache
.screen_line_starting_pos
[i
]
1015 if pos
== State
.screen_top1
.pos
then
1018 if pos
> State
.screen_top1
.pos
then
1019 -- make sure screen top is at start of a screen line
1020 local prev
= line_cache
.screen_line_starting_pos
[i
-1]
1021 if State
.screen_top1
.pos
- prev
< pos
- State
.screen_top1
.pos
then
1022 State
.screen_top1
.pos
= prev
1024 State
.screen_top1
.pos
= pos
1029 -- make sure cursor is on screen
1030 local screen_bottom1
= Text
.screen_bottom1(State
)
1031 if Text
.lt1(State
.cursor1
, State
.screen_top1
) then
1032 State
.cursor1
= {line
=State
.screen_top1
.line
, pos
=State
.screen_top1
.pos
}
1033 elseif State
.cursor1
.line
>= screen_bottom1
.line
then
1034 if Text
.cursor_out_of_screen(State
) then
1035 State
.cursor1
= Text
.final_text_loc_on_screen(State
)
1040 -- slightly expensive since it redraws the screen
1041 function Text
.cursor_out_of_screen(State
)
1043 return State
.cursor_y
== nil
1046 function Text
.redraw_all(State
)
1047 --? print('clearing fragments')
1048 -- Perform some early sanity checking here, in hopes that we correctly call
1049 -- this whenever we change editor state.
1050 if State
.right
<= State
.left
then
1051 assert(false, ('Right margin %d must be to the right of the left margin %d'):format(State
.right
, State
.left
))
1054 State
.line_cache
= {}
1055 for i
=1,#State
.lines
do
1056 State
.line_cache
[i
] = {}
1060 function Text
.clear_screen_line_cache(State
, line_index
)
1061 State
.line_cache
[line_index
].screen_line_starting_pos
= nil
1065 return s
:gsub('^%s+', ''):gsub('%s+$', '')
1069 return s
:gsub('^%s+', '')
1073 return s
:gsub('%s+$', '')
1076 function starts_with(s
, prefix
)
1077 if #s
< #prefix
then
1081 if s
:sub(i
,i
) ~= prefix
:sub(i
,i
) then
1088 function ends_with(s
, suffix
)
1089 if #s
< #suffix
then
1092 for i
=0,#suffix
-1 do
1093 if s
:sub(#s
-i
,#s
-i
) ~= suffix
:sub(#suffix
-i
,#suffix
-i
) then