Text overwriting (#1495)

* added text overwriting

* rewrote `DocView:draw_caret` to not use the order of draws

* forgot to delete some old code in `DocView:draw_overlay`
also added a temporary solution to overwriting
and added the missing arguments in `DocView:draw_ime_decoration`
and fixed `DocView:draw_caret`

* accidentally broke the `draw_caret` call in `draw_overlay` in the process

* multiline

* fixed calling `Doc:get_char` as a function
that, in turn, crashed the editor because "can't index a number"

* move and rename some stuff

* remove unneeded extra check

I just had to change the `~=` to `<` in the second condition

* overwrite disregards pasting text

* disregard overwrite on selections; doc only removes selection

* Fixed error where `doc` was used, instead of `self`.

---------

Co-authored-by: ThaCuber <70547062+ThaCuber@users.noreply.github.com>
Co-authored-by: Adam Harrison <adamdharrison@gmail.com>
This commit is contained in:
ThaCuber 2023-11-29 15:12:43 -04:00 committed by GitHub
parent e95094f0ca
commit 9615ead41b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 67 additions and 53 deletions

View File

@ -545,6 +545,11 @@ local commands = {
dv.doc.crlf = not dv.doc.crlf dv.doc.crlf = not dv.doc.crlf
end, end,
["doc:toggle-overwrite"] = function(dv)
dv.doc.overwrite = not dv.doc.overwrite
core.blink_reset() -- to show the cursor has changed edit modes
end,
["doc:save-as"] = function(dv) ["doc:save-as"] = function(dv)
local last_doc = core.last_active_view and core.last_active_view.doc local last_doc = core.last_active_view and core.last_active_view.doc
local text local text

View File

@ -1,5 +1,6 @@
local Object = require "core.object" local Object = require "core.object"
local Highlighter = require "core.doc.highlighter" local Highlighter = require "core.doc.highlighter"
local translate = require "core.doc.translate"
local core = require "core" local core = require "core"
local syntax = require "core.syntax" local syntax = require "core.syntax"
local config = require "core.config" local config = require "core.config"
@ -29,7 +30,6 @@ function Doc:new(filename, abs_filename, new_file)
end end
end end
function Doc:reset() function Doc:reset()
self.lines = { "\n" } self.lines = { "\n" }
self.selections = { 1, 1, 1, 1 } self.selections = { 1, 1, 1, 1 }
@ -38,10 +38,10 @@ function Doc:reset()
self.redo_stack = { idx = 1 } self.redo_stack = { idx = 1 }
self.clean_change_id = 1 self.clean_change_id = 1
self.highlighter = Highlighter(self) self.highlighter = Highlighter(self)
self.overwrite = false
self:reset_syntax() self:reset_syntax()
end end
function Doc:reset_syntax() function Doc:reset_syntax()
local header = self:get_text(1, 1, self:position_offset(1, 1, 128)) local header = self:get_text(1, 1, self:position_offset(1, 1, 128))
local path = self.abs_filename local path = self.abs_filename
@ -56,16 +56,14 @@ function Doc:reset_syntax()
end end
end end
function Doc:set_filename(filename, abs_filename) function Doc:set_filename(filename, abs_filename)
self.filename = filename self.filename = filename
self.abs_filename = abs_filename self.abs_filename = abs_filename
self:reset_syntax() self:reset_syntax()
end end
function Doc:load(filename) function Doc:load(filename)
local fp = assert( io.open(filename, "rb") ) local fp = assert(io.open(filename, "rb"))
self:reset() self:reset()
self.lines = {} self.lines = {}
local i = 1 local i = 1
@ -85,7 +83,6 @@ function Doc:load(filename)
self:reset_syntax() self:reset_syntax()
end end
function Doc:reload() function Doc:reload()
if self.filename then if self.filename then
local sel = { self:get_selection() } local sel = { self:get_selection() }
@ -95,7 +92,6 @@ function Doc:reload()
end end
end end
function Doc:save(filename, abs_filename) function Doc:save(filename, abs_filename)
if not filename then if not filename then
assert(self.filename, "no filename set to default to") assert(self.filename, "no filename set to default to")
@ -104,7 +100,7 @@ function Doc:save(filename, abs_filename)
else else
assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path") assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path")
end end
local fp = assert( io.open(filename, "wb") ) local fp = assert(io.open(filename, "wb"))
for _, line in ipairs(self.lines) do for _, line in ipairs(self.lines) do
if self.crlf then line = line:gsub("\n", "\r\n") end if self.crlf then line = line:gsub("\n", "\r\n") end
fp:write(line) fp:write(line)
@ -115,12 +111,10 @@ function Doc:save(filename, abs_filename)
self:clean() self:clean()
end end
function Doc:get_name() function Doc:get_name()
return self.filename or "unsaved" return self.filename or "unsaved"
end end
function Doc:is_dirty() function Doc:is_dirty()
if self.new_file then if self.new_file then
if self.filename then return true end if self.filename then return true end
@ -130,20 +124,17 @@ function Doc:is_dirty()
end end
end end
function Doc:clean() function Doc:clean()
self.clean_change_id = self:get_change_id() self.clean_change_id = self:get_change_id()
end end
function Doc:get_indent_info() function Doc:get_indent_info()
if not self.indent_info then return config.tab_type, config.indent_size, false end if not self.indent_info then return config.tab_type, config.indent_size, false end
return self.indent_info.type or config.tab_type, return self.indent_info.type or config.tab_type,
self.indent_info.size or config.indent_size, self.indent_info.size or config.indent_size,
self.indent_info.confirmed self.indent_info.confirmed
end end
function Doc:get_change_id() function Doc:get_change_id()
return self.undo_stack.idx return self.undo_stack.idx
end end
@ -167,13 +158,14 @@ function Doc:get_selection(sort)
return line1, col1, line2, col2, swap return line1, col1, line2, col2, swap
end end
---Get the selection specified by `idx` ---Get the selection specified by `idx`
---@param idx integer @the index of the selection to retrieve ---@param idx integer @the index of the selection to retrieve
---@param sort? boolean @whether to sort the selection returned ---@param sort? boolean @whether to sort the selection returned
---@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted ---@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted
function Doc:get_selection_idx(idx, sort) function Doc:get_selection_idx(idx, sort)
local line1, col1, line2, col2 = self.selections[idx*4-3], self.selections[idx*4-2], self.selections[idx*4-1], self.selections[idx*4] local line1, col1, line2, col2 = self.selections[idx * 4 - 3], self.selections[idx * 4 - 2],
self.selections[idx * 4 - 1],
self.selections[idx * 4]
if line1 and sort then if line1 and sort then
return sort_positions(line1, col1, line2, col2) return sort_positions(line1, col1, line2, col2)
else else
@ -217,7 +209,7 @@ function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm)
if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end
line1, col1 = self:sanitize_position(line1, col1) line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2 or line1, col2 or col1) line2, col2 = self:sanitize_position(line2 or line1, col2 or col1)
common.splice(self.selections, (idx - 1)*4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 }) common.splice(self.selections, (idx - 1) * 4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 })
end end
function Doc:add_selection(line1, col1, line2, col2, swap) function Doc:add_selection(line1, col1, line2, col2, swap)
@ -233,7 +225,6 @@ function Doc:add_selection(line1, col1, line2, col2, swap)
self.last_selection = target self.last_selection = target
end end
function Doc:remove_selection(idx) function Doc:remove_selection(idx)
if self.last_selection >= idx then if self.last_selection >= idx then
self.last_selection = self.last_selection - 1 self.last_selection = self.last_selection - 1
@ -241,7 +232,6 @@ function Doc:remove_selection(idx)
common.splice(self.selections, (idx - 1) * 4 + 1, 4) common.splice(self.selections, (idx - 1) * 4 + 1, 4)
end end
function Doc:set_selection(line1, col1, line2, col2, swap) function Doc:set_selection(line1, col1, line2, col2, swap)
self.selections = {} self.selections = {}
self:set_selections(1, line1, col1, line2, col2, swap) self:set_selections(1, line1, col1, line2, col2, swap)
@ -252,24 +242,24 @@ function Doc:merge_cursors(idx)
for i = (idx or (#self.selections - 3)), (idx or 5), -4 do for i = (idx or (#self.selections - 3)), (idx or 5), -4 do
for j = 1, i - 4, 4 do for j = 1, i - 4, 4 do
if self.selections[i] == self.selections[j] and if self.selections[i] == self.selections[j] and
self.selections[i+1] == self.selections[j+1] then self.selections[i + 1] == self.selections[j + 1] then
common.splice(self.selections, i, 4) common.splice(self.selections, i, 4)
if self.last_selection >= (i+3)/4 then if self.last_selection >= (i + 3) / 4 then
self.last_selection = self.last_selection - 1 self.last_selection = self.last_selection - 1
end end
break break
end end
end end
end end
end end
local function selection_iterator(invariant, idx) local function selection_iterator(invariant, idx)
local target = invariant[3] and (idx*4 - 7) or (idx*4 + 1) local target = invariant[3] and (idx * 4 - 7) or (idx * 4 + 1)
if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end
if invariant[2] then if invariant[2] then
return idx+(invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target+4)) return idx + (invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target + 4))
else else
return idx+(invariant[3] and -1 or 1), table.unpack(invariant[1], target, target+4) return idx + (invariant[3] and -1 or 1), table.unpack(invariant[1], target, target + 4)
end end
end end
@ -277,8 +267,9 @@ end
-- If a number, runs for exactly that iteration. -- If a number, runs for exactly that iteration.
function Doc:get_selections(sort_intra, idx_reverse) function Doc:get_selections(sort_intra, idx_reverse)
return selection_iterator, { self.selections, sort_intra, idx_reverse }, return selection_iterator, { self.selections, sort_intra, idx_reverse },
idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1)+1) idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1) + 1)
end end
-- End of cursor seciton. -- End of cursor seciton.
function Doc:sanitize_position(line, col) function Doc:sanitize_position(line, col)
@ -291,7 +282,6 @@ function Doc:sanitize_position(line, col)
return line, common.clamp(col, 1, #self.lines[line]) return line, common.clamp(col, 1, #self.lines[line])
end end
local function position_offset_func(self, line, col, fn, ...) local function position_offset_func(self, line, col, fn, ...)
line, col = self:sanitize_position(line, col) line, col = self:sanitize_position(line, col)
return fn(self, line, col, ...) return fn(self, line, col, ...)
@ -330,7 +320,6 @@ function Doc:position_offset(line, col, ...)
end end
end end
function Doc:get_text(line1, col1, line2, col2) function Doc:get_text(line1, col1, line2, col2)
line1, col1 = self:sanitize_position(line1, col1) line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2, col2) line2, col2 = self:sanitize_position(line2, col2)
@ -346,13 +335,11 @@ function Doc:get_text(line1, col1, line2, col2)
return table.concat(lines) return table.concat(lines)
end end
function Doc:get_char(line, col) function Doc:get_char(line, col)
line, col = self:sanitize_position(line, col) line, col = self:sanitize_position(line, col)
return self.lines[line]:sub(col, col) return self.lines[line]:sub(col, col)
end end
local function push_undo(undo_stack, time, type, ...) local function push_undo(undo_stack, time, type, ...)
undo_stack[undo_stack.idx] = { type = type, time = time, ... } undo_stack[undo_stack.idx] = { type = type, time = time, ... }
undo_stack[undo_stack.idx - config.max_undos] = nil undo_stack[undo_stack.idx - config.max_undos] = nil
@ -413,7 +400,8 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
if cline1 < line then break end if cline1 < line then break end
local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0
local column_addition = line == cline1 and ccol1 > col and len or 0 local column_addition = line == cline1 and ccol1 > col and len or 0
self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition) self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition,
ccol2 + column_addition)
end end
-- push undo -- push undo
@ -426,7 +414,6 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
self:sanitize_selection() self:sanitize_selection()
end end
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- push undo -- push undo
local text = self:get_text(line1, col1, line2, col2) local text = self:get_text(line1, col1, line2, col2)
@ -485,7 +472,6 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
self:sanitize_selection() self:sanitize_selection()
end end
function Doc:insert(line, col, text) function Doc:insert(line, col, text)
self.redo_stack = { idx = 1 } self.redo_stack = { idx = 1 }
-- Reset the clean id when we're pushing something new before it -- Reset the clean id when we're pushing something new before it
@ -497,7 +483,6 @@ function Doc:insert(line, col, text)
self:on_text_change("insert") self:on_text_change("insert")
end end
function Doc:remove(line1, col1, line2, col2) function Doc:remove(line1, col1, line2, col2)
self.redo_stack = { idx = 1 } self.redo_stack = { idx = 1 }
line1, col1 = self:sanitize_position(line1, col1) line1, col1 = self:sanitize_position(line1, col1)
@ -507,28 +492,34 @@ function Doc:remove(line1, col1, line2, col2)
self:on_text_change("remove") self:on_text_change("remove")
end end
function Doc:undo() function Doc:undo()
pop_undo(self, self.undo_stack, self.redo_stack, false) pop_undo(self, self.undo_stack, self.redo_stack, false)
end end
function Doc:redo() function Doc:redo()
pop_undo(self, self.redo_stack, self.undo_stack, false) pop_undo(self, self.redo_stack, self.undo_stack, false)
end end
function Doc:text_input(text, idx) function Doc:text_input(text, idx)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
local had_selection = false
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
self:delete_to_cursor(sidx) self:delete_to_cursor(sidx)
had_selection = true
end end
if self.overwrite
and not had_selection
and col1 < #self.lines[line1]
and text:ulen() == 1 then
self:remove(line1, col1, translate.next_char(self, line1, col1))
end
self:insert(line1, col1, text) self:insert(line1, col1, text)
self:move_to_cursor(sidx, #text) self:move_to_cursor(sidx, #text)
end end
end end
function Doc:ime_text_editing(text, start, length, idx) function Doc:ime_text_editing(text, start, length, idx)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
@ -539,7 +530,6 @@ function Doc:ime_text_editing(text, start, length, idx)
end end
end end
function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
local old_text = self:get_text(line1, col1, line2, col2) local old_text = self:get_text(line1, col1, line2, col2)
local new_text, res = fn(old_text) local new_text, res = fn(old_text)
@ -555,7 +545,7 @@ function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
end end
function Doc:replace(fn) function Doc:replace(fn)
local has_selection, results = false, { } local has_selection, results = false, {}
for idx, line1, col1, line2, col2 in self:get_selections(true) do for idx, line1, col1, line2, col2 in self:get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn) results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn)
@ -569,7 +559,6 @@ function Doc:replace(fn)
return results return results
end end
function Doc:delete_to_cursor(idx, ...) function Doc:delete_to_cursor(idx, ...)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
@ -583,6 +572,7 @@ function Doc:delete_to_cursor(idx, ...)
end end
self:merge_cursors(idx) self:merge_cursors(idx)
end end
function Doc:delete_to(...) return self:delete_to_cursor(nil, ...) end function Doc:delete_to(...) return self:delete_to_cursor(nil, ...) end
function Doc:move_to_cursor(idx, ...) function Doc:move_to_cursor(idx, ...)
@ -591,8 +581,8 @@ function Doc:move_to_cursor(idx, ...)
end end
self:merge_cursors(idx) self:merge_cursors(idx)
end end
function Doc:move_to(...) return self:move_to_cursor(nil, ...) end
function Doc:move_to(...) return self:move_to_cursor(nil, ...) end
function Doc:select_to_cursor(idx, ...) function Doc:select_to_cursor(idx, ...)
for sidx, line, col, line2, col2 in self:get_selections(false, idx) do for sidx, line, col, line2, col2 in self:get_selections(false, idx) do
@ -601,8 +591,8 @@ function Doc:select_to_cursor(idx, ...)
end end
self:merge_cursors(idx) self:merge_cursors(idx)
end end
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
function Doc:get_indent_string() function Doc:get_indent_string()
local indent_type, indent_size = self:get_indent_info() local indent_type, indent_size = self:get_indent_info()
@ -625,7 +615,7 @@ function Doc:get_line_indent(line, rnd_up)
local indent = e and line:sub(1, e):gsub("\t", soft_tab) or "" local indent = e and line:sub(1, e):gsub("\t", soft_tab) or ""
local number = #indent / #soft_tab local number = #indent / #soft_tab
return e, indent:sub(1, return e, indent:sub(1,
(rnd_up and math.ceil(number) or math.floor(number))*#soft_tab) (rnd_up and math.ceil(number) or math.floor(number)) * #soft_tab)
end end
end end
@ -674,5 +664,4 @@ function Doc:on_close()
core.log_quiet("Closed doc \"%s\"", self:get_name()) core.log_quiet("Closed doc \"%s\"", self:get_name())
end end
return Doc return Doc

View File

@ -460,9 +460,14 @@ function DocView:draw_line_text(line, x, y)
return self:get_line_height() return self:get_line_height()
end end
function DocView:draw_caret(x, y) function DocView:draw_caret(x, y, line, col)
local lh = self:get_line_height() local lh = self:get_line_height()
if self.doc.overwrite then
local w = self:get_font():get_width(self.doc:get_char(line, col))
renderer.draw_rect(x, y + lh, w, style.caret_width * 2, style.caret)
else
renderer.draw_rect(x, y, style.caret_width, lh, style.caret) renderer.draw_rect(x, y, style.caret_width, lh, style.caret)
end
end end
function DocView:draw_line_body(line, x, y) function DocView:draw_line_body(line, x, y)
@ -542,7 +547,7 @@ function DocView:draw_ime_decoration(line1, col1, line2, col2)
line_size = style.caret_width line_size = style.caret_width
renderer.draw_rect(x + math.min(x1, x2), y + lh - line_size, math.abs(x1 - x2), line_size, style.caret) renderer.draw_rect(x + math.min(x1, x2), y + lh - line_size, math.abs(x1 - x2), line_size, style.caret)
end end
self:draw_caret(x + x1, y) self:draw_caret(x + x1, y, line1, col)
end end
@ -559,7 +564,8 @@ function DocView:draw_overlay()
else else
if config.disable_blink if config.disable_blink
or (core.blink_timer - core.blink_start) % T < T / 2 then or (core.blink_timer - core.blink_start) % T < T / 2 then
self:draw_caret(self:get_line_screen_position(line1, col1)) local x, y = self:get_line_screen_position(line1, col1)
self:draw_caret(x, y, line1, col1)
end end
end end
end end

View File

@ -341,6 +341,7 @@ keymap.add_direct {
["ctrl+x"] = "doc:cut", ["ctrl+x"] = "doc:cut",
["ctrl+c"] = "doc:copy", ["ctrl+c"] = "doc:copy",
["ctrl+v"] = "doc:paste", ["ctrl+v"] = "doc:paste",
["insert"] = "doc:toggle-overwrite",
["ctrl+insert"] = "doc:copy", ["ctrl+insert"] = "doc:copy",
["shift+insert"] = "doc:paste", ["shift+insert"] = "doc:paste",
["escape"] = { "command:escape", "doc:select-none", "dialog:select-no" }, ["escape"] = { "command:escape", "doc:select-none", "dialog:select-no" },

View File

@ -319,6 +319,19 @@ function StatusView:register_docview_items()
end, end,
command = "doc:toggle-line-ending" command = "doc:toggle-line-ending"
}) })
self:add_item {
predicate = predicate_docview,
name = "doc:overwrite-mode",
alignment = StatusView.Item.RIGHT,
get_item = function()
return {
style.text, core.active_view.doc.overwrite and "OVR" or "INS"
}
end,
command = "doc:toggle-overwrite",
separator = StatusView.separator2
}
end end