diff --git a/.gp.md b/.gp.md.backup similarity index 100% rename from .gp.md rename to .gp.md.backup diff --git a/lua/gp/init.lua b/lua/gp/init.lua index 656ef611..6ade2985 100644 --- a/lua/gp/init.lua +++ b/lua/gp/init.lua @@ -1198,7 +1198,7 @@ M.create_handler = function(buf, win, line, first_undojoin, prefix, cursor) local skip_first_undojoin = not first_undojoin local hl_handler_group = "GpHandlerStandout" - vim.cmd("highlight default link " .. hl_handler_group .. " Search") + vim.cmd("highlight default link " .. hl_handler_group .. " search") local ns_id = vim.api.nvim_create_namespace("GpHandler_" .. M._H.uuid()) @@ -2520,8 +2520,7 @@ M.Prompt = function(params, target, prompt, model, template, system_template, wh selection = table.concat(lines, "\n") if selection == "" then - M.warning("Please select some text to rewrite") - return + M.warning("Empty selection") end end @@ -3310,4 +3309,50 @@ M.cmd.LspNewCompletion = function(params) require("gp.lsp").completion() end +M.cmd.LspProbe = function(params) + local lsp = require("gp.lsp") + + local buf = vim.api.nvim_get_current_buf() + local filetype = vim.api.nvim_buf_get_option(buf, "filetype") + print(filetype) + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + + local ns_id = vim.api.nvim_create_namespace("GpProbe_" .. M._H.uuid()) + + -- create extmark at the end of the buffer + local ex_id = vim.api.nvim_buf_set_extmark(buf, ns_id, #lines - 1, 0, { + strict = false, + right_gravity = false, + -- for debug + virt_text = { { "GpProbe" } }, + virt_text_pos = "overlay", + }) + + local first_line = vim.api.nvim_buf_get_extmark_by_id(buf, ns_id, ex_id, {})[1] + + M.spinner.start_spinner("Runnig LSP...") + local cleanup = function() + vim.schedule(function() + local fl = vim.api.nvim_buf_get_extmark_by_id(buf, ns_id, ex_id, {})[1] + -- delete everything after the fl + M._H.undojoin(buf) + vim.api.nvim_buf_set_lines(buf, fl + 1, -1, false, {}) + vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1) + M.spinner.stop_spinner() + end) + end + local queue = require("gp.queue").create(cleanup) + + -- write probe template + M._H.undojoin(buf) + vim.api.nvim_buf_set_lines(buf, first_line + 1, first_line + 1, false, lsp.probe_template(filetype)) + + lsp.completion(first_line + 1, 0, buf, function(items) + local tbuf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(tbuf, 0, -1, false, vim.split(vim.inspect(items), "\n")) + vim.api.nvim_win_set_buf(0, tbuf) + queue.runNextTask() + end, lsp.complete_ignored_root_items(filetype)) +end + return M diff --git a/lua/gp/lsp/ft/go.lua b/lua/gp/lsp/ft/go.lua new file mode 100644 index 00000000..10fe0fbf --- /dev/null +++ b/lua/gp/lsp/ft/go.lua @@ -0,0 +1,60 @@ +return { + template = "func gp_lsp_probe() {\n \n}", + suffixes = { ". " }, + root_ignore = { + Class = { + bool = "", + byte = "", + complex128 = "", + complex64 = "", + float32 = "", + float64 = "", + int = "", + int16 = "", + int32 = "", + int64 = "", + int8 = "", + rune = "", + string = "", + uint = "", + uint16 = "", + uint32 = "", + uint64 = "", + uint8 = "", + uintptr = "", + }, + Constant = { + ["false"] = "", + ["true"] = "", + }, + Function = { + append = "func(slice []Type, elems ...Type) []Type", + cap = "func(v Type) int", + clear = "func(t T)", + close = "func(c chan<- Type)", + complex = "func(r float64, i float64) complex128", + copy = "func(dst []Type, src []Type) int", + delete = "func(m map[Type]Type1, key Type)", + gp_lsp_probe = "func()", + imag = "func(c complex128) float64", + len = "func(v Type) int", + make = "func(t Type, size ...int) Type", + max = "func(x T, y ...T) T", + min = "func(x T, y ...T) T", + new = "func(Type) *Type", + panic = "func(v any)", + print = "func(args ...Type)", + println = "func(args ...Type)", + real = "func(c complex128) float64", + recover = "func() any", + }, + Interface = { + any = "", + comparable = "", + error = "", + }, + Variable = { + ["nil"] = "", + }, + }, +} diff --git a/lua/gp/lsp/ft/lua.lua b/lua/gp/lsp/ft/lua.lua new file mode 100644 index 00000000..7bc1986e --- /dev/null +++ b/lua/gp/lsp/ft/lua.lua @@ -0,0 +1,76 @@ +return { + template = "function gp_lsp_probe() {\n \n}", + suffixes = { ". " }, + root_ignore = { + Enum = { + _VERSION = "", + }, + Field = { + _G = "", + arg = "", + coroutine = "", + debug = "", + io = "", + loadfile = "", + math = "", + os = "", + package = "", + string = "", + table = "", + utf8 = "", + vim = "", + }, + Function = { + ["assert(v, message, ...)"] = "", + ["collectgarbage(opt, ...)"] = "", + ["dofile(filename)"] = "", + ["error(message, level)"] = "", + ["getfenv(f)"] = "", + ["getmetatable(object)"] = "", + ["ipairs(t)"] = "", + ["load(chunk, chunkname, mode, env)"] = "", + ["loadfile(filename, mode, env)"] = "", + ["loadstring(text, chunkname)"] = "", + ["module(name, ...)"] = "", + ["newproxy(proxy)"] = "", + ["next(table, index)"] = "", + ["pairs(t)"] = "", + ["pcall(f, arg1, ...)"] = "", + ["print(...)"] = "", + ["rawequal(v1, v2)"] = "", + ["rawget(table, index)"] = "", + ["rawlen(v)"] = "", + ["rawset(table, index, value)"] = "", + ["require(modname)"] = "", + ["select(index, ...)"] = "", + ["setfenv(f, table)"] = "", + ["setmetatable(table, metatable)"] = "", + ["tonumber(e)"] = "", + ["tonumber(e, base)"] = "", + ["tostring(v)"] = "", + ["type(v)"] = "", + ["unpack(list, i, j)"] = "", + ["warn(message, ...)"] = "", + ["xpcall(f, msgh, arg1, ...)"] = "", + }, + Keyword = { + ["and"] = "", + ["break"] = "", + continue = "", + ["else"] = "", + ["end"] = "", + ["false"] = "", + ["goto"] = "", + ["local"] = "", + ["nil"] = "", + ["not"] = "", + ["or"] = "", + ["return"] = "", + ["true"] = "", + ["until"] = "", + }, + Variable = { + _ENV = "", + }, + }, +} diff --git a/lua/gp/lsp/ft/python.lua b/lua/gp/lsp/ft/python.lua new file mode 100644 index 00000000..9708de4c --- /dev/null +++ b/lua/gp/lsp/ft/python.lua @@ -0,0 +1,217 @@ +return { + template = "def gp_probe():\n \n", + suffixes = { ". " }, + root_ignore = { + Class = { + ArithmeticError = "", + AssertionError = "", + AttributeError = "", + BaseException = "", + BaseExceptionGroup = "", + BlockingIOError = "", + BrokenPipeError = "", + BufferError = "", + BytesWarning = "", + ChildProcessError = "", + ConnectionAbortedError = "", + ConnectionError = "", + ConnectionRefusedError = "", + ConnectionResetError = "", + DeprecationWarning = "", + EOFError = "", + EncodingWarning = "", + Exception = "", + ExceptionGroup = "", + FileExistsError = "", + FileNotFoundError = "", + FloatingPointError = "", + FutureWarning = "", + GeneratorExit = "", + ImportError = "", + ImportWarning = "", + IndentationError = "", + IndexError = "", + InterruptedError = "", + IsADirectoryError = "", + KeyError = "", + KeyboardInterrupt = "", + LookupError = "", + MemoryError = "", + ModuleNotFoundError = "", + NameError = "", + NotADirectoryError = "", + NotImplementedError = "", + OSError = "", + OverflowError = "", + PendingDeprecationWarning = "", + PermissionError = "", + ProcessLookupError = "", + RecursionError = "", + ReferenceError = "", + ResourceWarning = "", + RuntimeError = "", + RuntimeWarning = "", + StopAsyncIteration = "", + StopIteration = "", + SyntaxError = "", + SyntaxWarning = "", + SystemError = "", + SystemExit = "", + TabError = "", + TimeoutError = "", + TypeError = "", + UnboundLocalError = "", + UnicodeDecodeError = "", + UnicodeEncodeError = "", + UnicodeError = "", + UnicodeTranslateError = "", + UnicodeWarning = "", + UserWarning = "", + ValueError = "", + Warning = "", + WindowsError = "", + ZeroDivisionError = "", + bool = "", + bytearray = "", + bytes = "", + classmethod = "", + complex = "", + dict = "", + ellipsis = "", + enumerate = "", + filter = "", + float = "", + frozenset = "", + ["function"] = "", + int = "", + list = "", + map = "", + memoryview = "", + object = "", + property = "", + range = "", + reversed = "", + set = "", + slice = "", + staticmethod = "", + str = "", + super = "", + tuple = "", + type = "", + zip = "", + }, + Function = { + __build_class__ = "", + __import__ = "", + abs = "", + aiter = "", + all = "", + anext = "", + any = "", + ascii = "", + bin = "", + breakpoint = "", + callable = "", + chr = "", + compile = "", + copyright = "", + credits = "", + delattr = "", + dir = "", + divmod = "", + eval = "", + exec = "", + exit = "", + filter = "", + format = "", + getattr = "", + globals = "", + gp_probe = "", + hasattr = "", + hash = "", + help = "", + hex = "", + id = "", + input = "", + isinstance = "", + issubclass = "", + iter = "", + len = "", + license = "", + locals = "", + map = "", + max = "", + min = "", + next = "", + oct = "", + open = "", + ord = "", + pow = "", + print = "", + quit = "", + repr = "", + reversed = "", + round = "", + setattr = "", + sorted = "", + sum = "", + vars = "", + zip = "", + }, + Keyword = { + False = "", + None = "", + True = "", + ["and"] = "", + assert = "", + async = "", + await = "", + ["break"] = "", + case = "", + class = "", + continue = "", + def = "", + del = "", + elif = "", + ["else"] = "", + except = "", + finally = "", + ["for"] = "", + from = "", + global = "", + ["if"] = "", + import = "", + ["in"] = "", + is = "", + lambda = "", + match = "", + nonlocal = "", + ["not"] = "", + ["or"] = "", + pass = "", + raise = "", + ["return"] = "", + try = "", + ["while"] = "", + with = "", + yield = "", + }, + Variable = { + Ellipsis = "", + EnvironmentError = "", + IOError = "", + NotImplemented = "", + __annotations__ = "", + __builtins__ = "", + __cached__ = "", + __dict__ = "", + __doc__ = "", + __file__ = "", + __loader__ = "", + __name__ = "", + __package__ = "", + __path__ = "", + __spec__ = "", + }, + }, +} diff --git a/lua/gp/lsp.lua b/lua/gp/lsp/init.lua similarity index 54% rename from lua/gp/lsp.lua rename to lua/gp/lsp/init.lua index cdb078be..b33e7fd5 100644 --- a/lua/gp/lsp.lua +++ b/lua/gp/lsp/init.lua @@ -1,8 +1,106 @@ local M = {} +-------------------------------------------------------------------------------- +-- LSP +-- Plan (all this should run async over a queue of tasks): +-- 1. Append probe function at the end of the buffer with empty line +-- 2. Try completion on the empty line +-- 3. Filter out the snippets and such +-- 4. Filter out the default completion items for given language +-- 5. Put the remaining items in probe function with continuation (". " etc) +-- 6. Try hover for those which don't have detail +-- 7. Try completion on variables, classes, etc + +-------------------------------------------------------------------------------- + +---@param filetype string +---@return table +M.complete_ignored_root_items = function(filetype) + local status, data = pcall(require, "gp.lsp.ft." .. filetype) + ---@diagnostic disable-next-line: undefined-field + if status and data and data.root_ignore then + ---@diagnostic disable-next-line: undefined-field + return data.root_ignore + end + return {} +end + +---@param filetype string +M.probe_template = function(filetype) + local probes = { + lua = { + "local function gp_probe()", + " ", + "end", + }, + go = vim.split(require("gp.lsp.ft.go").template, "\n"), + python = { + "def gp_probe():", + " ", + }, + javascript = { + "function gp_probe() {", + " ", + "}", + }, + rust = { + "fn gp_probe() {", + " ", + "}", + }, + c = { + "void gp_probe() {", + " ", + "}", + }, + h = { + "void gp_probe() {", + " ", + "}", + }, + java = { + "void gp_probe() {", + " ", + "}", + }, + kotlin = { + "fun gp_probe() {", + " ", + "}", + }, + php = { + "function gp_probe() {", + " ", + "}", + }, + csharp = { + "void gp_probe() {", + " ", + "}", + }, + cpp = { + "void gp_probe() {", + " ", + "}", + }, + ruby = { + "def gp_probe()", + " ", + "end", + }, + typescript = { + "function gp_probe() {", + " ", + "}", + }, + } + local result = probes[filetype] or {} + return result +end + ---@param lines string[]|nil lines of text ---@return string[]|nil snippet lines -M.extract_snippet = function(lines) +M.first_snippet = function(lines) if not lines then return nil end @@ -45,7 +143,8 @@ end ---@param win integer|nil window handle or 0 for current, defaults to current ---@param row integer|nil mark-indexed line number, defaults to current line ---@param col integer|nil mark-indexed column number, defaults to current column -M.hover = function(buf, win, row, col, offset_encoding) +---@param callback function | nil receives hover result +M.hover = function(buf, win, row, col, callback) local params = M.make_given_position_param(row, col, buf) vim.lsp.buf_request_all(buf, "textDocument/hover", params, function(results) @@ -61,7 +160,12 @@ M.hover = function(buf, win, row, col, offset_encoding) if #contents == 0 then return end - local snippet_lines = M.extract_snippet(contents) or {} + local snippet_lines = M.first_snippet(contents) or {} + + if callback then + callback(snippet_lines) + end + table.insert(contents, "$$$$$$$$$$$$$$$$") for _, line in ipairs(snippet_lines) do table.insert(contents, line) @@ -77,7 +181,9 @@ end ---@param col integer|nil mark-indexed column number, defaults to current column ---@param bufnr integer|nil buffer handle or 0 for current, defaults to current ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#completionParams -M.completion = function(row, col, bufnr) +---@param callback function | nil receives completion result +---@param filtered table | nil filtered out items with given label +M.completion = function(row, col, bufnr, callback, filtered) bufnr = bufnr or vim.api.nvim_get_current_buf() row = row or vim.api.nvim_win_get_cursor(0)[1] col = col or vim.api.nvim_win_get_cursor(0)[2] @@ -85,17 +191,26 @@ M.completion = function(row, col, bufnr) vim.lsp.buf_request_all(bufnr, "textDocument/completion", params, function(results) local items = {} - -- text = vim.inspect(results) .. "\n" .. "-------------------" .. "\n" .. text - for cid, r in pairs(results) do - for _, item in ipairs(r.result.items) do - table.insert(items, { cid = cid, item = item }) + for _, r in pairs(results) do + local result = {} + if r.result then + -- CompletionItem[] | CompletionList => CompletionItem[] + result = r.result.items and r.result.items or r.result + end + for _, item in ipairs(result) do + item.kind = vim.lsp.protocol.CompletionItemKind[item.kind] + if + item.kind ~= "Snippet" + and not (filtered and filtered[item.kind] and filtered[item.kind][item.label]) + then + items[item.kind] = items[item.kind] or {} + items[item.kind][item.label] = item.detail or "" + end end end - - local res = vim.lsp.util.text_document_completion_list_to_complete_items(results) - local tbuf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_lines(tbuf, 0, -1, false, vim.split(vim.inspect(res), "\n")) - vim.api.nvim_win_set_buf(0, tbuf) + if callback then + callback(items) + end end) end return M diff --git a/lua/gp/queue.lua b/lua/gp/queue.lua new file mode 100644 index 00000000..fdfb17cb --- /dev/null +++ b/lua/gp/queue.lua @@ -0,0 +1,46 @@ +local TaskQueue = {} + +function TaskQueue.create(onAllTasksComplete) + local self = { + tasks = {}, + onAllTasksComplete = onAllTasksComplete, + canceled = false, + } + + ---@param taskFunction function + function self.addTask(taskFunction) + if not self.canceled then + table.insert(self.tasks, taskFunction) + end + end + + ---@return function | nil + function self.getNextTask() + if self.canceled then + self.tasks = {} + end + return table.remove(self.tasks, 1) + end + + function self.runNextTask() + if self.canceled then + return + end + + local taskFunction = self.getNextTask() + if taskFunction then + taskFunction(self.runNextTask) + elseif self.onAllTasksComplete then + self.onAllTasksComplete() + end + end + + function self.cancel() + self.tasks = {} + self.canceled = true + end + + return self +end + +return TaskQueue