Skip to content

Commit d161421

Browse files
committed
WIP.
1 parent b5a366a commit d161421

File tree

3 files changed

+250
-0
lines changed

3 files changed

+250
-0
lines changed

lua/minuet/config.lua

+8
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ local M = {
133133
blink = {
134134
enable_auto_complete = true,
135135
},
136+
lsp = {
137+
-- Whether start the in-process lsp to provide completion.
138+
-- When `enabled = true`, manual completion will always be available.
139+
enabled = false,
140+
-- Enable or disable auto-completion. Note that this option is only
141+
-- effective when `enabled = true`.
142+
enable_auto_complete = false,
143+
},
136144
virtualtext = {
137145
-- Specify the filetypes to enable automatic virtual text completion,
138146
-- e.g., { 'python', 'lua' }. Note that you can still invoke manual

lua/minuet/lsp.lua

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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 = 60
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+
details = result,
118+
kind = config.provider_options[config.provider].name or config.provider,
119+
-- for nvim-cmp
120+
kind_text = config.provider_options[config.provider].name or config.provider,
121+
-- for blink-cmp
122+
kind_name = config.provider_options[config.provider].name or config.provider,
123+
})
124+
end
125+
126+
callback {
127+
isIncomplete = false,
128+
items = items,
129+
}
130+
131+
if notify_callback then
132+
notify_callback(id)
133+
end
134+
end)
135+
end
136+
137+
if config.throttle > 0 and M.is_in_throttle then
138+
vim.schedule(function()
139+
callback(nil, { isIncomplete = false, items = {} })
140+
if notify_callback then
141+
notify_callback(id)
142+
end
143+
end)
144+
end
145+
146+
if config.debounce > 0 then
147+
if M.debounce_timer and not M.debounce_timer:is_closing() then
148+
M.debounce_timer:stop()
149+
M.debounce_timer:close()
150+
end
151+
M.debounce_timer = vim.defer_fn(_complete, config.debounce)
152+
else
153+
_complete()
154+
end
155+
156+
return true, id
157+
end
158+
159+
M.request_handler.shutdown = function(_, _, callback, _)
160+
local id = M.generate_request_id()
161+
vim.schedule(function()
162+
callback(nil, nil)
163+
end)
164+
return true, id
165+
end
166+
167+
---@param dispatchers vim.lsp.rpc.Dispatchers
168+
---@return vim.lsp.rpc.PublicClient
169+
function M.server(dispatchers)
170+
local closing = false
171+
return {
172+
request = function(method, params, callback, notify_callback)
173+
if M.request_handler[method] then
174+
local ok, id = M.request_handler[method](method, params, callback, notify_callback)
175+
return ok, id
176+
else
177+
return false, nil
178+
end
179+
end,
180+
notify = function(method, _)
181+
if method == 'exit' then
182+
-- code 0 (success), signal 15 (SIGTERM)
183+
dispatchers.on_exit(0, 15)
184+
end
185+
return true
186+
end,
187+
is_closing = function()
188+
return closing
189+
end,
190+
terminate = function()
191+
closing = true
192+
end,
193+
}
194+
end
195+
196+
function M.start_server(args)
197+
---@type vim.lsp.ClientConfig
198+
local config = {
199+
name = 'minuet',
200+
cmd = M.server,
201+
}
202+
---@type vim.lsp.start.Opts
203+
local opts = {
204+
bufnr = args.buf,
205+
reuse_client = function(lsp_client, lsp_config)
206+
return lsp_client.name == lsp_config.name
207+
end,
208+
}
209+
vim.lsp.start(config, opts)
210+
end
211+
212+
function M.setup()
213+
vim.api.nvim_create_autocmd('FileType', {
214+
pattern = '*',
215+
callback = M.start_server,
216+
desc = 'Starts the minuet LSP server',
217+
group = M.augroup,
218+
})
219+
end
220+
221+
return M

lua/minuet/utils.lua

+21
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,27 @@ 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 = params.vim.uri_to_bufnr(params.textDocument.uri)
193+
end
194+
self.cursor = {
195+
row = params.position.line,
196+
col = params.position.character + 1,
197+
}
198+
return self
199+
end
200+
180201
--- Get the context around the cursor position for code completion
181202
---@param cmp_context table The completion context object containing cursor position and line info
182203
---@return table Context information with the following fields:

0 commit comments

Comments
 (0)