lines2.love/text.lua

254 lines
10 KiB
Lua

-- text editor, particularly text drawing, horizontal wrap, vertical scrolling
Text = {}
function Text.draw_cursor(editor, x, y)
-- blink every 0.5s
if math.floor(Cursor_time*2)%2 == 0 then
App.color(Cursor_color)
love.graphics.rectangle('fill', x,y, 3,editor.line_height)
end
end
function Text.text_input(editor, t)
if love.mouse.isDown(1) then return end
if any_modifier_down() then
if Keys_down[t] then
-- The modifiers didn't change the key. Handle it in keychord_press.
return
else
-- Key mutated by the keyboard layout. Continue below.
end
end
local before = snapshot(editor, editor.cursor.line)
Text.insert_at_cursor(editor, t)
maybe_snap_cursor_to_bottom_of_screen(editor)
record_undo_event(editor, {before=before, after=snapshot(editor, editor.cursor.line)})
end
function Text.insert_at_cursor(editor, t)
assert(editor.lines[editor.cursor.line].mode == 'text', 'line is not text')
local byte_offset = Text.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos)
editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_offset-1)..t..string.sub(editor.lines[editor.cursor.line].data, byte_offset)
editor.cursor.pos = editor.cursor.pos+1
end
-- Don't handle any keys here that would trigger text_input above.
function Text.keychord_press(editor, chord)
assert(editor.lines[editor.cursor.line].mode == editor.cursor.mode)
--== shortcuts that mutate text (must schedule_save)
if chord == 'return' then
local before_line = editor.cursor.line
local before = snapshot(editor, before_line)
Text.insert_return(editor)
maybe_snap_cursor_to_bottom_of_screen(editor)
record_undo_event(editor, {before=before, after=snapshot(editor, before_line, editor.cursor.line)})
schedule_save(editor)
elseif chord == 'tab' then
if editor.cursor.mode == 'text' then
local before = snapshot(editor, editor.cursor.line)
Text.insert_at_cursor(editor, '\t')
maybe_snap_cursor_to_bottom_of_screen(editor)
record_undo_event(editor, {before=before, after=snapshot(editor, editor.cursor.line)})
schedule_save(editor)
end
elseif chord == 'backspace' then
if editor.cursor.mode == 'text' then
if editor.selection1.line then
Text.delete_selection_and_record_undo_event(editor)
schedule_save(editor)
return
end
local before
if editor.cursor.pos > 1 then
before = snapshot(editor, editor.cursor.line)
local byte_start = utf8.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos-1)
local byte_end = utf8.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos)
if byte_start then
if byte_end then
editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_start-1)..string.sub(editor.lines[editor.cursor.line].data, byte_end)
else
editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_start-1)
end
editor.cursor.pos = editor.cursor.pos-1
end
elseif editor.cursor.line > 1 then
before = snapshot(editor, editor.cursor.line-1, editor.cursor.line)
if editor.lines[editor.cursor.line-1].mode == 'drawing' then
table.remove(editor.lines, editor.cursor.line-1)
if editor.screen_top.line == editor.cursor.line-1 then
editor.screen_top = {mode='text', line=editor.screen_top.line, pos=1}
end
else
-- join lines
editor.cursor.pos = utf8.len(editor.lines[editor.cursor.line-1].data)+1
editor.lines[editor.cursor.line-1].data = editor.lines[editor.cursor.line-1].data..editor.lines[editor.cursor.line].data
table.remove(editor.lines, editor.cursor.line)
end
editor.cursor.line = editor.cursor.line-1
end
if editor.screen_top.line > #editor.lines then
editor.screen_top = vert(editor, editor.cursor, 0)
elseif edit.lt(editor.cursor, editor.screen_top) then
maybe_snap_cursor_to_top_of_screen(editor)
end
assert(edit.le(editor.screen_top, editor.cursor), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(editor.screen_top.line, editor.screen_top.pos or -1, editor.cursor.line, editor.cursor.pos or -1))
record_undo_event(editor, {before=before, after=snapshot(editor, editor.cursor.line)})
schedule_save(editor)
end
elseif chord == 'delete' then
if editor.cursor.mode == 'drawing' then
assert(editor.cursor.line < #editor.lines)
local before = snapshot(editor, editor.cursor.line, editor.cursor.line+1)
table.remove(editor.lines, editor.cursor.line)
assert(editor.cursor.line <= #editor.lines)
if editor.lines[editor.cursor.line].mode == 'text' then
editor.cursor = {mode='text', line=editor.cursor.line, pos=1}
else
editor.cursor = {mode='drawing', line=editor.cursor.line}
end
-- no need to scroll, but screen_top may have switched mode
if editor.screen_top.line == editor.cursor.line then
editor.screen_top = deepcopy(editor.cursor)
end
record_undo_event(editor, {before=before, after=snapshot(editor, editor.cursor.line)})
else
-- cursor in text line
if editor.selection1.line then
Text.delete_selection_and_record_undo_event(editor)
schedule_save(editor)
return
end
local before
if editor.cursor.pos <= utf8.len(editor.lines[editor.cursor.line].data) then
before = snapshot(editor, editor.cursor.line)
else
before = snapshot(editor, editor.cursor.line, editor.cursor.line+1)
end
if editor.cursor.pos <= utf8.len(editor.lines[editor.cursor.line].data) then
local byte_start = utf8.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos)
local byte_end = utf8.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos+1)
if byte_start then
if byte_end then
editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_start-1)..string.sub(editor.lines[editor.cursor.line].data, byte_end)
else
editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_start-1)
end
-- no change to editor.cursor.pos
end
elseif editor.cursor.line < #editor.lines then
if editor.lines[editor.cursor.line+1].mode == 'text' then
-- join lines
editor.lines[editor.cursor.line].data = editor.lines[editor.cursor.line].data..editor.lines[editor.cursor.line+1].data
end
table.remove(editor.lines, editor.cursor.line+1)
end
record_undo_event(editor, {before=before, after=snapshot(editor, editor.cursor.line)})
schedule_save(editor)
end
--== shortcuts that move the cursor
elseif chord == 'left' then
Text.left(editor)
editor.selection1 = {}
elseif chord == 'right' then
Text.right(editor)
editor.selection1 = {}
elseif chord == 'S-left' then
if editor.selection1.line == nil then
editor.selection1 = deepcopy(editor.cursor)
end
Text.left(editor)
elseif chord == 'S-right' then
if editor.selection1.line == nil then
editor.selection1 = deepcopy(editor.cursor)
end
Text.right(editor)
-- C- hotkeys reserved for drawings, so we'll use M-
elseif chord == 'M-left' then
Text.word_left(editor)
editor.selection1 = {}
elseif chord == 'M-right' then
Text.word_right(editor)
editor.selection1 = {}
elseif chord == 'M-S-left' then
if editor.selection1.line == nil then
editor.selection1 = deepcopy(editor.cursor)
end
Text.word_left(editor)
elseif chord == 'M-S-right' then
if editor.selection1.line == nil then
editor.selection1 = deepcopy(editor.cursor)
end
Text.word_right(editor)
elseif chord == 'home' then
Text.start_of_line(editor)
editor.selection1 = {}
elseif chord == 'end' then
Text.end_of_line(editor)
editor.selection1 = {}
elseif chord == 'S-home' then
if editor.selection1.line == nil then
editor.selection1 = deepcopy(editor.cursor)
end
Text.start_of_line(editor)
elseif chord == 'S-end' then
if editor.selection1.line == nil then
editor.selection1 = deepcopy(editor.cursor)
end
Text.end_of_line(editor)
elseif chord == 'up' then
Text.up(editor)
editor.selection1 = {}
elseif chord == 'down' then
Text.down(editor)
editor.selection1 = {}
elseif chord == 'S-up' then
if editor.selection1.line == nil then
editor.selection1 = deepcopy(editor.cursor)
end
Text.up(editor)
elseif chord == 'S-down' then
if editor.selection1.line == nil then
editor.selection1 = deepcopy(editor.cursor)
end
Text.down(editor)
elseif chord == 'pageup' then
Text.pageup(editor)
editor.selection1 = {}
elseif chord == 'pagedown' then
Text.pagedown(editor)
editor.selection1 = {}
elseif chord == 'S-pageup' then
if editor.selection1.line == nil then
editor.selection1 = deepcopy(editor.cursor)
end
Text.pageup(editor)
elseif chord == 'S-pagedown' then
if editor.selection1.line == nil then
editor.selection1 = deepcopy(editor.cursor)
end
Text.pagedown(editor)
end
end
function Text.insert_return(editor)
if editor.cursor.mode == 'drawing' then
table.insert(editor.lines, editor.cursor.line+1, {mode='text', data=''})
editor.cursor = {mode='text', line=editor.cursor.line+1, pos=1}
return
end
local byte_offset = Text.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos)
table.insert(editor.lines, editor.cursor.line+1, {mode='text', data=string.sub(editor.lines[editor.cursor.line].data, byte_offset)})
editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_offset-1)
editor.cursor = {mode='text', line=editor.cursor.line+1, pos=1}
maybe_snap_cursor_to_bottom_of_screen(editor)
end
function Text.offset(s, pos1)
if pos1 == 1 then return 1 end
local result = utf8.offset(s, pos1)
if result == nil then
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))
end
return result
end