Skip to content

Commit 215b341

Browse files
committed
WIP.
1 parent b5a366a commit 215b341

File tree

3 files changed

+296
-0
lines changed

3 files changed

+296
-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

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