From 37a3884ee29823252d84c5abf1fa22f4e0f25508 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sat, 5 Jun 2021 15:22:56 -0400 Subject: [PATCH] Initial commit of multicursor. Next step is to investigate how multicursor works on various other IDEs and ape those. --- data/core/commands/doc.lua | 245 +++++++++++++++++++++---------------- data/core/doc/init.lua | 124 +++++++++++++------ data/core/docview.lua | 69 +++++++---- 3 files changed, 270 insertions(+), 168 deletions(-) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index c8c27509..ae67d5e1 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -24,13 +24,19 @@ 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 = doc():get_selections(sort) + return function() + local idx, line1, col1, line2, col2 = iter() + if not idx then + return + end + if 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) @@ -76,46 +82,51 @@ local commands = { 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() 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() 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() 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, line, col in doc():get_selections() do + if not doc():has_selection(idx) and doc().lines[line]:find("^%s*$", col) then + doc():remove(line, col, line, math.huge) + end + doc():delete_to(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, line, col in doc():get_selections() do + if not doc():has_selection(idx) then + local text = doc():get_text(line, 1, line, col) + if #text >= config.indent_size and text:find("^ *$") then + doc():delete_to(idx, 0, -config.indent_size) + return + end end + doc():delete_to(idx, translate.previous_char) end - doc():delete_to(translate.previous_char) end, ["doc:select-all"] = function() @@ -128,75 +139,92 @@ local commands = { 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, _, line2 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 doc():has_selection(idx) then + doc():set_selections(idx, line1, math.huge) + end end end, ["doc:indent"] = function() - doc():indent_text(false, doc_multiline_selection(true)) + 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 not l1 then + doc():set_selections(idx, l1, c1, l2, c2) + end + end end, ["doc:unindent"] = function() - doc():indent_text(true, doc_multiline_selection(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 not 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, @@ -205,28 +233,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, @@ -350,26 +379,30 @@ local translations = { } for name, fn in pairs(translations) do - commands["doc:move-to-" .. name] = function() doc():move_to(fn, dv()) end - commands["doc:select-to-" .. name] = function() doc():select_to(fn, dv()) end - commands["doc:delete-to-" .. name] = function() doc():delete_to(fn, dv()) end + commands["doc:move-to-" .. name] = function() doc():move_to(nil, fn, dv()) end + commands["doc:select-to-" .. name] = function() doc():select_to(nil, fn, dv()) end + commands["doc:delete-to-" .. name] = function() doc():delete_to(nil, fn, dv()) end 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, line, col in doc():get_selections(true) do + if doc():has_selection(idx) then + doc():set_selections(idx, line, col) + end + end + if not doc():has_selection() then + doc():move_to(nil, translate.previous_char) end 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, _, _, line, col in doc():get_selections(true) do + if doc():has_selection(idx) then + doc():set_selections(idx, line, col) + end + end + if not doc():has_selection() then + doc():move_to(nil, translate.next_char) end end diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 88f63a68..63a4225b 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -46,7 +46,7 @@ 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.undo_stack = { idx = 1 } self.redo_stack = { idx = 1 } self.clean_change_id = 1 @@ -131,9 +131,20 @@ function Doc:set_selection(line1, col1, line2, col2, swap) assert(not line2 == not col2, "expected 2 or 4 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.selections = { line1, col1, line2, col2 } +end + +function Doc:set_selections(idx, line1, col1, line2, col2, swap) + 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 + local target = (idx - 1)*4 + 1 + self.selections[target] = line1 + self.selections[target+1] = col1 + self.selections[target+2] = line2 + self.selections[target+3] = col2 end @@ -147,22 +158,53 @@ 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) + return sort_positions(self.selections[1], self.selections[2], self.selections[3], self.selections[4]) + end + return self.selections[1], self.selections[2], self.selections[3], self.selections[4] +end + +function Doc:get_selection_count() + return #self.selections / 4 +end + +function Doc:get_selections(sort) + local idx = 1 + return function() + if idx >= #self.selections then + return + end + idx = idx + 4 + if sort then + return ((idx - 5) / 4) + 1, sort_positions(self.selections[idx - 4], self.selections[idx - 3], self.selections[idx - 2], self.selections[idx - 1]) + else + return ((idx - 5) / 4) + 1, self.selections[idx - 4], self.selections[idx - 3], self.selections[idx - 2], self.selections[idx - 1] + 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) +function Doc:has_selection(idx) + if idx then + local target = (idx-1)*4+1 + return + self.selections[target] ~= self.selections[target+2] or + self.selections[target+1] ~= self.selections[target+3] + end + for target = 1, #self.selections, 4 do + if self.selections[target] ~= self.selections[target+2] or + self.selections[target+1] ~= self.selections[target+3] then + return true + end + end + return false end function Doc:sanitize_selection() - self:set_selection(self:get_selection()) + for idx, line1, col1, line2, col2 in self:get_selections() do + self:set_selections(idx, line1, col1, line2, col2) + end end @@ -348,13 +390,16 @@ 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, line, col in self:get_selections() do + if not idx or idx == sidx then + if self:has_selection(sidx) then + self:delete_to(sidx) + end + self:insert(line, col, text) + self:move_to(sidx, #text) + end end - local line, col = self:get_selection() - self:insert(line, col, text) - self:move_to(#text) end @@ -380,29 +425,38 @@ function Doc:replace(fn) 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(idx, ...) + for sidx, line1, col1, line2, col2 in self:get_selections(true) do + if not idx or sidx == idx then + if self:has_selection(sidx) then + self:remove(line1, col1, line2, col2) + else + local l2, c2 = self:position_offset(line, col, ...) + self:remove(line1, col1, l2, c2) + line1, col1 = sort_positions(line1, col1, l2, c2) + end + self:set_selections(sidx, line1, col1) + end end - self:set_selection(line, col) end -function Doc:move_to(...) - local line, col = self:get_selection() - self:set_selection(self:position_offset(line, col, ...)) +function Doc:move_to(idx, ...) + for sidx, line, col in self:get_selections() do + if not idx or sidx == idx then + self:set_selections(sidx, self:position_offset(line, col, ...)) + end + 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) +function Doc:select_to(idx, ...) + for sidx, line, col, line2, col2 in self:get_selections() do + if not idx or idx == sidx then + line, col = self:position_offset(line, col, ...) + self:set_selections(sidx, line, col, line2, col2) + end + end end @@ -439,7 +493,7 @@ end -- 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, swap) +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) @@ -455,9 +509,9 @@ function Doc:indent_text(unindent, line1, col1, line2, col2, swap) l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d if (unindent or in_beginning_whitespace) and not self:has_selection() then local start_cursor = (se and se + 1 or 1) + l1d or #(self.lines[line1]) - self:set_selection(line1, start_cursor, line2, start_cursor, swap) + return line1, start_cursor, line2, start_cursor else - self:set_selection(line1, col1 + l1d, line2, col2 + l2d, swap) + return line1, col1 + l1d, line2, col2 + l2d end else self:text_input(text) diff --git a/data/core/docview.lua b/data/core/docview.lua index 070ee0c4..edde4c55 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -276,7 +276,18 @@ 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 + l2 = l1 + end + local idx = 1 + for i = l1, l2 do + idx = idx + 1 + self.doc:set_selections(idx, i, math.min(c1, #self.doc.lines[i]), i, math.min(c1, #self.doc.lines[i])) + end + else + self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2)) + end end end @@ -340,46 +351,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 not self.doc:has_selection(lidx) + 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