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:
parent
b52fe1605e
commit
0f160e614e
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue