Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit b8d999a

Browse files
committed
feat: replace custom cmdline autocomplete with wildmenu noselect
Requires: neovim/neovim#32709 Signed-off-by: Tomas Slusny <[email protected]>
1 parent fcd91c9 commit b8d999a

File tree

2 files changed

+27
-322
lines changed

2 files changed

+27
-322
lines changed

README.md

+1-14
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,7 @@ require("autocomplete.buffer").setup {
3838
}
3939

4040
-- cmdline autocompletion
41-
require("autocomplete.cmd").setup {
42-
mappings = {
43-
accept = '<C-y>',
44-
reject = '<C-e>',
45-
complete = '<C-space>',
46-
next = '<C-n>',
47-
previous = '<C-p>',
48-
},
49-
border = nil, -- Cmdline completion border style
50-
columns = 5, -- Number of columns per row
51-
rows = 0.3, -- Number of rows, if < 1 then its fraction of total vim lines, if > 1 then its absolute number
52-
close_on_done = true, -- Close completion window when done (accept/reject)
53-
debounce_delay = 100,
54-
}
41+
require("autocomplete.cmd").setup()
5542
```
5643

5744
You also probably want to enable `popup` in completeopt to show documentation preview:

lua/autocomplete/cmd.lua

+26-308
Original file line numberDiff line numberDiff line change
@@ -1,317 +1,35 @@
1-
local util = require('autocomplete.util')
2-
31
local M = {}
42

5-
local state = {
6-
entry = nil,
7-
ns = {
8-
selection = nil,
9-
directory = nil,
10-
},
11-
completion = {
12-
current = 0,
13-
last = nil,
14-
skip_next = false,
15-
noselect = false,
16-
menuone = false,
17-
data = {},
18-
},
19-
window = {
20-
id = nil,
21-
bufnr = nil,
22-
},
23-
}
24-
25-
local function open_win(height)
26-
if not state.window.bufnr or not vim.api.nvim_buf_is_valid(state.window.bufnr) then
27-
state.window.bufnr = vim.api.nvim_create_buf(false, true)
28-
end
29-
30-
if not state.window.id or not vim.api.nvim_win_is_valid(state.window.id) then
31-
state.window.id = vim.api.nvim_open_win(state.window.bufnr, false, {
32-
relative = 'editor',
33-
border = M.config.border,
34-
style = 'minimal',
35-
width = vim.o.columns,
36-
height = height,
37-
row = vim.o.lines - 2,
38-
col = 0,
39-
})
40-
end
41-
end
42-
43-
local function close_win()
44-
if state.window.id and vim.api.nvim_win_is_valid(state.window.id) then
45-
vim.api.nvim_win_close(state.window.id, true)
46-
state.window.id = nil
47-
end
48-
end
49-
50-
local function is_cmdline()
51-
return vim.fn.getcmdwintype() == '' and vim.v.event.cmdtype == ':' and vim.fn.mode() == 'c'
52-
end
53-
54-
local function highlight_selection()
55-
if state.completion.current < 1 or state.completion.current > #state.completion.data then
56-
return
57-
end
58-
59-
vim.api.nvim_buf_clear_namespace(state.window.bufnr, state.ns.selection, 0, -1)
60-
vim.highlight.range(
61-
state.window.bufnr,
62-
state.ns.selection,
63-
'PmenuSel',
64-
state.completion.data[state.completion.current].start,
65-
state.completion.data[state.completion.current].finish,
66-
{}
67-
)
68-
end
69-
70-
local function update_cmdline(accept, reset)
71-
if vim.tbl_isempty(state.completion.data) then
72-
return
73-
end
74-
75-
if state.completion.current > #state.completion.data then
76-
state.completion.current = 1
77-
end
78-
if state.completion.current < 1 then
79-
state.completion.current = #state.completion.data
80-
end
81-
82-
if accept or reset then
83-
if M.config.close_on_done then
84-
state.completion.skip_next = true
85-
close_win()
86-
end
87-
else
88-
state.completion.skip_next = not accept
89-
end
90-
91-
if reset then
92-
vim.fn.setcmdline(state.completion.last)
93-
return
94-
end
95-
96-
highlight_selection()
97-
vim.cmd('redraw')
98-
99-
local commands = vim.split(vim.fn.getcmdline(), ' ')
100-
table.remove(commands, #commands)
101-
local new_cmdline = table.concat(commands, ' ')
102-
.. ' '
103-
.. state.completion.data[state.completion.current].completion
104-
new_cmdline = vim.trim(new_cmdline)
105-
if accept and not M.config.close_on_done then
106-
new_cmdline = new_cmdline .. (vim.endswith(new_cmdline, '/') and '' or ' ')
107-
end
108-
109-
vim.fn.setcmdline(new_cmdline)
110-
end
111-
112-
local function cmdline_changed()
113-
-- We are currently updating command line via next/prev, so do not refresh it
114-
if state.completion.skip_next then
115-
state.completion.skip_next = false
116-
return
117-
end
118-
119-
local window_height = M.config.rows
120-
if window_height < 1 then
121-
window_height = math.floor(vim.o.lines * window_height)
122-
end
123-
124-
local col_width = math.floor(vim.o.columns / M.config.columns)
125-
126-
-- Recreate window if we closed it before
127-
open_win(window_height)
128-
129-
-- Clear window
130-
local window_data = {}
131-
for _ = 0, window_height do
132-
window_data[#window_data + 1] = (' '):rep(vim.o.columns)
133-
end
134-
vim.api.nvim_buf_set_lines(state.window.bufnr, 0, window_height, false, window_data)
135-
136-
-- Get completions
137-
local input = vim.fn.getcmdline()
138-
local completions = vim.fn.getcompletion(input, 'cmdline')
139-
140-
state.completion.last = input
141-
state.completion.data = {}
142-
state.completion.current = state.completion.noselect and 0 or 1
143-
144-
if #completions <= (state.completion.menuone and 0 or 1) then
145-
close_win()
146-
return
147-
end
148-
149-
local completion_data = {}
150-
151-
local i = 1
152-
for line = 0, window_height - 1 do
153-
for col = 0, math.floor(vim.o.columns / col_width) - 1 do
154-
if i > #completions then
155-
break
156-
end
157-
158-
local completion = completions[i]
159-
local is_directory = vim.fn.isdirectory(vim.fn.fnamemodify(completion, ':p')) == 1
160-
161-
local shortened = vim.fn.fnamemodify(completion, ':p:t')
162-
if shortened == '' then
163-
shortened = vim.fn.fnamemodify(completion, ':p:h:t')
164-
end
165-
if is_directory then
166-
shortened = shortened .. '/'
167-
end
168-
shortened = ' ' .. shortened .. ' '
169-
170-
if string.len(shortened) >= col_width then
171-
shortened = string.sub(shortened, 1, col_width - 4) .. '...'
172-
end
173-
174-
local end_col = col * col_width + string.len(shortened)
175-
if end_col > vim.o.columns then
176-
break
177-
end
178-
179-
vim.api.nvim_buf_set_text(
180-
state.window.bufnr,
181-
line,
182-
col * col_width,
183-
line,
184-
end_col,
185-
{ shortened }
186-
)
187-
188-
local data = {
189-
start = { line, col * col_width },
190-
finish = { line, end_col },
191-
completion = completion,
192-
}
193-
194-
completion_data[i] = data
195-
if is_directory then
196-
vim.highlight.range(
197-
state.window.bufnr,
198-
state.ns.directory,
199-
'Directory',
200-
data.start,
201-
data.finish,
202-
{}
203-
)
204-
end
205-
206-
i = i + 1
207-
end
208-
end
209-
210-
state.completion.data = completion_data
211-
212-
vim.api.nvim_win_set_height(
213-
state.window.id,
214-
math.min(math.floor(#completions / (math.floor(vim.o.columns / col_width))), window_height)
215-
)
216-
highlight_selection()
217-
vim.cmd('redraw')
218-
end
219-
220-
local function changed_handler()
221-
if not is_cmdline() then
222-
close_win()
223-
return
224-
end
225-
226-
util.debounce(state.entry, M.config.debounce_delay, cmdline_changed)
227-
end
3+
function M.setup()
4+
local term = vim.api.nvim_replace_termcodes('<C-z>', true, true, true)
2285

229-
local function enter_handler()
230-
if not is_cmdline() then
231-
close_win()
232-
return
233-
end
234-
235-
state.completion.menuone = string.find(vim.o.completeopt, 'menuone')
236-
state.completion.noselect = string.find(vim.o.completeopt, 'noselect')
237-
changed_handler()
238-
end
239-
240-
local function leave_handler()
241-
util.stop(state.entry)
242-
state.completion.data = {}
243-
state.completion.last = nil
244-
close_win()
245-
end
6+
vim.opt.wildmenu = true
7+
vim.opt.wildmode = 'noselect:lastused,full'
8+
vim.opt.wildcharm = vim.fn.char2nr(term)
2469

247-
M.config = {
248-
mappings = {
249-
accept = '<C-y>',
250-
reject = '<C-e>',
251-
complete = '<C-space>',
252-
next = '<C-n>',
253-
previous = '<C-p>',
254-
},
255-
border = nil, -- Cmdline completion border style
256-
columns = 5, -- Number of columns per row
257-
rows = 0.3, -- Number of rows, if < 1 then its fraction of total vim lines, if > 1 then its absolute number
258-
close_on_done = true, -- Close completion window when done (accept/reject)
259-
debounce_delay = 100,
260-
}
10+
vim.keymap.set('c', '<Up>', '<End><C-U><Up>', { silent = true })
11+
vim.keymap.set('c', '<Down>', '<End><C-U><Down>', { silent = true })
26112

262-
function M.setup(config)
263-
M.config = vim.tbl_deep_extend('force', M.config, config or {})
264-
265-
-- Wild menu needs to be always disabled otherwise its in background for no reason
266-
vim.o.wildmenu = false
267-
268-
state.entry = util.entry()
269-
state.ns.selection = vim.api.nvim_create_namespace('CmdlineCompletionSelection')
270-
state.ns.directory = vim.api.nvim_create_namespace('CmdlineCompletionDirectory')
271-
272-
if M.config.mappings.accept then
273-
vim.keymap.set('c', M.config.mappings.accept, function()
274-
update_cmdline(true)
275-
end, { desc = 'Accept cmdline completion' })
276-
end
277-
if M.config.mappings.reject then
278-
vim.keymap.set('c', M.config.mappings.reject, function()
279-
update_cmdline(false, true)
280-
end, { desc = 'Reject cmdline completion' })
281-
end
282-
if M.config.mappings.complete then
283-
vim.keymap.set('c', M.config.mappings.complete, function()
284-
cmdline_changed()
285-
end, { desc = 'Force complete cmdline completion' })
286-
end
287-
if M.config.mappings.next then
288-
vim.keymap.set('c', M.config.mappings.next, function()
289-
state.completion.current = state.completion.current + 1
290-
update_cmdline()
291-
end, { desc = 'Next cmdline completion' })
292-
end
293-
if M.config.mappings.previous then
294-
vim.keymap.set('c', M.config.mappings.previous, function()
295-
state.completion.current = state.completion.current - 1
296-
update_cmdline()
297-
end, { desc = 'Previous cmdline completion' })
298-
end
299-
300-
local group = vim.api.nvim_create_augroup('CmdlineCompletion', {})
301-
vim.api.nvim_create_autocmd('CmdlineEnter', {
302-
desc = 'Setup cmdline completion handlers',
303-
group = group,
304-
callback = enter_handler,
305-
})
306-
vim.api.nvim_create_autocmd({ 'CmdlineLeave', 'CmdWinEnter' }, {
307-
desc = 'Tear down cmdline completion handlers',
308-
group = group,
309-
callback = leave_handler,
310-
})
31113
vim.api.nvim_create_autocmd('CmdlineChanged', {
312-
desc = 'Auto update cmdline completion',
313-
group = group,
314-
callback = changed_handler,
14+
pattern = ':',
15+
callback = function()
16+
local cmdline = vim.fn.getcmdline()
17+
local curpos = vim.fn.getcmdpos()
18+
local last_char = cmdline:sub(-1)
19+
20+
if
21+
curpos == #cmdline + 1
22+
and vim.fn.pumvisible() == 0
23+
and last_char:match('[%w%/%: ]')
24+
and not cmdline:match('^%d+$')
25+
then
26+
vim.opt.eventignore:append('CmdlineChanged')
27+
vim.api.nvim_feedkeys(term, 'ti', false)
28+
vim.schedule(function()
29+
vim.opt.eventignore:remove('CmdlineChanged')
30+
end)
31+
end
32+
end,
31533
})
31634
end
31735

0 commit comments

Comments
 (0)