From ce62b9c8dacd10d59e6eaf38fd72a46701d81192 Mon Sep 17 00:00:00 2001 From: George Sokianos Date: Fri, 7 Jan 2022 11:00:50 +0000 Subject: [PATCH] Applied PR255 changes --- data/core/commands/doc.lua | 349 ++++++++++++++++++++----------------- data/core/common.lua | 20 +++ data/core/doc/init.lua | 250 +++++++++++++++++--------- data/core/docview.lua | 72 +++++--- data/core/keymap-macos.lua | 2 + data/core/keymap.lua | 2 + 6 files changed, 426 insertions(+), 269 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 8165b426..25dd3fa2 100755 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -24,13 +24,16 @@ local function get_indent_string() end -local function doc_multiline_selection(sort) - local line1, col1, line2, col2, swap = doc():get_selection(sort) - if line2 > line1 and col2 == 1 then - line2 = line2 - 1 - col2 = #doc().lines[line2] +local function doc_multiline_selections(sort) + local iter, state, idx, line1, col1, line2, col2 = doc():get_selections(sort) + return function() + idx, line1, col1, line2, col2 = iter(state, idx) + if idx and line2 > line1 and col2 == 1 then + line2 = line2 - 1 + col2 = #doc().lines[line2] + end + return idx, line1, col1, line2, col2 end - return line1, col1, line2, col2, swap end local function append_line_if_last_line(line) @@ -39,7 +42,6 @@ local function append_line_if_last_line(line) end end - local function save(filename) doc():save(filename and core.normalize_to_project_dir(filename)) local saved_filename = doc().filename @@ -47,55 +49,31 @@ local function save(filename) core.log("Saved \"%s\"", saved_filename) end --- returns the size of the original indent, and the indent --- in your config format, rounded either up or down -local function get_line_indent(line, rnd_up) - local _, e = line:find("^[ \t]+") - local soft_tab = string.rep(" ", config.indent_size) - if config.tab_type == "hard" then - local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or "" - return e, indent:gsub(" +", rnd_up and "\t" or "") - else - local indent = e and line:sub(1, e):gsub("\t", soft_tab) or "" - local number = #indent / #soft_tab - return e, indent:sub(1, - (rnd_up and math.ceil(number) or math.floor(number))*#soft_tab) +local function cut_or_copy(delete) + local full_text = "" + for idx, line1, col1, line2, col2 in doc():get_selections() do + if line1 ~= line2 or col1 ~= col2 then + local text = doc():get_text(line1, col1, line2, col2) + if delete then + doc():delete_to_cursor(idx, 0) + end + full_text = full_text == "" and text or (full_text .. "\n" .. text) + doc().cursor_clipboard[idx] = text + else + doc().cursor_clipboard[idx] = "" + end end + system.set_clipboard(full_text) end --- un/indents text; behaviour varies based on selection and un/indent. --- * if there's a selection, it will stay static around the --- text for both indenting and unindenting. --- * if you are in the beginning whitespace of a line, and are indenting, the --- cursor will insert the exactly appropriate amount of spaces, and jump the --- cursor to the beginning of first non whitespace characters --- * if you are not in the beginning whitespace of a line, and you indent, it --- inserts the appropriate whitespace, as if you typed them normally. --- * if you are unindenting, the cursor will jump to the start of the line, --- and remove the appropriate amount of spaces (or a tab). -local function indent_text(unindent) - local text = get_indent_string() - local line1, col1, line2, col2, swap = doc_multiline_selection(true) - local _, se = doc().lines[line1]:find("^[ \t]+") - local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) - if unindent or doc():has_selection() or in_beginning_whitespace then - local l1d, l2d = #doc().lines[line1], #doc().lines[line2] - for line = line1, line2 do - local e, rnded = get_line_indent(doc().lines[line], unindent) - doc():remove(line, 1, line, (e or 0) + 1) - doc():insert(line, 1, - unindent and rnded:sub(1, #rnded - #text) or rnded .. text) +local function split_cursor(direction) + local new_cursors = {} + for _, line1, col1 in doc():get_selections() do + if line1 > 1 and line1 < #doc().lines then + table.insert(new_cursors, { line1 + direction, col1 }) end - l1d, l2d = #doc().lines[line1] - l1d, #doc().lines[line2] - l2d - if (unindent or in_beginning_whitespace) and not doc():has_selection() then - local start_cursor = (se and se + 1 or 1) + l1d or #(doc().lines[line1]) - doc():set_selection(line1, start_cursor, line2, start_cursor, swap) - else - doc():set_selection(line1, col1 + l1d, line2, col2 + l2d, swap) - end - else - doc():text_input(text) end + for i,v in ipairs(new_cursors) do doc():add_selection(v[1], v[2]) end end local commands = { @@ -108,65 +86,66 @@ local commands = { end, ["doc:cut"] = function() - if doc():has_selection() then - local text = doc():get_text(doc():get_selection()) - system.set_clipboard(text) - doc():delete_to(0) - end + cut_or_copy(true) end, ["doc:copy"] = function() - if doc():has_selection() then - local text = doc():get_text(doc():get_selection()) - system.set_clipboard(text) - end + cut_or_copy(false) end, ["doc:paste"] = function() - doc():text_input(system.get_clipboard():gsub("\r", "")) + for idx, line1, col1, line2, col2 in doc():get_selections() do + local value = doc().cursor_clipboard[idx] or system.get_clipboard() + doc():text_input(value:gsub("\r", ""), idx) + end end, ["doc:newline"] = function() - local line, col = doc():get_selection() - local indent = doc().lines[line]:match("^[\t ]*") - if col <= #indent then - indent = indent:sub(#indent + 2 - col) + for idx, line, col in doc():get_selections(false, true) do + local indent = doc().lines[line]:match("^[\t ]*") + if col <= #indent then + indent = indent:sub(#indent + 2 - col) + end + doc():text_input("\n" .. indent, idx) end - doc():text_input("\n" .. indent) end, ["doc:newline-below"] = function() - local line = doc():get_selection() - local indent = doc().lines[line]:match("^[\t ]*") - doc():insert(line, math.huge, "\n" .. indent) - doc():set_selection(line + 1, math.huge) + for idx, line in doc():get_selections(false, true) do + local indent = doc().lines[line]:match("^[\t ]*") + doc():insert(line, math.huge, "\n" .. indent) + doc():set_selections(idx, line + 1, math.huge) + end end, ["doc:newline-above"] = function() - local line = doc():get_selection() - local indent = doc().lines[line]:match("^[\t ]*") - doc():insert(line, 1, indent .. "\n") - doc():set_selection(line, math.huge) + for idx, line in doc():get_selections(false, true) do + local indent = doc().lines[line]:match("^[\t ]*") + doc():insert(line, 1, indent .. "\n") + doc():set_selections(idx, line, math.huge) + end end, ["doc:delete"] = function() - local line, col = doc():get_selection() - if not doc():has_selection() and doc().lines[line]:find("^%s*$", col) then - doc():remove(line, col, line, math.huge) + for idx, line1, col1, line2, col2 in doc():get_selections() do + if line1 == line2 and col1 == col2 and doc().lines[line1]:find("^%s*$", col1) then + doc():remove(line1, col1, line1, math.huge) + end + doc():delete_to_cursor(idx, translate.next_char) end - doc():delete_to(translate.next_char) end, ["doc:backspace"] = function() - local line, col = doc():get_selection() - if not doc():has_selection() then - local text = doc():get_text(line, 1, line, col) - if #text >= config.indent_size and text:find("^ *$") then - doc():delete_to(0, -config.indent_size) - return + for idx, line1, col1, line2, col2 in doc():get_selections() do + if line1 == line2 and col1 == col2 then + local text = doc():get_text(line1, 1, line1, col1) + if #text >= config.indent_size and text:find("^ *$") then + doc():delete_to_cursor(idx, 0, -config.indent_size) + return + end end + doc():delete_to_cursor(idx, translate.previous_char) end - doc():delete_to(translate.previous_char) end, ["doc:select-all"] = function() @@ -177,77 +156,104 @@ local commands = { local line, col = doc():get_selection() doc():set_selection(line, col) end, + + + ["doc:indent"] = function() + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + local l1, c1, l2, c2 = doc():indent_text(false, line1, col1, line2, col2) + if l1 then + doc():set_selections(idx, l1, c1, l2, c2) + end + end + end, ["doc:select-lines"] = function() - local line1, _, line2, _, swap = doc():get_selection(true) - append_line_if_last_line(line2) - doc():set_selection(line1, 1, line2 + 1, 1, swap) + for idx, line1, _, line2 in doc():get_selections(true) do + append_line_if_last_line(line2) + doc():set_selections(idx, line1, 1, line2 + 1, 1, swap) + end end, ["doc:select-word"] = function() - local line1, col1 = doc():get_selection(true) - local line1, col1 = translate.start_of_word(doc(), line1, col1) - local line2, col2 = translate.end_of_word(doc(), line1, col1) - doc():set_selection(line2, col2, line1, col1) + for idx, line1, col1 in doc():get_selections(true) do + local line1, col1 = translate.start_of_word(doc(), line1, col1) + local line2, col2 = translate.end_of_word(doc(), line1, col1) + doc():set_selections(idx, line2, col2, line1, col1) + end end, ["doc:join-lines"] = function() - local line1, _, line2 = doc():get_selection(true) - if line1 == line2 then line2 = line2 + 1 end - local text = doc():get_text(line1, 1, line2, math.huge) - text = text:gsub("(.-)\n[\t ]*", function(x) - return x:find("^%s*$") and x or x .. " " - end) - doc():insert(line1, 1, text) - doc():remove(line1, #text + 1, line2, math.huge) - if doc():has_selection() then - doc():set_selection(line1, math.huge) + for idx, line1, col1, line2, col2 in doc():get_selections(true) do + if line1 == line2 then line2 = line2 + 1 end + local text = doc():get_text(line1, 1, line2, math.huge) + text = text:gsub("(.-)\n[\t ]*", function(x) + return x:find("^%s*$") and x or x .. " " + end) + doc():insert(line1, 1, text) + doc():remove(line1, #text + 1, line2, math.huge) + if line1 ~= line2 or col1 ~= col2 then + doc():set_selections(idx, line1, math.huge) + end end end, ["doc:indent"] = function() - indent_text() + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + local l1, c1, l2, c2 = doc():indent_text(false, line1, col1, line2, col2) + if l1 then + doc():set_selections(idx, l1, c1, l2, c2) + end + end end, ["doc:unindent"] = function() - indent_text(true) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + local l1, c1, l2, c2 = doc():indent_text(true, line1, col1, line2, col2) + if l1 then + doc():set_selections(idx, l1, c1, l2, c2) + end + end end, ["doc:duplicate-lines"] = function() - local line1, col1, line2, col2, swap = doc_multiline_selection(true) - append_line_if_last_line(line2) - local text = doc():get_text(line1, 1, line2 + 1, 1) - doc():insert(line2 + 1, 1, text) - local n = line2 - line1 + 1 - doc():set_selection(line1 + n, col1, line2 + n, col2, swap) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + append_line_if_last_line(line2) + local text = doc():get_text(line1, 1, line2 + 1, 1) + doc():insert(line2 + 1, 1, text) + local n = line2 - line1 + 1 + doc():set_selections(idx, line1 + n, col1, line2 + n, col2, swap) + end end, ["doc:delete-lines"] = function() - local line1, col1, line2 = doc_multiline_selection(true) - append_line_if_last_line(line2) - doc():remove(line1, 1, line2 + 1, 1) - doc():set_selection(line1, col1) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + append_line_if_last_line(line2) + doc():remove(line1, 1, line2 + 1, 1) + doc():set_selections(idx, line1, col1) + end end, ["doc:move-lines-up"] = function() - local line1, col1, line2, col2, swap = doc_multiline_selection(true) - append_line_if_last_line(line2) - if line1 > 1 then - local text = doc().lines[line1 - 1] - doc():insert(line2 + 1, 1, text) - doc():remove(line1 - 1, 1, line1, 1) - doc():set_selection(line1 - 1, col1, line2 - 1, col2, swap) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + append_line_if_last_line(line2) + if line1 > 1 then + local text = doc().lines[line1 - 1] + doc():insert(line2 + 1, 1, text) + doc():remove(line1 - 1, 1, line1, 1) + doc():set_selections(idx, line1 - 1, col1, line2 - 1, col2) + end end end, ["doc:move-lines-down"] = function() - local line1, col1, line2, col2, swap = doc_multiline_selection(true) - append_line_if_last_line(line2 + 1) - if line2 < #doc().lines then - local text = doc().lines[line2 + 1] - doc():remove(line2 + 1, 1, line2 + 2, 1) - doc():insert(line1, 1, text) - doc():set_selection(line1 + 1, col1, line2 + 1, col2, swap) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + append_line_if_last_line(line2 + 1) + if line2 < #doc().lines then + local text = doc().lines[line2 + 1] + doc():remove(line2 + 1, 1, line2 + 2, 1) + doc():insert(line1, 1, text) + doc():set_selections(idx, line1 + 1, col1, line2 + 1, col2) + end end end, @@ -256,28 +262,29 @@ local commands = { if not comment then return end local indentation = get_indent_string() local comment_text = comment .. " " - local line1, _, line2 = doc_multiline_selection(true) - local uncomment = true - local start_offset = math.huge - for line = line1, line2 do - local text = doc().lines[line] - local s = text:find("%S") - local cs, ce = text:find(comment_text, s, true) - if s and cs ~= s then - uncomment = false - start_offset = math.min(start_offset, s) - end - end - for line = line1, line2 do - local text = doc().lines[line] - local s = text:find("%S") - if uncomment then + for idx, line1, _, line2 in doc_multiline_selections(true) do + local uncomment = true + local start_offset = math.huge + for line = line1, line2 do + local text = doc().lines[line] + local s = text:find("%S") local cs, ce = text:find(comment_text, s, true) - if ce then - doc():remove(line, cs, line, ce + 1) + if s and cs ~= s then + uncomment = false + start_offset = math.min(start_offset, s) + end + end + for line = line1, line2 do + local text = doc().lines[line] + local s = text:find("%S") + if uncomment then + local cs, ce = text:find(comment_text, s, true) + if ce then + doc():remove(line, cs, line, ce + 1) + end + elseif s then + doc():insert(line, start_offset, comment_text) end - elseif s then - doc():insert(line, start_offset, comment_text) end end end, @@ -363,6 +370,32 @@ local commands = { end end, common.path_suggest) end, + + + ["file:delete"] = function() + local filename = doc().abs_filename + if not filename then + core.error("Cannot remove unsaved doc") + return + end + for i,docview in ipairs(core.get_views_referencing_doc(doc())) do + local node = core.root_view.root_node:get_node_for_view(docview) + node:close_view(core.root_view, docview) + end + os.remove(filename) + core.log("Removed \"%s\"", filename) + end, + + ["doc:create-cursor-previous-line"] = function() + split_cursor(-1) + doc():merge_cursors() + end, + + ["doc:create-cursor-next-line"] = function() + split_cursor(1) + doc():merge_cursors() + end + } @@ -392,21 +425,21 @@ for name, fn in pairs(translations) do end commands["doc:move-to-previous-char"] = function() - if doc():has_selection() then - local line, col = doc():get_selection(true) - doc():set_selection(line, col) - else - doc():move_to(translate.previous_char) + for idx, line1, col1, line2, col2 in doc():get_selections(true) do + if line1 ~= line2 or col1 ~= col2 then + doc():set_selections(idx, line1, col1) + end end + doc():move_to(translate.previous_char) end commands["doc:move-to-next-char"] = function() - if doc():has_selection() then - local _, _, line, col = doc():get_selection(true) - doc():set_selection(line, col) - else - doc():move_to(translate.next_char) + for idx, line1, col1, line2, col2 in doc():get_selections(true) do + if line1 ~= line2 or col1 ~= col2 then + doc():set_selections(idx, line2, col2) + end end + doc():move_to(translate.next_char) end command.add("core.docview", commands) diff --git a/data/core/common.lua b/data/core/common.lua index e8a989df..c1fd2498 100755 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -54,6 +54,26 @@ function common.color(str) end +function common.splice(t, at, remove, insert) + insert = insert or {} + local offset = #insert - remove + local old_len = #t + if offset < 0 then + for i = at - offset, old_len - offset do + t[i + offset] = t[i] + end + elseif offset > 0 then + for i = old_len, at, -1 do + t[i + offset] = t[i] + end + end + for i, item in ipairs(insert) do + t[at + i - 1] = item + end +end + + + local function compare_score(a, b) return a.score > b.score end diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 39fae9ca..0c5ba8fb 100755 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -16,26 +16,6 @@ local function split_lines(text) return res end - -local function splice(t, at, remove, insert) - insert = insert or {} - local offset = #insert - remove - local old_len = #t - if offset < 0 then - for i = at - offset, old_len - offset do - t[i + offset] = t[i] - end - elseif offset > 0 then - for i = old_len, at, -1 do - t[i + offset] = t[i] - end - end - for i, item in ipairs(insert) do - t[at + i - 1] = item - end -end - - function Doc:new(filename) self:reset() if filename then @@ -46,7 +26,8 @@ end function Doc:reset() self.lines = { "\n" } - self.selection = { a = { line=1, col=1 }, b = { line=1, col=1 } } + self.selections = { 1, 1, 1, 1 } + self.cursor_clipboard = {} self.undo_stack = { idx = 1 } self.redo_stack = { idx = 1 } self.clean_change_id = 1 @@ -126,45 +107,87 @@ function Doc:get_change_id() return self.undo_stack.idx 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 = self:get_selections(sort)({ self.selections, sort }, 0) + return line1, col1, line2, col2, sort +end -function Doc:set_selection(line1, col1, line2, col2, swap) - assert(not line2 == not col2, "expected 2 or 4 arguments") +function Doc:has_selection() + local line1, col1, line2, col2 = self:get_selection(false) + return line1 ~= line2 or col1 ~= col2 +end + +function Doc:sanitize_selection() + for idx, line1, col1, line2, col2 in self:get_selections() do + self:set_selections(idx, line1, col1, line2, col2) + 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 + end + return line1, col1, line2, col2 +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 line1, col1 = self:sanitize_position(line1, col1) line2, col2 = self:sanitize_position(line2 or line1, col2 or col1) - self.selection.a.line, self.selection.a.col = line1, col1 - self.selection.b.line, self.selection.b.col = line2, col2 + common.splice(self.selections, (idx - 1)*4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 }) 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 +function Doc:add_selection(line1, col1, line2, col2, swap) + local l1, c1 = sort_positions(line1, col1, line2 or line1, col2 or col1) + local target = #self.selections / 4 + 1 + for idx, tl1, tc1 in self:get_selections(true) do + if l1 < tl1 or l1 == tl1 and c1 < tc1 then + target = idx + break + end end - return line1, col1, line2, col2, false + self:set_selections(target, line1, col1, line2, col2, swap, 0) end +function Doc:set_selection(line1, col1, line2, col2, swap) + self.selections, self.cursor_clipboard = {}, {} + self:set_selections(1, line1, col1, line2, col2, swap) +end -function Doc:get_selection(sort) - local a, b = self.selection.a, self.selection.b - if sort then - return sort_positions(a.line, a.col, b.line, b.col) +function Doc:merge_cursors(idx) + for i = (idx or (#self.selections - 3)), (idx or 5), -4 do + for j = 1, i - 4, 4 do + if self.selections[i] == self.selections[j] and + self.selections[i+1] == self.selections[j+1] then + common.splice(self.selections, i, 4) + break + end + end end - return a.line, a.col, b.line, b.col end - -function Doc:has_selection() - local a, b = self.selection.a, self.selection.b - return not (a.line == b.line and a.col == b.col) +local function selection_iterator(invariant, idx) + 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 invariant[2] then + return idx+(invariant[3] and -1 or 1), sort_positions(unpack(invariant[1], target, target+4)) + else + return idx+(invariant[3] and -1 or 1), unpack(invariant[1], target, target+4) + end end - -function Doc:sanitize_selection() - self:set_selection(self:get_selection()) +-- If idx_reverse is true, it'll reverse iterate. If nil, or false, regular iterate. +-- If a number, runs for exactly that iteration. +function Doc:get_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) end - +-- End of cursor seciton. function Doc:sanitize_position(line, col) line = common.clamp(line, 1, #self.lines) @@ -251,14 +274,11 @@ local function pop_undo(self, undo_stack, redo_stack, modified) if cmd.type == "insert" then local line, col, text = table.unpack(cmd) self:raw_insert(line, col, text, redo_stack, cmd.time) - elseif cmd.type == "remove" then local line1, col1, line2, col2 = table.unpack(cmd) self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time) - elseif cmd.type == "selection" then - self.selection.a.line, self.selection.a.col = cmd[1], cmd[2] - self.selection.b.line, self.selection.b.col = cmd[3], cmd[4] + self.selections = { unpack(cmd) } end modified = modified or (cmd.type ~= "selection") @@ -288,11 +308,11 @@ function Doc:raw_insert(line, col, text, undo_stack, time) lines[#lines] = lines[#lines] .. after -- splice lines into line array - splice(self.lines, line, 1, lines) + common.splice(self.lines, line, 1, lines) -- push undo local line2, col2 = self:position_offset(line, col, #text) - push_undo(undo_stack, time, "selection", self:get_selection()) + push_undo(undo_stack, time, "selection", unpack(self.selections)) push_undo(undo_stack, time, "remove", line, col, line2, col2) -- update highlighter and assure selection is in bounds @@ -304,7 +324,7 @@ end function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) -- push undo local text = self:get_text(line1, col1, line2, col2) - push_undo(undo_stack, time, "selection", self:get_selection()) + push_undo(undo_stack, time, "selection", unpack(self.selections)) push_undo(undo_stack, time, "insert", line1, col1, text) -- get line content before/after removed text @@ -312,7 +332,7 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) local after = self.lines[line2]:sub(col2) -- splice line into line array - splice(self.lines, line1, line2 - line1 + 1, { before .. after }) + common.splice(self.lines, line1, line2 - line1 + 1, { before .. after }) -- update highlighter and assure selection is in bounds self.highlighter:invalidate(line1) @@ -348,22 +368,20 @@ function Doc:redo() end -function Doc:text_input(text) - if self:has_selection() then - self:delete_to() +function Doc:text_input(text, idx) + for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do + if line1 ~= line2 or col1 ~= col2 then + self:delete_to_cursor(sidx) + end + self:insert(line1, col1, text) + self:move_to_cursor(sidx, #text) end - local line, col = self:get_selection() - self:insert(line, col, text) - self:move_to(#text) end function Doc:replace(fn) - local line1, col1, line2, col2, swap - local had_selection = self:has_selection() - if had_selection then - line1, col1, line2, col2, swap = self:get_selection(true) - else + local line1, col1, line2, col2 = self:get_selection(true) + if line1 == line2 and col1 == col2 then line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines] end local old_text = self:get_text(line1, col1, line2, col2) @@ -371,38 +389,104 @@ function Doc:replace(fn) if old_text ~= new_text then self:insert(line2, col2, new_text) self:remove(line1, col1, line2, col2) - if had_selection then + if line1 == line2 and col1 == col2 then line2, col2 = self:position_offset(line1, col1, #new_text) - self:set_selection(line1, col1, line2, col2, swap) + self:set_selection(line1, col1, line2, col2) end end return n end -function Doc:delete_to(...) - local line, col = self:get_selection(true) - if self:has_selection() then - self:remove(self:get_selection()) - else - local line2, col2 = self:position_offset(line, col, ...) - self:remove(line, col, line2, col2) - line, col = sort_positions(line, col, line2, col2) +function Doc:delete_to_cursor(idx, ...) + for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do + if line1 ~= line2 or col1 ~= col2 then + self:remove(line1, col1, line2, col2) + else + local l2, c2 = self:position_offset(line1, col1, ...) + self:remove(line1, col1, l2, c2) + line1, col1 = sort_positions(line1, col1, l2, c2) + end + self:set_selections(sidx, line1, col1) end - self:set_selection(line, col) + self:merge_cursors(idx) +end +function Doc:delete_to(...) return self:delete_to_cursor(nil, ...) end + +function Doc:move_to_cursor(idx, ...) + for sidx, line, col in self:get_selections(false, idx) do + self:set_selections(sidx, self:position_offset(line, col, ...)) + end + self:merge_cursors(idx) +end +function Doc:move_to(...) return self:move_to_cursor(nil, ...) end + + +function Doc:select_to_cursor(idx, ...) + for sidx, line, col, line2, col2 in self:get_selections(false, idx) do + line, col = self:position_offset(line, col, ...) + self:set_selections(sidx, line, col, line2, col2) + end + self:merge_cursors(idx) +end +function Doc:select_to(...) return self:select_to_cursor(nil, ...) end + + +local function get_indent_string() + if config.tab_type == "hard" then + return "\t" + end + return string.rep(" ", config.indent_size) end - -function Doc:move_to(...) - local line, col = self:get_selection() - self:set_selection(self:position_offset(line, col, ...)) +-- returns the size of the original indent, and the indent +-- in your config format, rounded either up or down +local function get_line_indent(line, rnd_up) + local _, e = line:find("^[ \t]+") + local soft_tab = string.rep(" ", config.indent_size) + if config.tab_type == "hard" then + local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or "" + return e, indent:gsub(" +", rnd_up and "\t" or "") + else + local indent = e and line:sub(1, e):gsub("\t", soft_tab) or "" + local number = #indent / #soft_tab + return e, indent:sub(1, + (rnd_up and math.ceil(number) or math.floor(number))*#soft_tab) + end end - -function Doc:select_to(...) - local line, col, line2, col2 = self:get_selection() - line, col = self:position_offset(line, col, ...) - self:set_selection(line, col, line2, col2) +-- un/indents text; behaviour varies based on selection and un/indent. +-- * if there's a selection, it will stay static around the +-- text for both indenting and unindenting. +-- * if you are in the beginning whitespace of a line, and are indenting, the +-- cursor will insert the exactly appropriate amount of spaces, and jump the +-- cursor to the beginning of first non whitespace characters +-- * if you are not in the beginning whitespace of a line, and you indent, it +-- inserts the appropriate whitespace, as if you typed them normally. +-- * if you are unindenting, the cursor will jump to the start of the line, +-- and remove the appropriate amount of spaces (or a tab). +function Doc:indent_text(unindent, line1, col1, line2, col2) + local text = get_indent_string() + local _, se = self.lines[line1]:find("^[ \t]+") + local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) + local has_selection = line1 ~= line2 or col1 ~= col2 + if unindent or has_selection or in_beginning_whitespace then + local l1d, l2d = #self.lines[line1], #self.lines[line2] + for line = line1, line2 do + local e, rnded = get_line_indent(self.lines[line], unindent) + self:remove(line, 1, line, (e or 0) + 1) + self:insert(line, 1, + unindent and rnded:sub(1, #rnded - #text) or rnded .. text) + end + l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d + if (unindent or in_beginning_whitespace) and not has_selection then + local start_cursor = (se and se + 1 or 1) + l1d or #(self.lines[line1]) + return line1, start_cursor, line2, start_cursor + end + return line1, col1 + l1d, line2, col2 + l2d + end + self:insert(line1, col1, text) + return line1, col1 + #text, line1, col1 + #text end -- For plugins to add custom actions of document change diff --git a/data/core/docview.lua b/data/core/docview.lua index 2375a499..0db548e3 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -259,7 +259,11 @@ function DocView:on_mouse_pressed(button, x, y, clicks) end else local line, col = self:resolve_screen_position(x, y) - self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col)) + if keymap.modkeys["ctrl"] then + self.doc:add_selection(mouse_selection(self.doc, clicks, line, col, line, col)) + else + self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col)) + end self.mouse_selecting = { line, col, clicks = clicks } end core.blink_reset() @@ -279,7 +283,15 @@ function DocView:on_mouse_moved(x, y, ...) local l1, c1 = self:resolve_screen_position(x, y) local l2, c2 = table.unpack(self.mouse_selecting) local clicks = self.mouse_selecting.clicks - self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2)) + if keymap.modkeys["ctrl"] then + if l1 > l2 then l1, l2 = l2, l1 end + self.doc.selections = { } + for i = l1, l2 do + self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i])) + end + else + self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2)) + end end end @@ -343,46 +355,50 @@ end function DocView:draw_line_body(idx, x, y) - local line, col = self.doc:get_selection() - -- draw selection if it overlaps this line - local line1, col1, line2, col2 = self.doc:get_selection(true) - if idx >= line1 and idx <= line2 then - local text = self.doc.lines[idx] - if line1 ~= idx then col1 = 1 end - if line2 ~= idx then col2 = #text + 1 end - local x1 = x + self:get_col_x_offset(idx, col1) - local x2 = x + self:get_col_x_offset(idx, col2) - local lh = self:get_line_height() - renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) + for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do + if idx >= line1 and idx <= line2 then + local text = self.doc.lines[idx] + if line1 ~= idx then col1 = 1 end + if line2 ~= idx then col2 = #text + 1 end + local x1 = x + self:get_col_x_offset(idx, col1) + local x2 = x + self:get_col_x_offset(idx, col2) + local lh = self:get_line_height() + renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) + end end - - -- draw line highlight if caret is on this line - if config.highlight_current_line and not self.doc:has_selection() - and line == idx and core.active_view == self then - self:draw_line_highlight(x + self.scroll.x, y) + for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do + -- draw line highlight if caret is on this line + if config.highlight_current_line and (line1 == line2 and col1 == col2) + and line1 == idx and core.active_view == self then + self:draw_line_highlight(x + self.scroll.x, y) + end end - + -- draw line's text self:draw_line_text(idx, x, y) -- draw caret if it overlaps this line local T = config.blink_period - if line == idx and core.active_view == self - and (core.blink_timer - core.blink_start) % T < T / 2 - and system.window_has_focus() then - local lh = self:get_line_height() - local x1 = x + self:get_col_x_offset(line, col) - renderer.draw_rect(x1, y, style.caret_width, lh, style.caret) + for _, line, col in self.doc:get_selections() do + if line == idx and core.active_view == self + and (core.blink_timer - core.blink_start) % T < T / 2 + and system.window_has_focus() then + local lh = self:get_line_height() + local x1 = x + self:get_col_x_offset(line, col) + renderer.draw_rect(x1, y, style.caret_width, lh, style.caret) + end end end function DocView:draw_line_gutter(idx, x, y) local color = style.line_number - local line1, _, line2, _ = self.doc:get_selection(true) - if idx >= line1 and idx <= line2 then - color = style.line_number2 + for _, line1, _, line2 in self.doc:get_selections(true) do + if idx >= line1 and idx <= line2 then + color = style.line_number2 + break + end end local yoffset = self:get_line_text_y_offset() x = x + style.padding.x diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index ccbb102a..3db4d4f7 100755 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -101,6 +101,8 @@ local function keymap_macos(keymap) ["cmd+shift+end"] = "doc:select-to-end-of-doc", ["shift+pageup"] = "doc:select-to-previous-page", ["shift+pagedown"] = "doc:select-to-next-page", + ["cmd+shift+up"] = "doc:create-cursor-previous-line", + ["cmd+shift+down"] = "doc:create-cursor-next-line" } end diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 2e1372fb..c916892f 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -205,6 +205,8 @@ keymap.add_direct { ["ctrl+shift+end"] = "doc:select-to-end-of-doc", ["shift+pageup"] = "doc:select-to-previous-page", ["shift+pagedown"] = "doc:select-to-next-page", + ["ctrl+shift+up"] = "doc:create-cursor-previous-line", + ["ctrl+shift+down"] = "doc:create-cursor-next-line" } return keymap