Improvements to multicursor copy/paste (#1123)

* Add `Doc:get_selection_idx`

* Make multicursor paste add a cursor at the end of each paste

* Better manage paste of multicursor whole line copy

* Document `Doc:get_selection_idx`

* Keep track of last added selection in `Doc`

* Make use of `doc.last_selection` in `Doc` commands

* Make `Doc:get_selection` return the `Doc.last_selection` if possible
This commit is contained in:
Guldoman 2022-11-01 23:16:39 +01:00 committed by GitHub
parent b52fe1605e
commit 0f160e614e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 109 additions and 30 deletions

View File

@ -59,8 +59,9 @@ local function cut_or_copy(delete)
doc():delete_to_cursor(idx, 0) doc():delete_to_cursor(idx, 0)
end end
else -- Cut/copy whole line else -- Cut/copy whole line
text = doc().lines[line1] -- Remove newline from the text. It will be added as needed on paste.
full_text = full_text == "" and text or (full_text .. text) text = string.sub(doc().lines[line1], 1, -2)
full_text = full_text == "" and text or (full_text .. text .. "\n")
core.cursor_clipboard_whole_line[idx] = true core.cursor_clipboard_whole_line[idx] = true
if delete then if delete then
if line1 < #doc().lines then if line1 < #doc().lines then
@ -85,7 +86,15 @@ local function split_cursor(direction)
table.insert(new_cursors, { line1 + direction, col1 }) table.insert(new_cursors, { line1 + direction, col1 })
end end
end end
for i,v in ipairs(new_cursors) do doc():add_selection(v[1], v[2]) end -- add selections in the order that will leave the "last" added one as doc.last_selection
local start, stop = 1, #new_cursors
if direction < 0 then
start, stop = #new_cursors, 1
end
for i = start, stop, direction do
local v = new_cursors[i]
doc():add_selection(v[1], v[2])
end
core.blink_reset() core.blink_reset()
end end
@ -177,10 +186,30 @@ local function block_comment(comment, line1, col1, line2, col2)
end end
end end
local function insert_paste(doc, value, whole_line, idx)
if whole_line then
local line1, col1 = doc:get_selection_idx(idx)
doc:insert(line1, 1, value:gsub("\r", "").."\n")
-- Because we're inserting at the start of the line,
-- if the cursor is in the middle of the line
-- it gets carried to the next line along with the old text.
-- If it's at the start of the line it doesn't get carried,
-- so we move it of as many characters as we're adding.
if col1 == 1 then
doc:move_to_cursor(idx, #value+1)
end
else
doc:text_input(value:gsub("\r", ""), idx)
end
end
local commands = { local commands = {
["doc:select-none"] = function(dv) ["doc:select-none"] = function(dv)
local line, col = dv.doc:get_selection() local l1, c1 = dv.doc:get_selection_idx(dv.doc.last_selection)
dv.doc:set_selection(line, col) if not l1 then
l1, c1 = dv.doc:get_selection_idx(1)
end
dv.doc:set_selection(l1, c1)
end, end,
["doc:cut"] = function() ["doc:cut"] = function()
@ -202,27 +231,51 @@ local commands = {
["doc:paste"] = function(dv) ["doc:paste"] = function(dv)
local clipboard = system.get_clipboard() local clipboard = system.get_clipboard()
-- If the clipboard has changed since our last look, use that instead -- If the clipboard has changed since our last look, use that instead
local external_paste = core.cursor_clipboard["full"] ~= clipboard if core.cursor_clipboard["full"] ~= clipboard then
if external_paste then
core.cursor_clipboard = {} core.cursor_clipboard = {}
core.cursor_clipboard_whole_line = {} core.cursor_clipboard_whole_line = {}
end for idx in dv.doc:get_selections() do
local value, whole_line insert_paste(dv.doc, clipboard, false, idx)
for idx, line1, col1, line2, col2 in dv.doc:get_selections() do
if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then
value = core.cursor_clipboard[idx]
whole_line = core.cursor_clipboard_whole_line[idx] == true
else
value = clipboard
whole_line = not external_paste and clipboard:find("\n") ~= nil
end end
if whole_line then return
dv.doc:insert(line1, 1, value:gsub("\r", "")) end
if col1 == 1 then -- Use internal clipboard(s)
dv.doc:move_to_cursor(idx, #value) -- If there are mixed whole lines and normal lines, consider them all as normal
local only_whole_lines = true
for _,whole_line in pairs(core.cursor_clipboard_whole_line) do
if not whole_line then
only_whole_lines = false
break
end
end
if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then
-- If we have the same number of clipboards and selections,
-- paste each clipboard into its corresponding selection
for idx in dv.doc:get_selections() do
insert_paste(dv.doc, core.cursor_clipboard[idx], only_whole_lines, idx)
end
else
-- Paste every clipboard and add a selection at the end of each one
local new_selections = {}
for idx in dv.doc:get_selections() do
for cb_idx in ipairs(core.cursor_clipboard_whole_line) do
insert_paste(dv.doc, core.cursor_clipboard[cb_idx], only_whole_lines, idx)
if not only_whole_lines then
table.insert(new_selections, {dv.doc:get_selection_idx(idx)})
end
end
if only_whole_lines then
table.insert(new_selections, {dv.doc:get_selection_idx(idx)})
end
end
local first = true
for _,selection in pairs(new_selections) do
if first then
dv.doc:set_selection(table.unpack(selection))
first = false
else
dv.doc:add_selection(table.unpack(selection))
end end
else
dv.doc:text_input(value:gsub("\r", ""), idx)
end end
end end
end, end,

View File

@ -33,6 +33,7 @@ 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 }
self.last_selection = 1
self.undo_stack = { idx = 1 } self.undo_stack = { idx = 1 }
self.redo_stack = { idx = 1 } self.redo_stack = { idx = 1 }
self.clean_change_id = 1 self.clean_change_id = 1
@ -141,15 +142,39 @@ function Doc:get_change_id()
return self.undo_stack.idx return self.undo_stack.idx
end end
local function sort_positions(line1, col1, line2, col2)
if line1 > line2 or line1 == line2 and col1 > col2 then
return line2, col2, line1, col1, true
end
return line1, col1, line2, col2, false
end
-- Cursor section. Cursor indices are *only* valid during a get_selections() call. -- Cursor section. Cursor indices are *only* valid during a get_selections() call.
-- Cursors will always be iterated in order from top to bottom. Through normal operation -- Cursors will always be iterated in order from top to bottom. Through normal operation
-- curors can never swap positions; only merge or split, or change their position in cursor -- curors can never swap positions; only merge or split, or change their position in cursor
-- order. -- order.
function Doc:get_selection(sort) function Doc:get_selection(sort)
local idx, line1, col1, line2, col2, swap = self:get_selections(sort)({ self.selections, sort }, 0) local line1, col1, line2, col2, swap = self:get_selection_idx(self.last_selection, sort)
if not line1 then
line1, col1, line2, col2, swap = self:get_selection_idx(1, sort)
end
return line1, col1, line2, col2, swap return line1, col1, line2, col2, swap
end end
---Get the selection specified by `idx`
---@param idx integer @the index of the selection to retrieve
---@param sort? boolean @whether to sort the selection returned
---@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted
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]
if sort then
return sort_positions(line1, col1, line2, col2)
else
return line1, col1, line2, col2
end
end
function Doc:get_selection_text(limit) function Doc:get_selection_text(limit)
limit = limit or math.huge limit = limit or math.huge
local result = {} local result = {}
@ -181,13 +206,6 @@ function Doc:sanitize_selection()
end end
end end
local function sort_positions(line1, col1, line2, col2)
if line1 > line2 or line1 == line2 and col1 > col2 then
return line2, col2, line1, col1, true
end
return line1, col1, line2, col2, false
end
function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm) function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm)
assert(not line2 == not col2, "expected 3 or 5 arguments") assert(not line2 == not col2, "expected 3 or 5 arguments")
if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end
@ -206,10 +224,14 @@ function Doc:add_selection(line1, col1, line2, col2, swap)
end end
end end
self:set_selections(target, line1, col1, line2, col2, swap, 0) self:set_selections(target, line1, col1, line2, col2, swap, 0)
self.last_selection = target
end end
function Doc:remove_selection(idx) function Doc:remove_selection(idx)
if self.last_selection >= idx then
self.last_selection = self.last_selection - 1
end
common.splice(self.selections, (idx - 1) * 4 + 1, 4) common.splice(self.selections, (idx - 1) * 4 + 1, 4)
end end
@ -217,6 +239,7 @@ 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)
self.last_selection = 1
end end
function Doc:merge_cursors(idx) function Doc:merge_cursors(idx)
@ -225,6 +248,9 @@ function Doc:merge_cursors(idx)
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
self.last_selection = self.last_selection - 1
end
break break
end end
end end