forked from Exafunction/windsurf.nvim
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvirtual_text.lua
More file actions
554 lines (468 loc) · 13.9 KB
/
virtual_text.lua
File metadata and controls
554 lines (468 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
local config = require("codeium.config")
local enums = require("codeium.enums")
local notify = require("codeium.notify")
local util = require("codeium.util")
local M = {}
local hlgroup = "CodeiumSuggestion"
local request_nonce = 0
local using_status_line = false
--- @type "idle" | "waiting" | "completions"
local codeium_status = "idle"
--- @class Completions
--- @field items table[] | nil
--- @field index number | nil
--- @field cancel function
--- @field request_id number
--- @field request_data table
--- @type Completions | nil
local completions
local idle_timer
local server = {
--- This will be replaced by the actual server when setup is called.
is_healthy = function()
return false
end,
}
function M.setup(_server)
server = _server
local augroup = vim.api.nvim_create_augroup("codeium_virtual_text", { clear = true })
if not config.options.virtual_text.enabled then
return
end
vim.api.nvim_create_autocmd({ "InsertEnter", "CursorMovedI", "CompleteChanged" }, {
group = augroup,
callback = function()
M.debounced_complete()
end,
})
vim.api.nvim_create_autocmd("BufEnter", {
group = augroup,
callback = function()
if vim.fn.mode():match("^[iR]") then
M.debounced_complete()
end
end,
})
vim.api.nvim_create_autocmd("InsertLeave", {
group = augroup,
callback = function()
M.clear()
end,
})
vim.api.nvim_create_autocmd("BufLeave", {
group = augroup,
callback = function()
if vim.fn.mode():match("^[iR]") then
M.clear()
end
end,
})
if config.options.virtual_text.map_keys then
local bindings = config.options.virtual_text.key_bindings
if bindings.clear and bindings.clear ~= "" then
vim.keymap.set("i", bindings.clear, function()
M.clear()
end, { silent = true })
end
if bindings.next and bindings.next ~= "" then
vim.keymap.set("i", bindings.next, function()
M.cycle_completions(1)
end, { silent = true })
end
if bindings.prev and bindings.prev ~= "" then
vim.keymap.set("i", bindings.prev, function()
M.cycle_completions(-1)
end, { silent = true })
end
if bindings.accept and bindings.accept ~= "" then
vim.keymap.set("i", bindings.accept, M.accept, { silent = true, expr = true, script = true, nowait = true })
end
if bindings.accept_word and bindings.accept_word ~= "" then
vim.keymap.set(
"i",
bindings.accept_word,
M.accept_next_word,
{ silent = true, expr = true, script = true, nowait = true }
)
end
if bindings.accept_line and bindings.accept_line ~= "" then
vim.keymap.set(
"i",
bindings.accept_line,
M.accept_next_line,
{ silent = true, expr = true, script = true, nowait = true }
)
end
end
vim.api.nvim_create_autocmd({ "ColorScheme", "VimEnter" }, {
group = augroup,
callback = function()
M.set_style()
end,
})
end
function M.set_style()
if vim.fn.has("termguicolors") == 1 and vim.o.termguicolors then
vim.api.nvim_set_hl(0, hlgroup, { fg = "#808080", default = true })
else
vim.api.nvim_set_hl(0, hlgroup, { ctermfg = 244, default = true })
end
end
function M.get_completion_text()
local completion_text = M.completion_text
M.completion_text = nil
return completion_text or ""
end
local function completion_inserter(current_completion, insert_text)
local default = config.options.virtual_text.accept_fallback or (vim.fn.pumvisible() == 1 and "<C-N>" or "<Tab>")
if not (vim.fn.mode():match("^[iR]")) then
return default
end
if current_completion == nil then
return default
end
local range = current_completion.range
local suffix = current_completion.suffix or {}
local suffix_text = suffix.text or ""
local delta = suffix.deltaCursorOffset or 0
local start_offset = range.startOffset or 0
local end_offset = range.endOffset or 0
local text = insert_text .. suffix_text
if text == "" then
return default
end
local delete_range = ""
if end_offset - start_offset > 0 then
local delete_bytes = end_offset - start_offset
local delete_chars = vim.fn.strchars(vim.fn.strpart(vim.fn.getline("."), 0, delete_bytes))
delete_range = ' <Esc>"_x0"_d' .. delete_chars .. "li"
end
local insert_text = '<C-R><C-O>=v:lua.require("codeium.virtual_text").get_completion_text()<CR>'
M.completion_text = text
local cursor_text = delta == 0 and "" or '<C-O>:exe "go" line2byte(line("."))+col(".")+(' .. delta .. ")<CR>"
server:accept_completion(current_completion.completion.completionId)
return '<C-g>u' .. delete_range .. insert_text .. cursor_text
end
function M.accept()
local current_completion = M.get_current_completion_item()
return completion_inserter(current_completion, current_completion and current_completion.completion.text or "")
end
function M.accept_next_word()
local current_completion = M.get_current_completion_item()
local completion_parts = current_completion and (current_completion.completionParts or {}) or {}
if #completion_parts == 0 then
return ""
end
local prefix_text = completion_parts[1].prefix or ""
local completion_text = completion_parts[1].text or ""
local next_word = completion_text:match("^%W*%w*")
return completion_inserter(current_completion, prefix_text .. next_word)
end
function M.accept_next_line()
local current_completion = M.get_current_completion_item()
local text = current_completion and current_completion.completion.text:gsub("\n.*$", "") or ""
return completion_inserter(current_completion, text)
end
function M.get_current_completion_item()
if completions and completions.items and completions.index and completions.index < #completions.items then
return completions.items[completions.index + 1]
end
return nil
end
local nvim_extmark_ids = {}
local function clear_completion()
local namespace = vim.api.nvim_create_namespace("codeium")
for _, id in ipairs(nvim_extmark_ids) do
vim.api.nvim_buf_del_extmark(0, namespace, id)
end
nvim_extmark_ids = {}
end
local function render_current_completion()
clear_completion()
M.redraw_status_line()
if not vim.fn.mode():match("^[iR]") then
return ""
end
local current_completion = M.get_current_completion_item()
if current_completion == nil then
return ""
end
local parts = current_completion.completionParts or {}
local inline_cumulative_cols = 0
local diff = 0
for idx, part in ipairs(parts) do
local row = (part.line or 0) + 1
if row ~= vim.fn.line(".") then
notify.debug("Ignoring completion, line number is not the current line.")
goto continue
end
local _col
if part.type == "COMPLETION_PART_TYPE_INLINE" then
_col = inline_cumulative_cols + #(part.prefix or "") + 1
inline_cumulative_cols = _col - 1
else
_col = #(part.prefix or "") + 1
end
local text = part.text
if
(part.type == "COMPLETION_PART_TYPE_INLINE" and idx == 1)
or part.type == "COMPLETION_PART_TYPE_INLINE_MASK"
then
local completion_prefix = part.prefix or ""
local completion_line = completion_prefix .. text
local full_line = vim.fn.getline(row)
local cursor_prefix = full_line:sub(1, vim.fn.col(".") - 1)
local matching_prefix = 0
for i = 1, #completion_line do
if i <= #full_line and completion_line:sub(i, i) == full_line:sub(i, i) then
matching_prefix = matching_prefix + 1
else
break
end
end
if #cursor_prefix > #completion_prefix then
diff = #cursor_prefix - #completion_prefix
elseif #cursor_prefix < #completion_prefix then
if matching_prefix >= #completion_prefix then
diff = matching_prefix - #completion_prefix
else
diff = #cursor_prefix - #completion_prefix
end
end
if diff > 0 then
diff = 0
end
if diff < 0 then
text = completion_prefix:sub(diff + 1) .. text
elseif diff > 0 then
text = text:sub(diff + 1)
end
end
local priority = config.options.virtual_text.virtual_text_priority
local _virtcol = vim.fn.virtcol({ row, _col + diff })
local data = { id = idx + 1, hl_mode = "combine", virt_text_win_col = _virtcol - 1, priority = priority }
if part.type == "COMPLETION_PART_TYPE_INLINE_MASK" then
data.virt_text = { { text, hlgroup } }
elseif part.type == "COMPLETION_PART_TYPE_BLOCK" then
local lines = vim.split(text, "\n")
if lines[#lines] == "" then
table.remove(lines)
end
data.virt_lines = vim.tbl_map(function(l)
return { { l, hlgroup } }
end, lines)
else
goto continue
end
table.insert(nvim_extmark_ids, data.id)
vim.api.nvim_buf_set_extmark(0, vim.api.nvim_create_namespace("codeium"), row - 1, 0, data)
::continue::
end
end
function M.clear()
codeium_status = "idle"
M.redraw_status_line()
if idle_timer then
vim.fn.timer_stop(idle_timer)
idle_timer = nil
end
if completions then
if completions.cancel then
completions.cancel()
end
render_current_completion()
completions = nil
end
render_current_completion()
return ""
end
--- @param n number
function M.cycle_completions(n)
if not completions or M.get_current_completion_item() == nil then
return
end
completions.index = completions.index + n
local n_items = #completions.items
if completions.index < 0 then
completions.index = completions.index + n_items
end
completions.index = completions.index % n_items
render_current_completion()
end
local warn_filetype_missing = true
--- @param buf_id number
--- @param cur_line number
--- @param cur_col number
--- @return table | nil
local function get_document(buf_id, cur_line, cur_col)
local lines = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
if vim.bo[buf_id].eol then
table.insert(lines, "")
end
local filetype = vim.bo[buf_id].filetype:gsub("%..*", "")
local language = enums.filetype_aliases[filetype == "" and "text" or filetype] or filetype
if filetype == "" and warn_filetype_missing ~= false then
notify.debug("No filetype detected. This will affect completion quality.")
warn_filetype_missing = false
end
local editor_language = vim.bo[buf_id].filetype == "" and "unspecified" or vim.bo[buf_id].filetype
local buf_name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf_id), ":p")
-- If it's already any sort of URI, this might be a special buffer for some plugins, so we ignore it to
-- avoid an LS error.
if buf_name:match("^%w+://") ~= nil then
return nil
end
local line_ending = util.get_newline(buf_id)
local doc = {
text = table.concat(lines, line_ending),
editor_language = editor_language,
language = enums.languages[language] or enums.languages.unspecified,
cursor_position = { row = cur_line - 1, col = cur_col - 1 },
absolute_uri = util.get_uri(vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf_id), ":p")),
workspace_uri = util.get_uri(util.get_project_root()),
line_ending = line_ending,
}
return doc
end
--- @param opts { bufnr: number, timer: any }?
function M.complete(opts)
if opts then
if opts.timer ~= idle_timer then
return
end
idle_timer = nil
if vim.fn.mode() ~= "i" or opts.bufnr ~= vim.fn.bufnr("") then
return
end
end
if idle_timer then
vim.fn.timer_stop(idle_timer)
idle_timer = nil
end
if vim.o.encoding ~= "latin1" and vim.o.encoding ~= "utf-8" then
error("Only latin1 and utf-8 are supported")
return
end
local bufnr = vim.fn.bufnr("")
if not M.filetype_enabled(bufnr) then
return
end
local document = get_document(bufnr, vim.fn.line("."), vim.fn.col("."))
if document == nil then
return
end
local other_documents = util.get_other_documents(bufnr)
local data = {
document = document,
editor_options = util.get_editor_options(bufnr),
other_documents = other_documents,
}
if completions and completions.request_data == data then
return
end
local request_data = vim.deepcopy(data)
request_nonce = request_nonce + 1
local request_id = request_nonce
codeium_status = "waiting"
local cancel = server:request_completion(
data.document,
data.editor_options,
data.other_documents,
function(success, json)
if completions and completions.request_id == request_id then
completions.cancel = nil
codeium_status = "idle"
end
if not success then
return
end
if json and json.state and json.state.state == "CODEIUM_STATE_SUCCESS" and json.completionItems then
M.handle_completions(json.completionItems)
end
end
)
completions = {
cancel = cancel,
request_data = request_data,
request_id = request_id,
}
end
function M.handle_completions(completion_items)
if not completions then
return
end
completions.items = completion_items
completions.index = 0
codeium_status = "completions"
render_current_completion()
end
function M.filetype_enabled(bufnr)
local filetype = vim.bo[bufnr].filetype
local enabled = config.options.virtual_text.filetypes[filetype]
if enabled == nil then
return config.options.virtual_text.default_filetype_enabled
end
return enabled
end
function M.debounced_complete()
M.clear()
if config.options.virtual_text.manual or not server:is_healthy() or not M.filetype_enabled(vim.fn.bufnr("")) then
return
end
local current_buf = vim.fn.bufnr("")
idle_timer = vim.fn.timer_start(config.options.virtual_text.idle_delay, function(timer)
M.complete({ bufnr = current_buf, timer = timer })
end)
end
function M.cycle_or_complete()
if M.get_current_completion_item() == nil then
M.complete()
else
M.cycle_completions(1)
end
end
function M.status()
if codeium_status == "completions" then
if completions and completions.items and completions.index then
return {
state = "completions",
current = completions.index + 1,
total = #completions.items,
}
else
return { state = "idle" }
end
else
return { state = codeium_status }
end
end
function M.status_string()
using_status_line = true
local status = M.status()
if status.state == "completions" then
if status.total > 0 then
return string.format("%d/%d", status.current, status.total)
else
return " 0 "
end
elseif status.state == "waiting" then
return " * "
elseif status.state == "idle" then
return " 0 "
else
return " "
end
end
local refresh_fn = function()
vim.cmd("redrawstatus")
end
function M.set_statusbar_refresh(refresh)
using_status_line = true
refresh_fn = refresh
end
function M.redraw_status_line()
if using_status_line then
refresh_fn()
end
end
return M