diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 3ddb7f16..0c94ed46 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -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, diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 12fb2dde..e3ea4b92 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -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