Skip to content

Commit d40d4c4

Browse files
committed
WIP.
1 parent b5a366a commit d40d4c4

File tree

3 files changed

+284
-0
lines changed

3 files changed

+284
-0
lines changed

lua/minuet/config.lua

+11
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,17 @@ local M = {
133133
blink = {
134134
enable_auto_complete = true,
135135
},
136+
-- LSP is recommended only for built-in completion. If you are using
137+
-- `cmp` or `blink`, utilizing LSP for code completion from Minuet is *not*
138+
-- recommended.
139+
lsp = {
140+
enabled_ft = {},
141+
-- Filetypes excluded from LSP activation. Useful when `enabled_ft` = { '*' }
142+
disabled_ft = {},
143+
-- (Neovim v0.11+ only) If true, uses `vim.lsp.completion.enable` for
144+
-- automatic completion triggering.
145+
enable_auto_complete = false,
146+
},
136147
virtualtext = {
137148
-- Specify the filetypes to enable automatic virtual text completion,
138149
-- e.g., { 'python', 'lua' }. Note that you can still invoke manual

lua/minuet/lsp.lua

+241
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
local M = {}
2+
3+
local utils = require 'minuet.utils'
4+
5+
M.augroup = vim.api.nvim_create_augroup('MinuetLSP', { clear = true })
6+
7+
M.is_in_throttle = nil
8+
M.debounce_timer = nil
9+
10+
function M.get_trigger_characters()
11+
return { '@', '.', '(', '[', ':', ' ' }
12+
end
13+
14+
function M.get_capabilities()
15+
return {
16+
completionProvider = {
17+
triggerCharacters = M.get_trigger_characters(),
18+
},
19+
}
20+
end
21+
22+
function M.generate_request_id()
23+
return os.time()
24+
end
25+
26+
M.request_handler = {}
27+
28+
M.request_handler.initialize = function(_, _, callback, _)
29+
local id = M.generate_request_id()
30+
vim.schedule(function()
31+
callback(nil, { capabilities = M.get_capabilities() })
32+
end)
33+
return true, id
34+
end
35+
36+
-- We want to trigger the notification callback to explicitly mark the
37+
-- operation as complete. This ensures the "complete" event is dispatched and
38+
-- associated completion requests are no longer considered pending.
39+
M.request_handler['textDocument/completion'] = function(_, params, callback, notify_callback)
40+
local id = M.generate_request_id()
41+
local config = require('minuet').config
42+
43+
local function _complete()
44+
-- NOTE: builtin completion will accumulate completion items during
45+
-- multiple callbacks, So for each back we must ensure we only deliver
46+
-- new arrived completion items to avoid duplicated completion items.
47+
local delivered_completion_items = {}
48+
49+
if config.throttle > 0 then
50+
M.is_in_throttle = true
51+
vim.defer_fn(function()
52+
M.is_in_throttle = nil
53+
end, config.throttle)
54+
end
55+
local ctx = utils.make_cmp_context_from_lsp_params(params)
56+
57+
local context = utils.get_context(ctx)
58+
utils.notify('Minuet completion started', 'verbose')
59+
60+
local provider = require('minuet.backends.' .. config.provider)
61+
62+
provider.complete(context, function(data)
63+
if not data then
64+
callback(nil, { isIncomplete = false, items = {} })
65+
return
66+
end
67+
68+
-- HACK: workaround to address an undesired behavior: When using
69+
-- completion with the cursor positioned mid-word, the partial word
70+
-- under the cursor is erased.
71+
-- Example: Current cursor position:
72+
-- he|
73+
-- (| represents the cursor)
74+
-- If the completion item is "llo" and selected, "he" will be
75+
-- removed from the buffer. To resolve this, we will determine
76+
-- whether to prepend the last word to the completion items,
77+
-- avoiding the overwriting issue.
78+
79+
data = vim.tbl_map(function(item)
80+
return utils.prepend_to_complete_word(item, context.lines_before)
81+
end, data)
82+
83+
if config.add_single_line_entry then
84+
data = utils.add_single_line_entry(data)
85+
end
86+
87+
data = utils.list_dedup(data)
88+
89+
local new_data = {}
90+
91+
for _, item in ipairs(data) do
92+
if not delivered_completion_items[item] then
93+
table.insert(new_data, item)
94+
delivered_completion_items[item] = true
95+
end
96+
end
97+
98+
local max_label_width = 80
99+
100+
local multi_lines_indicators = ' [...]'
101+
102+
local items = {}
103+
for _, result in ipairs(new_data) do
104+
local item_lines = vim.split(result, '\n')
105+
local item_label
106+
107+
if #item_lines == 1 then
108+
item_label = result
109+
else
110+
item_label = vim.fn.strcharpart(item_lines[1], 0, max_label_width - #multi_lines_indicators)
111+
.. multi_lines_indicators
112+
end
113+
114+
table.insert(items, {
115+
label = item_label,
116+
insertText = result,
117+
-- insert, don't adjust indentation
118+
insertTextMode = 1,
119+
documentation = {
120+
kind = 'markdown',
121+
value = '```' .. (vim.bo.ft or '') .. '\n' .. result .. '\n```',
122+
},
123+
kind = vim.lsp.protocol.CompletionItemKind.Text,
124+
-- for nvim-cmp
125+
kind_text = config.provider_options[config.provider].name or config.provider,
126+
-- for blink-cmp
127+
kind_name = config.provider_options[config.provider].name or config.provider,
128+
})
129+
end
130+
131+
callback(nil, {
132+
isIncomplete = false,
133+
items = items,
134+
})
135+
136+
if notify_callback then
137+
notify_callback(id)
138+
end
139+
end)
140+
end
141+
142+
if config.throttle > 0 and M.is_in_throttle then
143+
vim.schedule(function()
144+
callback(nil, { isIncomplete = false, items = {} })
145+
if notify_callback then
146+
notify_callback(id)
147+
end
148+
end)
149+
end
150+
151+
if config.debounce > 0 then
152+
if M.debounce_timer and not M.debounce_timer:is_closing() then
153+
M.debounce_timer:stop()
154+
M.debounce_timer:close()
155+
end
156+
M.debounce_timer = vim.defer_fn(_complete, config.debounce)
157+
else
158+
_complete()
159+
end
160+
161+
return true, id
162+
end
163+
164+
M.request_handler.shutdown = function(_, _, callback, _)
165+
local id = M.generate_request_id()
166+
vim.schedule(function()
167+
callback(nil, nil)
168+
end)
169+
return true, id
170+
end
171+
172+
---@param dispatchers vim.lsp.rpc.Dispatchers
173+
---@return vim.lsp.rpc.PublicClient
174+
function M.server(dispatchers)
175+
local closing = false
176+
return {
177+
request = function(method, params, callback, notify_callback)
178+
if M.request_handler[method] then
179+
local ok, id = M.request_handler[method](method, params, callback, notify_callback)
180+
return ok, id
181+
else
182+
return false, nil
183+
end
184+
end,
185+
notify = function(method, _)
186+
if method == 'exit' then
187+
-- code 0 (success), signal 15 (SIGTERM)
188+
dispatchers.on_exit(0, 15)
189+
end
190+
return true
191+
end,
192+
is_closing = function()
193+
return closing
194+
end,
195+
terminate = function()
196+
closing = true
197+
end,
198+
}
199+
end
200+
201+
function M.start_server(args)
202+
---@type vim.lsp.ClientConfig
203+
local config = {
204+
name = 'minuet',
205+
cmd = M.server,
206+
on_attach = function(client, bufnr)
207+
if vim.fn.has 'nvim-0.11' == 1 then
208+
vim.lsp.completion.enable(true, client.id, bufnr, { autotrigger = true })
209+
end
210+
end,
211+
}
212+
---@type vim.lsp.start.Opts
213+
local opts = {
214+
bufnr = args.buf,
215+
reuse_client = function(lsp_client, lsp_config)
216+
return lsp_client.name == lsp_config.name
217+
end,
218+
}
219+
vim.lsp.start(config, opts)
220+
end
221+
222+
function M.setup()
223+
local config = require('minuet').config
224+
vim.api.nvim_clear_autocmds { group = M.augroup }
225+
226+
if #config.lsp.enabled_ft > 0 then
227+
vim.api.nvim_create_autocmd('FileType', {
228+
pattern = config.lsp.enabled_ft,
229+
callback = function(args)
230+
if vim.tbl_contains(config.lsp.disabled_ft, vim.b[args.buf].ft) then
231+
return
232+
end
233+
M.start_server(args)
234+
end,
235+
desc = 'Starts the minuet LSP server',
236+
group = M.augroup,
237+
})
238+
end
239+
end
240+
241+
return M

lua/minuet/utils.lua

+32
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,38 @@ function M.make_cmp_context(blink_context)
177177
return self
178178
end
179179

180+
---@class LSPPositionParams
181+
---@field context {triggerKind: number}
182+
---@field position {character: number, line: number}
183+
---@field textDocument {uri: string}
184+
185+
---@param params LSPPositionParams
186+
function M.make_cmp_context_from_lsp_params(params)
187+
local bufnr
188+
local self = {}
189+
if params.textDocument.uri == 'file://' then
190+
bufnr = 0
191+
else
192+
bufnr = vim.uri_to_bufnr(params.textDocument.uri)
193+
end
194+
195+
local row = params.position.line
196+
local col = math.max(params.position.character - 1, 0)
197+
self.cursor = {
198+
row = row,
199+
line = row,
200+
col = col,
201+
}
202+
203+
local current_line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or ''
204+
local cursor_before_line = vim.fn.strcharpart(current_line, 0, col)
205+
local cursor_after_line = vim.fn.strcharpart(current_line, col)
206+
207+
self.cursor_before_line = cursor_before_line
208+
self.cursor_after_line = cursor_after_line
209+
return self
210+
end
211+
180212
--- Get the context around the cursor position for code completion
181213
---@param cmp_context table The completion context object containing cursor position and line info
182214
---@return table Context information with the following fields:

0 commit comments

Comments
 (0)