Skip to content

Commit 3675278

Browse files
committed
WIP.
1 parent b5a366a commit 3675278

File tree

4 files changed

+325
-0
lines changed

4 files changed

+325
-0
lines changed

lua/minuet/config.lua

+16
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,22 @@ 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+
-- uses `vim.lsp.completion.enable` for automatic completion
144+
-- triggering.
145+
enabled_auto_trigger_ft = {},
146+
-- Filetypes excluded from autotriggering. Useful when `enabled_auto_trigger_ft` = { '*' }
147+
disabled_auto_trigger_ft = {},
148+
-- if true, when the user is using blink or nvim-cmp, warn the user
149+
-- that they should use the native source instead.
150+
warn_on_blink_or_cmp = true,
151+
},
136152
virtualtext = {
137153
-- Specify the filetypes to enable automatic virtual text completion,
138154
-- e.g., { 'python', 'lua' }. Note that you can still invoke manual

lua/minuet/init.lua

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ function M.setup(config)
1616
end
1717

1818
require('minuet.virtualtext').setup()
19+
require('minuet.lsp').setup()
1920
require 'minuet.deprecate'
2021
end
2122

lua/minuet/lsp.lua

+276
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
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+
if config.add_single_line_entry then
73+
data = utils.add_single_line_entry(data)
74+
end
75+
76+
data = utils.list_dedup(data)
77+
78+
local new_data = {}
79+
80+
for _, item in ipairs(data) do
81+
if not delivered_completion_items[item] then
82+
table.insert(new_data, item)
83+
delivered_completion_items[item] = true
84+
end
85+
end
86+
87+
local max_label_width = 80
88+
89+
local multi_lines_indicators = ' [...]'
90+
91+
local items = {}
92+
for _, result in ipairs(new_data) do
93+
local item_lines = vim.split(result, '\n')
94+
local item_label
95+
96+
if #item_lines == 1 then
97+
item_label = result
98+
else
99+
item_label = vim.fn.strcharpart(item_lines[1], 0, max_label_width - #multi_lines_indicators)
100+
.. multi_lines_indicators
101+
end
102+
103+
table.insert(items, {
104+
label = item_label,
105+
insertText = result,
106+
-- insert, don't adjust indentation
107+
insertTextMode = 1,
108+
documentation = {
109+
kind = 'markdown',
110+
value = '```' .. (vim.bo.ft or '') .. '\n' .. result .. '\n```',
111+
},
112+
kind = vim.lsp.protocol.CompletionItemKind.Text,
113+
-- for nvim-cmp
114+
kind_text = config.provider_options[config.provider].name or config.provider,
115+
kind_hl = 'LspKindMinuet',
116+
-- for blink-cmp
117+
kind_name = config.provider_options[config.provider].name or config.provider,
118+
kind_hl_group = 'LspKindMinuet',
119+
})
120+
end
121+
122+
callback(nil, {
123+
isIncomplete = false,
124+
items = items,
125+
})
126+
127+
if notify_callback then
128+
notify_callback(id)
129+
end
130+
end)
131+
end
132+
133+
if config.throttle > 0 and M.is_in_throttle then
134+
vim.schedule(function()
135+
callback(nil, { isIncomplete = false, items = {} })
136+
if notify_callback then
137+
notify_callback(id)
138+
end
139+
end)
140+
end
141+
142+
if config.debounce > 0 then
143+
if M.debounce_timer and not M.debounce_timer:is_closing() then
144+
M.debounce_timer:stop()
145+
M.debounce_timer:close()
146+
end
147+
M.debounce_timer = vim.defer_fn(_complete, config.debounce)
148+
else
149+
_complete()
150+
end
151+
152+
return true, id
153+
end
154+
155+
M.request_handler.shutdown = function(_, _, callback, _)
156+
local id = M.generate_request_id()
157+
vim.schedule(function()
158+
callback(nil, nil)
159+
end)
160+
return true, id
161+
end
162+
163+
---@param dispatchers vim.lsp.rpc.Dispatchers
164+
---@return vim.lsp.rpc.PublicClient
165+
function M.server(dispatchers)
166+
local closing = false
167+
return {
168+
request = function(method, params, callback, notify_callback)
169+
if M.request_handler[method] then
170+
local ok, id = M.request_handler[method](method, params, callback, notify_callback)
171+
return ok, id
172+
else
173+
return false, nil
174+
end
175+
end,
176+
notify = function(method, _)
177+
if method == 'exit' then
178+
-- code 0 (success), signal 15 (SIGTERM)
179+
dispatchers.on_exit(0, 15)
180+
end
181+
return true
182+
end,
183+
is_closing = function()
184+
return closing
185+
end,
186+
terminate = function()
187+
closing = true
188+
end,
189+
}
190+
end
191+
192+
function M.start_server(args)
193+
if vim.fn.has 'nvim-0.11' == 0 then
194+
vim.notify('minuet LSP requires nvim-0.11+', vim.log.levels.WARN)
195+
return
196+
end
197+
198+
---@type vim.lsp.ClientConfig
199+
local config = {
200+
name = 'minuet',
201+
cmd = M.server,
202+
on_attach = function(client, bufnr)
203+
local config = require('minuet').config
204+
local auto_trigger_ft = config.lsp.enabled_auto_trigger_ft
205+
local disable_trigger_ft = config.lsp.disabled_auto_trigger_ft
206+
local ft = vim.bo[bufnr].filetype
207+
208+
utils.notify('Minuet LSP attached to current buffer', 'verbose', vim.log.levels.INFO)
209+
210+
if
211+
(vim.tbl_contains(auto_trigger_ft, ft) or vim.tbl_contains(auto_trigger_ft, '*'))
212+
and not vim.tbl_contains(disable_trigger_ft, ft)
213+
then
214+
vim.lsp.completion.enable(true, client.id, bufnr, { autotrigger = true })
215+
utils.notify('Minuet LSP is enabled for auto triggering', 'verbose', vim.log.levels.INFO)
216+
else
217+
vim.defer_fn(function()
218+
-- NOTE: Auto-triggering is explicitly disabled for
219+
-- filetypes that are not enabled auto triggering. This is
220+
-- because some users uses the `LspAttach` event to
221+
-- determine if a LSP supports completion, then enabling
222+
-- auto-triggering if it does.
223+
--
224+
-- Minuet, as a LLM completion source, can be subject to
225+
-- substantial rate limits during auto-triggering.
226+
-- Therefore, completion is disabled by default unless
227+
-- explicitly enabled by the user.
228+
vim.lsp.completion.enable(false, client.id, bufnr)
229+
utils.notify('Minuet LSP is disabled for auto triggering', 'verbose', vim.log.levels.INFO)
230+
end, 200)
231+
end
232+
end,
233+
}
234+
---@type vim.lsp.start.Opts
235+
local opts = {
236+
bufnr = args.buf,
237+
reuse_client = function(lsp_client, lsp_config)
238+
return lsp_client.name == lsp_config.name
239+
end,
240+
}
241+
vim.lsp.start(config, opts)
242+
end
243+
244+
function M.setup()
245+
local config = require('minuet').config
246+
247+
local has_cmp = pcall(require, 'cmp')
248+
local has_blink = pcall(require, 'blink-cmp')
249+
250+
if (has_cmp or has_blink) and (#config.lsp.enabled_ft > 0) and config.lsp.warn_on_blink_or_cmp then
251+
vim.notify(
252+
'Blink or Nvim-cmp detected, it is recommended to use the native source instead of lsp',
253+
vim.log.levels.WARN
254+
)
255+
return
256+
end
257+
258+
vim.api.nvim_clear_autocmds { group = M.augroup }
259+
260+
if #config.lsp.enabled_ft > 0 then
261+
vim.api.nvim_create_autocmd('FileType', {
262+
pattern = config.lsp.enabled_ft,
263+
callback = function(args)
264+
if vim.tbl_contains(config.lsp.disabled_ft, vim.b[args.buf].ft) then
265+
return
266+
end
267+
268+
M.start_server(args)
269+
end,
270+
desc = 'Starts the minuet LSP server',
271+
group = M.augroup,
272+
})
273+
end
274+
end
275+
276+
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)