Skip to content

Commit 8326f3a

Browse files
authored
autoinsert: add multiple cursors support (#36)
adds the functionality for multiple cursors as well as matching the behavior of sublime/vscode more closely Co-authored-by: Lore M <maierulorenzo@gmail.com>
1 parent 20957b9 commit 8326f3a

2 files changed

Lines changed: 161 additions & 61 deletions

File tree

manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,9 @@
221221
{
222222
"description": "Automatically inserts closing brackets and quotes. Also allows selected text to be wrapped with brackets or quotes.",
223223
"id": "autoinsert",
224-
"mod_version": "3",
224+
"mod_version": "3.1",
225225
"path": "plugins/autoinsert.lua",
226-
"version": "0.2"
226+
"version": "0.3"
227227
},
228228
{
229229
"description": "Automatically saves files when they are changed",

plugins/autoinsert.lua

Lines changed: 159 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- mod-version:3
1+
-- mod-version:3.1
22
local core = require "core"
33
local translate = require "core.doc.translate"
44
local config = require "core.config"
@@ -8,24 +8,33 @@ local command = require "core.command"
88
local keymap = require "core.keymap"
99

1010

11-
config.plugins.autoinsert = common.merge({ map = {
12-
["["] = "]",
13-
["{"] = "}",
14-
["("] = ")",
15-
['"'] = '"',
16-
["'"] = "'",
17-
["`"] = "`",
18-
} }, config.plugins.autoinsert)
11+
config.plugins.autoinsert = common.merge({
12+
map = {
13+
["["] = "]",
14+
["{"] = "}",
15+
["("] = ")",
16+
['"'] = '"',
17+
["'"] = "'",
18+
["`"] = "`",
19+
}
20+
}, config.plugins.autoinsert)
1921

2022

23+
-- @param chr stirng
24+
-- @return boolean
2125
local function is_closer(chr)
2226
for _, v in pairs(config.plugins.autoinsert.map) do
2327
if v == chr then
2428
return true
2529
end
2630
end
31+
return false
2732
end
2833

34+
35+
-- @param text stirng
36+
-- @param chr stirng
37+
-- @return number
2938
local function count_char(text, chr)
3039
local count = 0
3140
for _ in text:gmatch(chr) do
@@ -35,87 +44,178 @@ local function count_char(text, chr)
3544
end
3645

3746

38-
local on_text_input = DocView.on_text_input
47+
-- @param dv DocView
48+
-- @param idx number
49+
-- @param text string
50+
-- @param mapping string
51+
-- @return boolean
52+
local function on_text_input_cursor(dv, idx, text, mapping)
53+
local l1, c1, l2, c2 = dv.doc:get_selection_idx(idx, true)
54+
local is_selection_empty = not (l1 ~= l2 or c1 ~= c2)
3955

40-
function DocView:on_text_input(text)
56+
-- wrap selection if we have a selection
57+
if mapping and not is_selection_empty then
58+
dv.doc:insert(l2, c2, mapping)
59+
dv.doc:insert(l1, c1, text)
60+
dv.doc:set_selections(idx, l1, c1 + 1, l2, c2 + 1, true)
4161

42-
-- Don't insert on multiselections
43-
if #self.doc.selections > 4 then return on_text_input(self, text) end
62+
return true
63+
end
64+
65+
-- no selections, check char next to cursor
66+
local chr = dv.doc:get_char(l1, c1)
4467

68+
-- skip inserting closing text if already there,
69+
-- instead just move the cursor to the right of the chr
70+
if text == chr and is_closer(chr) then
71+
dv.doc:move_to_cursor(idx, 1)
72+
return true
73+
end
74+
75+
-- don't insert closing quote if we have a non-even number on this line
76+
if text == mapping and count_char(dv.doc.lines[l1], text) % 2 == 1 then
77+
return false
78+
end
79+
80+
-- auto insert closing bracket
81+
-- checks that character next to the cursor is:
82+
-- either whitespace (%s) or the mapped closer character.
83+
-- and it's not a double quote character ('"')
84+
if mapping and (chr:find("%s") or is_closer(chr) and chr ~= '"') then
85+
dv.doc:insert(l1, c1, text)
86+
dv.doc:insert(l2, c2 + 1, mapping)
87+
-- move inside the bracket pair:
88+
dv.doc:move_to_cursor(idx, 1)
89+
return true
90+
end
91+
92+
return false
93+
end
94+
95+
96+
-- save the original on_text_input to call it later
97+
local on_text_input = DocView.on_text_input
98+
99+
function DocView:on_text_input(text)
45100
local mapping = config.plugins.autoinsert.map[text]
46101

47102
-- prevents plugin from operating on `CommandView`
48103
if getmetatable(self) ~= DocView then
49104
return on_text_input(self, text)
50105
end
51106

52-
-- wrap selection if we have a selection
53-
if mapping and self.doc:has_selection() then
54-
local l1, c1, l2, c2, swap = self.doc:get_selection(true)
55-
self.doc:insert(l2, c2, mapping)
56-
self.doc:insert(l1, c1, text)
57-
self.doc:set_selection(l1, c1, l2, c2 + 2, swap)
107+
-- call auto insert on every selection
108+
for idx in self.doc:get_selections() do
109+
local inserted = on_text_input_cursor(self, idx, text, mapping)
110+
111+
-- operate normally on the cursor when nothing was inserted
112+
if not inserted then
113+
self.doc:text_input(text, idx)
114+
end
115+
end
116+
end
117+
118+
-- this deletes the matching pair when backspacing the opening one
119+
-- @param doc DocView.doc
120+
-- @param idx number
121+
local function delete_matching_pair(doc, idx)
122+
local l1, c1, l2, c2 = doc:get_selection_idx(idx, true)
123+
124+
-- skip backspace if at the beginning of the line
125+
if c1 <= 1 then
58126
return
59127
end
60128

61-
-- skip inserting closing text
62-
local chr = self.doc:get_char(self.doc:get_selection())
63-
if text == chr and is_closer(chr) then
64-
self.doc:move_to(1)
129+
-- only do it if there's nothing selected for the cursor
130+
local is_selection_empty = not (l1 ~= l2 or c1 ~= c2)
131+
if not is_selection_empty then
65132
return
66133
end
67134

68-
-- don't insert closing quote if we have a non-even number on this line
69-
local line = self.doc:get_selection()
70-
if text == mapping and count_char(self.doc.lines[line], text) % 2 == 1 then
71-
return on_text_input(self, text)
135+
-- check if the character to the right of the one being deleted
136+
-- is the expected matching pair
137+
local chr = doc:get_char(l1, c1)
138+
local mapped = config.plugins.autoinsert.map[doc:get_char(l1, c1 - 1)]
139+
if mapped and mapped == chr then
140+
-- delete 1 more character
141+
doc:remove(l1, c1, l2, c2 + 1)
72142
end
143+
end
73144

74-
-- auto insert closing bracket
75-
if mapping and (chr:find("%s") or is_closer(chr) and chr ~= '"') then
76-
on_text_input(self, text)
77-
on_text_input(self, mapping)
78-
self.doc:move_to(-1)
79-
return
145+
-- @param doc DocView.doc
146+
local function on_backspace(doc)
147+
for idx in doc:get_selections() do
148+
delete_matching_pair(doc, idx)
149+
end
150+
-- execute the backspace normally
151+
command.perform "doc:backspace"
152+
end
153+
154+
-- need this because the doc:backspace already operates on all cursors,
155+
-- we only want to do it on a single cursor
156+
local function perform_backspace_cursor(doc, idx)
157+
local _, indent_size = doc:get_indent_info()
158+
local line1, col1, line2, col2 = doc:get_selection(idx, true)
159+
if line1 == line2 and col1 == col2 then
160+
local text = doc:get_text(line1, 1, line1, col1)
161+
if #text >= indent_size and text:find("^ *$") then
162+
doc:delete_to_cursor(idx, 0, -indent_size)
163+
return
164+
end
80165
end
166+
doc:delete_to_cursor(idx, translate.previous_char)
167+
end
81168

82-
on_text_input(self, text)
169+
-- @param doc DocView.doc
170+
local function on_delete_to_previous_word_start(doc)
171+
for idx in doc:get_selections() do
172+
local le, ce = translate.previous_word_start(
173+
doc, doc:get_selection_idx(idx, true))
174+
ce = ce + 1 -- dont over delete
175+
repeat
176+
local l, c = doc:get_selection_idx(idx, true)
177+
178+
-- delete character and matching pair if any
179+
-- we dont call on_backspace because that already operates on every cursor.
180+
delete_matching_pair(doc, idx)
181+
perform_backspace_cursor(doc, idx)
182+
until l <= le and c <= ce
183+
end
83184
end
84185

85186

187+
-- @param doc DocView.doc
188+
local function on_delete_to_start_of_line(doc)
189+
for idx in doc:get_selections() do
190+
local le, ce = translate.start_of_line(
191+
doc, doc:get_selection_idx(idx, true))
192+
ce = ce + 1 -- dont over delete
193+
repeat
194+
local l, c = doc:get_selection_idx(idx, true)
195+
196+
-- delete character and matching pair if any
197+
-- we dont call on_backspace because that already operates on every cursor.
198+
delete_matching_pair(doc, idx)
199+
perform_backspace_cursor(doc, idx)
200+
until l <= le and c <= ce
201+
end
202+
end
203+
86204

87205
local function predicate()
88-
return core.active_view:is(DocView)
89-
and #core.active_view.doc.selections <= 4 and not core.active_view.doc:has_selection(), core.active_view.doc
206+
return core.active_view:is(DocView), core.active_view.doc
90207
end
91208

209+
92210
command.add(predicate, {
93-
["autoinsert:backspace"] = function(doc)
94-
local l, c = doc:get_selection()
95-
if c > 1 then
96-
local chr = doc:get_char(l, c)
97-
local mapped = config.plugins.autoinsert.map[doc:get_char(l, c - 1)]
98-
if mapped and mapped == chr then
99-
doc:delete_to(1)
100-
end
101-
end
102-
command.perform "doc:backspace"
103-
end,
104-
105-
["autoinsert:delete-to-previous-word-start"] = function(doc)
106-
local le, ce = translate.previous_word_start(doc, doc:get_selection())
107-
while true do
108-
local l, c = doc:get_selection()
109-
if l == le and c == ce then
110-
break
111-
end
112-
command.perform "autoinsert:backspace"
113-
end
114-
end,
211+
["autoinsert:backspace"] = on_backspace,
212+
["autoinsert:delete-to-previous-word-start"] = on_delete_to_previous_word_start,
213+
["autoinsert:delete-to-start-of-line"] = on_delete_to_start_of_line,
115214
})
116215

117216
keymap.add {
118217
["backspace"] = "autoinsert:backspace",
119218
["ctrl+backspace"] = "autoinsert:delete-to-previous-word-start",
120-
["ctrl+shift+backspace"] = "autoinsert:delete-to-previous-word-start",
219+
["ctrl+shift+backspace"] = "autoinsert:delete-to-start-of-line",
121220
}
221+

0 commit comments

Comments
 (0)