72087d4
Branches > main > 72087d4 > files / nvim / lua
Add epic completion code from the LLM
Commit: 72087d4205db567269f122e912d395a806877cfb
Parent: 70744c0
3 changed files:
nvim/init.lua view file
@@ -16,6 +16,7 @@ require("lsp") require("format") require("lazy_ui")+ require("complete") end) --- TODO there's a bug tha parens and virtual text likes to stick around for longer than it should...nvim/lua/complete.lua view file
@@ -0,0 +1,377 @@+local api = vim.api+local autocmd = api.nvim_create_autocmd+local protocol = vim.lsp.protocol++local max_shown = 200+local clojure_lsp_max_items = 600+local symbol_chars = "[^%s%(%)%[%]{}\"',`;@^~\\]"++local prose_filetypes = {[""] = true, markdown = true, text = true, gitcommit = true}++local function syntax_names_at(buf, row, col)+ local names = {}+ if vim.treesitter.highlighter.active[buf] then+ for _, capture in ipairs(vim.treesitter.get_captures_at_pos(buf, row - 1, col - 1)) do+ names[#names + 1] = capture.capture+ end+ else+ for _, id in ipairs(vim.fn.synstack(row, col)) do+ names[#names + 1] = vim.fn.synIDattr(id, "name")+ end+ end+ return names+end++local function in_string_or_comment(buf, row, col)+ if col == 0 then return false end+ for _, name in ipairs(syntax_names_at(buf, row, col)) do+ name = name:lower()+ if name:match("string") or name:match("comment") then return true end+ end+ return false+end++local function namespace_of(detail)+ if not detail then return end+ return detail:match("^([%w%.%-_]+)/") or detail:match("^([%w%._]+)%.[%w_$]+$")+end++local function from_lsp(item)+ local is_snippet = item.insertTextFormat == protocol.InsertTextFormat.Snippet+ local insert = item.textEdit and item.textEdit.newText or item.insertText or item.label+ return {label = item.label,+ word = is_snippet and item.label or insert,+ snippet = is_snippet and insert or nil,+ ns = namespace_of(item.detail),+ is_local = item.kind == protocol.CompletionItemKind.Variable and item.detail == "",+ lsp = item}+end++local function ns_and_aliases(buf)+ local ns_lines = {}+ for _, line in ipairs(api.nvim_buf_get_lines(buf, 0, 200, false)) do+ if #ns_lines > 0 and line:match("^%(") then break end+ if #ns_lines > 0 or line:match("^%(ns%s") then ns_lines[#ns_lines + 1] = line end+ end+ local ns_form = table.concat(ns_lines, "\n")++ local aliases = {}+ for lib, alias in ns_form:gmatch("%[([^%s%]]+)[^%]]-:as%s+([^%s%]]+)") do+ aliases[lib] = alias+ end+ return ns_form:match("^%(ns%s+([^%s%)]+)"), aliases+end++local function auto_resolved_keyword(keyword)+ return {label = keyword, word = keyword, auto_resolved = true}+end++local function add_auto_resolved_keywords(buf, items)+ local ns, aliases = ns_and_aliases(buf)+ local keywords = {}+ for _, item in ipairs(items) do+ local kw_ns, name = item.label:match("^:([^/]+)/(.+)$")+ if kw_ns == ns then+ keywords[#keywords + 1] = auto_resolved_keyword("::" .. name)+ elseif aliases[kw_ns] then+ keywords[#keywords + 1] = auto_resolved_keyword("::" .. aliases[kw_ns] .. "/" .. name)+ end+ end++ for _, line in ipairs(api.nvim_buf_get_lines(buf, 0, -1, false)) do+ for keyword in line:gmatch("::" .. symbol_chars .. "+") do+ keywords[#keywords + 1] = auto_resolved_keyword(keyword)+ end+ end+ return vim.list_extend(items, keywords)+end++local function is_unfiltered_fallback(items, word)+ if word == "" or #items < clojure_lsp_max_items then return false end+ for _, item in ipairs(items) do+ if not vim.startswith(item.label:lower(), word:lower()) then return true end+ end+ return false+end++local clojure = {}++function clojure.word(before)+ return (before:match(symbol_chars .. "*$"):gsub("^#+", ""))+end++function clojure.request_prefix(word)+ return word:match("^:") or word:match("^.*/") or word:sub(1, 1)+end++function clojure.completes_nothing_typed(before)+ return before:match("[%s%(%[{]$") ~= nil+end++local last_locals = {}++function clojure.fetch(buf, word, done)+ local client = vim.lsp.get_clients({bufnr = buf, method = "textDocument/completion"})[1]+ if not client then return end++ local form_start = vim.fn.search("^[(\\[{]", "bcnW")+ local function same_form()+ return last_locals.buf == buf and last_locals.form_start == form_start+ end++ if word == "" then+ local before = api.nvim_get_current_line():sub(1, api.nvim_win_get_cursor(0)[2])+ local after_open_bracket = before:match("[%(%[{]%s*$")+ if not after_open_bracket then+ if same_form() then done(last_locals.items) end+ return+ end+ end++ local params = vim.lsp.util.make_position_params(0, client.offset_encoding)+ local request_id, cancelled++ local function request(retries_left)+ _, request_id = client:request("textDocument/completion", params, function(err, result)+ if err or not result or cancelled then return end+ local items = vim.tbl_map(from_lsp, result.items or result)++ if retries_left > 0 and is_unfiltered_fallback(items, word) then+ return vim.defer_fn(function()+ if not cancelled then request(retries_left - 1) end+ end, 50)+ end++ if word == "" then+ local locals = vim.tbl_filter(function(item) return item.is_local end, items)+ if #locals > 0 then+ last_locals = {buf = buf, form_start = form_start, items = locals}+ elseif same_form() then+ items = last_locals.items+ end+ end++ local truncated = result.isIncomplete or #items >= clojure_lsp_max_items+ done(add_auto_resolved_keywords(buf, items), client, truncated)+ end, buf)+ end+ request(5)++ return function()+ cancelled = true+ if request_id then client:cancel_request(request_id) end+ end+end++local buffer_words = {}++function buffer_words.word(before)+ return before:match("[%w_]*$")+end++function buffer_words.request_prefix()+ return ""+end++function buffer_words.fetch(buf, word, done)+ local row, col = unpack(api.nvim_win_get_cursor(0))+ local items, seen = {}, {}++ for i, line in ipairs(api.nvim_buf_get_lines(buf, 0, -1, false)) do+ if i == row then line = line:sub(1, col - #word) .. " " .. line:sub(col + 1) end++ for w in line:gmatch("[%w_]+") do+ if not seen[w] and not w:match("^%d") then+ seen[w] = true+ items[#items + 1] = {label = w, word = w}+ end+ end+ end++ done(items)+end++local function source_for(buf)+ local ft = vim.bo[buf].filetype+ if ft == "clojure" then return clojure end+ if vim.bo[buf].buftype == "" and not prose_filetypes[ft] then return buffer_words end+end++local session+local cancel_fetch+local accepted_at++local function close()+ if vim.fn.pumvisible() == 1 then+ vim.fn.complete(vim.fn.col("."), {})+ end+end++local function set_candidates(s, items)+ s.items, s.labels, s.by_label = items, {}, {}+ for _, item in ipairs(items) do+ if not s.by_label[item.label] then+ s.by_label[item.label] = item+ s.labels[#s.labels + 1] = item.label+ end+ end+end++local function group_of(item, word_is_capitalized)+ if item.is_local then return 1 end+ if word_is_capitalized and not item.label:match("^%u") then return 3 end+ return 2+end++local function matching(s, word)+ if word == "" then+ return vim.tbl_filter(function(item) return item.is_local end, s.items)+ end++ local fuzzy = {}+ for _, label in ipairs(vim.fn.matchfuzzy(s.labels, word)) do+ local item = s.by_label[label]+ if not item.auto_resolved or word:match("^::") then fuzzy[#fuzzy + 1] = item end+ end++ local prefixed = vim.tbl_filter(function(item)+ return vim.startswith(item.label:lower(), word:lower())+ end, fuzzy)+ local items = #prefixed > 0 and prefixed or fuzzy++ local fuzzy_rank = {}+ for i, item in ipairs(items) do fuzzy_rank[item] = i end+ local word_is_capitalized = word:match("^%u") ~= nil++ table.sort(items, function(a, b)+ local group_a, group_b = group_of(a, word_is_capitalized), group_of(b, word_is_capitalized)+ if group_a ~= group_b then return group_a < group_b end+ if #prefixed > 0 and #a.label ~= #b.label then return #a.label < #b.label end+ return fuzzy_rank[a] < fuzzy_rank[b]+ end)+ return items+end++local function show(s, word)+ local items = matching(s, word)+ if #items == 0 then return close() end++ local entries = {}+ for i = 1, math.min(#items, max_shown) do+ entries[i] = {word = items[i].word,+ abbr = items[i].label,+ menu = items[i].ns,+ equal = 1,+ user_data = {complete = i}}+ end++ s.shown = items+ vim.fn.complete(s.start + 1, entries)+end++local function refresh()+ local buf = api.nvim_get_current_buf()+ local row, col = unpack(api.nvim_win_get_cursor(0))++ if accepted_at and vim.deep_equal(accepted_at, {buf, row, col}) then return end+ accepted_at = nil++ local navigating_menu = vim.fn.complete_info({"selected"}).selected ~= -1+ if navigating_menu then return end++ local source = source_for(buf)+ if not source then return end++ local before = api.nvim_get_current_line():sub(1, col)+ local word = source.word(before)+ local wants_completion = word ~= ""+ or (source.completes_nothing_typed and source.completes_nothing_typed(before))++ if in_string_or_comment(buf, row, col) or word:match("^%d") or not wants_completion then+ session = nil+ return close()+ end++ local start = col - #word+ local key = table.concat({buf, row, start, source.request_prefix(word)}, ":")++ local still_typing_same_word = session and session.key == key and vim.startswith(word, session.word)+ local more_typing_narrows_results = session and session.truncated and word ~= session.word+ and not word:match("^::")+ if still_typing_same_word and not more_typing_narrows_results then+ if session.items then show(session, word) end+ return+ end++ if cancel_fetch then cancel_fetch() end++ local s = {key = key, row = row, start = start, word = word}+ if session and session.items and session.row == row and session.start == start then+ s.items, s.labels, s.by_label = session.items, session.labels, session.by_label+ show(s, word)+ end+ session = s++ cancel_fetch = source.fetch(buf, word, function(items, client, truncated)+ if session ~= s or not api.nvim_get_mode().mode:match("^i") then return end+ cancel_fetch = nil+ set_candidates(s, items)+ s.client, s.truncated = client, truncated+ refresh()+ end)+end++local function apply_extra_edits(s, item)+ local client, lsp_item = s.client, item.lsp+ if not (client and lsp_item) then return end++ local buf = api.nvim_get_current_buf()+ local function apply(resolved)+ if resolved.additionalTextEdits then+ vim.lsp.util.apply_text_edits(resolved.additionalTextEdits, buf, client.offset_encoding)+ end+ end++ if lsp_item.additionalTextEdits or not client:supports_method("completionItem/resolve") then+ apply(lsp_item)+ else+ client:request("completionItem/resolve", lsp_item, function(err, resolved)+ if not err and resolved then apply(resolved) end+ end, buf)+ end+end++local function accept(s, item)+ if item.snippet then+ local row, col = unpack(api.nvim_win_get_cursor(0))+ api.nvim_buf_set_text(0, row - 1, s.start, row - 1, col, {""})+ api.nvim_win_set_cursor(0, {row, s.start})+ vim.snippet.expand(item.snippet)+ end+ apply_extra_edits(s, item)+end++autocmd({"TextChangedI", "TextChangedP"}, {callback = refresh})++autocmd("CompleteDone", {callback = function()+ local data = vim.v.completed_item.user_data+ if type(data) ~= "table" or not data.complete then return end++ local s = session+ local item = s and s.shown and s.shown[data.complete]+ local col = api.nvim_win_get_cursor(0)[2]+ local was_inserted = item and api.nvim_get_current_line():sub(s.start + 1, col) == item.word+ if not was_inserted then return end++ accept(s, item)+ local row, new_col = unpack(api.nvim_win_get_cursor(0))+ accepted_at = {api.nvim_get_current_buf(), row, new_col}+end})++autocmd("InsertLeave", {callback = function()+ if cancel_fetch then cancel_fetch() end+ session, cancel_fetch, accepted_at = nil, nil, nil+end})++vim.opt.completeopt = {"menu", "menuone", "noinsert", "noselect"}+vim.opt.shortmess:append("c")nvim/lua/opts.lua view file
@@ -38,8 +38,4 @@ o.swapfile = false o.backup = false o.undofile = true-- --- Completion- o.completeopt = {"menu", "menuone", "fuzzy"}- end)