-
Notifications
You must be signed in to change notification settings - Fork 151
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
593 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
local M = {} | ||
|
||
M.kind_icons = { | ||
Text = '', | ||
Method = '', | ||
Function = '', | ||
Constructor = '', | ||
|
||
Field = '', | ||
Variable = '', | ||
Property = '', | ||
|
||
Class = '', | ||
Interface = '', | ||
Struct = '', | ||
Module = '', | ||
|
||
Unit = '', | ||
Value = '', | ||
Enum = '', | ||
EnumMember = '', | ||
|
||
Keyword = '', | ||
Constant = '', | ||
|
||
Snippet = '', | ||
Color = '', | ||
File = '', | ||
Reference = '', | ||
Folder = '', | ||
Event = '', | ||
Operator = '', | ||
TypeParameter = '', | ||
} | ||
M.filtered_items = {} | ||
|
||
M.fuzzy = require('blink.fuzzy').fuzzy | ||
M.lsp = require('blink.cmp.lsp') | ||
|
||
M.accept = function(cmp_win) | ||
local bufnr = vim.api.nvim_get_current_buf() | ||
local current_line, start_col, end_col = M.get_query_to_replace(bufnr) | ||
|
||
-- Get the item from the filtered items based on the cursorline position | ||
local item = M.filtered_items[vim.api.nvim_win_get_cursor(cmp_win.id)[1]] | ||
|
||
-- Apply text edit | ||
vim.api.nvim_buf_set_text(bufnr, current_line, start_col, current_line, end_col, { item.word }) | ||
vim.api.nvim_win_set_cursor(0, { current_line + 1, start_col + #item.word }) | ||
|
||
-- Apply additional text edits | ||
-- LSPs can either include these in the initial response or require a resolve | ||
-- These are used for things like auto-imports | ||
-- todo: check capabilities to know ahead of time | ||
if item.additionalTextEdits ~= nil then | ||
M.apply_additional_text_edits(item.client_id, item) | ||
else | ||
M.lsp.resolve(item, function(client_id, resolved_item) M.apply_additional_text_edits(client_id, resolved_item) end) | ||
end | ||
end | ||
|
||
M.select_next = function(cmp_win) | ||
local current_line = vim.api.nvim_win_get_cursor(cmp_win.id)[1] | ||
vim.api.nvim_win_set_cursor(cmp_win.id, { current_line + 1, 0 }) | ||
end | ||
|
||
M.select_prev = function(cmp_win) | ||
local current_line = vim.api.nvim_win_get_cursor(cmp_win.id)[1] | ||
vim.api.nvim_win_set_cursor(cmp_win.id, { current_line - 1, 0 }) | ||
end | ||
|
||
M.update = function(cmp_win, doc_win, items, opts) | ||
local start_time = vim.loop.hrtime() | ||
|
||
local query = M.get_query() | ||
|
||
-- get the items based on the user's query | ||
local filtered_items = M.filter_items(query, items) | ||
|
||
-- guards for cases where we shouldn't show the completion window | ||
local no_items = #filtered_items == 0 | ||
local is_exact_match = #filtered_items == 1 and filtered_items[1].word == query and opts.force ~= true | ||
local not_in_insert = vim.api.nvim_get_mode().mode ~= 'i' | ||
local no_query = query == '' and opts.force ~= true | ||
if no_items or is_exact_match or not_in_insert or no_query then | ||
cmp_win:close() | ||
doc_win:close() | ||
return | ||
end | ||
cmp_win:open() | ||
|
||
-- update completion window | ||
vim.api.nvim_buf_set_lines(cmp_win.buf, 0, -1, true, {}) | ||
for idx, item in ipairs(filtered_items) do | ||
local max_length = 40 | ||
local kind_hl = 'CmpItemKind' .. item.kind | ||
local kind_icon = M.kind_icons[item.kind] or M.kind_icons.Field | ||
local kind = item.kind | ||
|
||
local utf8len = vim.fn.strdisplaywidth | ||
local other_content_length = utf8len(kind_icon) + utf8len(kind) + 5 | ||
local remaining_length = math.max(0, max_length - other_content_length - utf8len(item.abbr)) | ||
local abbr = string.sub(item.abbr, 1, max_length - other_content_length) .. string.rep(' ', remaining_length) | ||
|
||
local line = string.format(' %s %s %s ', kind_icon, abbr, kind) | ||
vim.api.nvim_buf_set_lines(cmp_win.buf, idx - 1, idx, false, { line }) | ||
vim.api.nvim_buf_add_highlight(cmp_win.buf, -1, kind_hl, idx - 1, 0, #kind_icon + 2) | ||
|
||
if idx > cmp_win.config.max_height then break end | ||
end | ||
|
||
-- set height | ||
vim.api.nvim_win_set_height(cmp_win.id, math.min(#filtered_items, cmp_win.config.max_height)) | ||
|
||
-- select first line | ||
vim.api.nvim_win_set_cursor(cmp_win.id, { 1, 0 }) | ||
|
||
-- documentation | ||
local first_item = filtered_items[1] | ||
if first_item ~= nil then | ||
M.lsp.resolve(first_item, function(_, resolved_item) | ||
if resolved_item.detail == nil then | ||
doc_win:close() | ||
return | ||
end | ||
local doc_lines = {} | ||
for s in resolved_item.detail:gmatch('[^\r\n]+') do | ||
table.insert(doc_lines, s) | ||
end | ||
vim.api.nvim_buf_set_lines(doc_win.buf, 0, -1, true, doc_lines) | ||
doc_win:open() | ||
end) | ||
end | ||
|
||
M.filtered_items = filtered_items | ||
|
||
local end_time = vim.loop.hrtime() | ||
local time_in_ms = (end_time - start_time) / 1e6 | ||
print('Time taken to filter ' .. #items .. ' completions: ' .. time_in_ms .. 'ms') | ||
end | ||
|
||
---------- UTILS ------------ | ||
|
||
M.filter_items = function(query, items) | ||
if query == '' then return items end | ||
|
||
-- convert to table of strings | ||
local words = {} | ||
for _, item in ipairs(items) do | ||
table.insert(words, item.word) | ||
end | ||
|
||
-- perform fuzzy search | ||
local filtered_items = {} | ||
local selected_indices = M.fuzzy(query, words) | ||
for _, selected_index in ipairs(selected_indices) do | ||
table.insert(filtered_items, items[selected_index + 1]) | ||
end | ||
|
||
return filtered_items | ||
end | ||
|
||
M.get_query = function() | ||
local bufnr = vim.api.nvim_get_current_buf() | ||
local current_line = vim.api.nvim_win_get_cursor(0)[1] - 1 | ||
local current_col = vim.api.nvim_win_get_cursor(0)[2] - 1 | ||
local line = vim.api.nvim_buf_get_lines(bufnr, current_line, current_line + 1, false)[1] | ||
local query = string.sub(line, 1, current_col + 1):match('[%w_\\-]+$') or '' | ||
return query | ||
end | ||
|
||
M.get_query_to_replace = function(bufnr) | ||
local current_line = vim.api.nvim_win_get_cursor(0)[1] | ||
local current_col = vim.api.nvim_win_get_cursor(0)[2] + 1 | ||
local line = vim.api.nvim_buf_get_lines(bufnr, current_line - 1, current_line, false)[1] | ||
|
||
-- Search forward/backward for the start/end of the word | ||
local start_col = current_col | ||
while start_col > 1 do | ||
local char = line:sub(start_col - 1, start_col - 1) | ||
if char:match('[%w_\\-]') == nil then break end | ||
start_col = start_col - 1 | ||
end | ||
|
||
local end_col = current_col | ||
while end_col < #line do | ||
local char = line:sub(end_col + 1, end_col + 1) | ||
if char:match('[%w_\\-]') == nil then break end | ||
end_col = end_col + 1 | ||
end | ||
|
||
-- convert to 0-index | ||
return current_line - 1, start_col - 1, end_col - 1 | ||
end | ||
|
||
M.apply_additional_text_edits = function(client_id, item) M.apply_text_edits(client_id, item.additionalTextEdits or {}) end | ||
|
||
M.apply_text_edits = function(client_id, edits) | ||
local offset_encoding = vim.lsp.get_client_by_id(client_id).offset_encoding | ||
vim.lsp.util.apply_text_edits(edits, vim.api.nvim_get_current_buf(), offset_encoding) | ||
end | ||
|
||
return M |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
local M = {} | ||
|
||
M.items = {} | ||
|
||
M.setup = function(config) | ||
M.cmp_win = require('blink.cmp.win').new({ | ||
cursorline = true, | ||
winhighlight = 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None', | ||
}) | ||
M.doc_win = require('blink.cmp.win').new({ | ||
width = 60, | ||
max_height = 20, | ||
relative = M.cmp_win, | ||
wrap = true, | ||
filetype = 'typescript', -- todo: set dynamically | ||
padding = true, | ||
}) | ||
M.lsp = require('blink.cmp.lsp') | ||
M.cmp = require('blink.cmp.cmp') | ||
|
||
local last_char = '' | ||
vim.api.nvim_create_autocmd('InsertCharPre', { | ||
callback = function() last_char = vim.v.char end, | ||
}) | ||
|
||
-- decide if we should show the completion window | ||
vim.api.nvim_create_autocmd('TextChangedI', { | ||
callback = function() | ||
if M.cmp_win.id ~= nil then return end | ||
-- todo: if went from prefix to no prefix, clear the items | ||
if last_char ~= '' and last_char ~= ' ' and last_char ~= '\n' then M.update() end | ||
end, | ||
}) | ||
|
||
-- update the completion window | ||
vim.api.nvim_create_autocmd('CursorMovedI', { | ||
callback = function() | ||
if M.cmp_win.id ~= nil then M.update() end | ||
end, | ||
}) | ||
|
||
-- show completion windows | ||
vim.api.nvim_create_autocmd({ 'InsertLeave', 'BufLeave' }, { | ||
callback = function() | ||
M.lsp.cancel_completions() | ||
M.cmp_win:close() | ||
M.doc_win:close() | ||
M.items = {} | ||
end, | ||
}) | ||
|
||
-- keybindings | ||
-- todo: SCUFFED | ||
local keymap = function(mode, key, callback) | ||
vim.api.nvim_set_keymap(mode, key, '', { | ||
expr = true, | ||
noremap = true, | ||
silent = true, | ||
callback = function() | ||
if M.cmp_win.id == nil then return vim.api.nvim_replace_termcodes(key, true, false, true) end | ||
vim.schedule(callback) | ||
end, | ||
}) | ||
end | ||
keymap('i', '<Tab>', M.accept) | ||
keymap('i', '<C-j>', M.select_next) | ||
keymap('i', '<C-k>', M.select_prev) | ||
keymap('i', '<Up>', M.select_prev) | ||
keymap('i', '<Down>', M.select_next) | ||
vim.api.nvim_set_keymap('i', '<C-space>', '', { | ||
noremap = true, | ||
silent = true, | ||
callback = function() M.update({ force = true }) end, | ||
}) | ||
end | ||
|
||
M.update = function(opts) | ||
opts = opts or { force = false } | ||
|
||
-- immediately update the results | ||
M.cmp.update(M.cmp_win, M.doc_win, M.items, opts) | ||
M.cmp_win:update() | ||
M.doc_win:update() | ||
-- trigger the lsp and update the results after retrieving | ||
M.lsp.completions(function(items) | ||
M.items = items | ||
M.cmp.update(M.cmp_win, M.doc_win, M.items, opts) | ||
M.cmp_win:update() | ||
M.doc_win:update() | ||
end) | ||
end | ||
|
||
M.accept = function() | ||
if M.cmp_win.id ~= nil then M.cmp.accept(M.cmp_win) end | ||
end | ||
|
||
M.select_prev = function() | ||
if M.cmp_win.id == nil then return end | ||
|
||
local current_line = vim.api.nvim_win_get_cursor(M.cmp_win.id)[1] | ||
vim.api.nvim_win_set_cursor(M.cmp_win.id, { math.max(1, current_line - 1), 0 }) | ||
end | ||
|
||
M.select_next = function() | ||
if M.cmp_win.id == nil then return end | ||
|
||
local current_line = vim.api.nvim_win_get_cursor(M.cmp_win.id)[1] | ||
local line_count = vim.api.nvim_buf_line_count(M.cmp_win.buf) | ||
vim.api.nvim_win_set_cursor(M.cmp_win.id, { math.min(line_count, current_line + 1), 0 }) | ||
end | ||
|
||
return M |
Oops, something went wrong.