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)
|
||||
end
|
||||
else -- Cut/copy whole line
|
||||
text = doc().lines[line1]
|
||||
full_text = full_text == "" and text or (full_text .. text)
|
||||
-- Remove newline from the text. It will be added as needed on paste.
|
||||
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
|
||||
if delete then
|
||||
if line1 < #doc().lines then
|
||||
|
@ -85,7 +86,15 @@ local function split_cursor(direction)
|
|||
table.insert(new_cursors, { line1 + direction, col1 })
|
||||
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()
|
||||
end
|
||||
|
||||
|
@ -177,10 +186,30 @@ local function block_comment(comment, line1, col1, line2, col2)
|
|||
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 = {
|
||||
["doc:select-none"] = function(dv)
|
||||
local line, col = dv.doc:get_selection()
|
||||
dv.doc:set_selection(line, col)
|
||||
local l1, c1 = dv.doc:get_selection_idx(dv.doc.last_selection)
|
||||
if not l1 then
|
||||
l1, c1 = dv.doc:get_selection_idx(1)
|
||||
end
|
||||
dv.doc:set_selection(l1, c1)
|
||||
end,
|
||||
|
||||
["doc:cut"] = function()
|
||||
|
@ -202,27 +231,51 @@ local commands = {
|
|||
["doc:paste"] = function(dv)
|
||||
local clipboard = system.get_clipboard()
|
||||
-- If the clipboard has changed since our last look, use that instead
|
||||
local external_paste = core.cursor_clipboard["full"] ~= clipboard
|
||||
if external_paste then
|
||||
if core.cursor_clipboard["full"] ~= clipboard then
|
||||
core.cursor_clipboard = {}
|
||||
core.cursor_clipboard_whole_line = {}
|
||||
end
|
||||
local value, whole_line
|
||||
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
|
||||
for idx in dv.doc:get_selections() do
|
||||
insert_paste(dv.doc, clipboard, false, idx)
|
||||
end
|
||||
if whole_line then
|
||||
dv.doc:insert(line1, 1, value:gsub("\r", ""))
|
||||
if col1 == 1 then
|
||||
dv.doc:move_to_cursor(idx, #value)
|
||||
return
|
||||
end
|
||||
-- Use internal clipboard(s)
|
||||
-- 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
|
||||
else
|
||||
dv.doc:text_input(value:gsub("\r", ""), idx)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
|
|
@ -33,6 +33,7 @@ end
|
|||
function Doc:reset()
|
||||
self.lines = { "\n" }
|
||||
self.selections = { 1, 1, 1, 1 }
|
||||
self.last_selection = 1
|
||||
self.undo_stack = { idx = 1 }
|
||||
self.redo_stack = { idx = 1 }
|
||||
self.clean_change_id = 1
|
||||
|
@ -141,15 +142,39 @@ function Doc:get_change_id()
|
|||
return self.undo_stack.idx
|
||||
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.
|
||||
-- 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
|
||||
-- order.
|
||||
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
|
||||
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)
|
||||
limit = limit or math.huge
|
||||
local result = {}
|
||||
|
@ -181,13 +206,6 @@ function Doc:sanitize_selection()
|
|||
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)
|
||||
assert(not line2 == not col2, "expected 3 or 5 arguments")
|
||||
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
|
||||
self:set_selections(target, line1, col1, line2, col2, swap, 0)
|
||||
self.last_selection = target
|
||||
end
|
||||
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
|
@ -217,6 +239,7 @@ end
|
|||
function Doc:set_selection(line1, col1, line2, col2, swap)
|
||||
self.selections = {}
|
||||
self:set_selections(1, line1, col1, line2, col2, swap)
|
||||
self.last_selection = 1
|
||||
end
|
||||
|
||||
function Doc:merge_cursors(idx)
|
||||
|
@ -225,6 +248,9 @@ function Doc:merge_cursors(idx)
|
|||
if self.selections[i] == self.selections[j] and
|
||||
self.selections[i+1] == self.selections[j+1] then
|
||||
common.splice(self.selections, i, 4)
|
||||
if self.last_selection >= (i+3)/4 then
|
||||
self.last_selection = self.last_selection - 1
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue