add stuff
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
-- Custom hover for CljElixir .clje files
|
||||
-- Resolves Module/function patterns against stub files in the project's stubs/ directory
|
||||
local M = {}
|
||||
|
||||
--- Walk up from the current buffer's directory to find the stubs/ directory
|
||||
local function find_stubs_dir()
|
||||
local bufpath = vim.api.nvim_buf_get_name(0)
|
||||
if bufpath == "" then return nil end
|
||||
local dir = vim.fn.fnamemodify(bufpath, ":p:h")
|
||||
for _ = 1, 20 do
|
||||
if dir == "/" or dir == "" then break end
|
||||
if vim.fn.isdirectory(dir .. "/stubs") == 1 then
|
||||
return dir .. "/stubs"
|
||||
end
|
||||
dir = vim.fn.fnamemodify(dir, ":h")
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Extract the symbol under cursor, handling namespace/function patterns.
|
||||
--- Returns (namespace, function_name) or nil.
|
||||
local function symbol_under_cursor()
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
local col = vim.api.nvim_win_get_cursor(0)[2] -- 0-indexed byte col
|
||||
|
||||
-- Expand outward from cursor to find the full symbol (letters, digits, -, ?, !, ., /, _)
|
||||
local s, e = col + 1, col + 1
|
||||
local sym_chars = "[%w%-%?%!%.%/_><=*+]"
|
||||
while s > 1 and line:sub(s - 1, s - 1):match(sym_chars) do
|
||||
s = s - 1
|
||||
end
|
||||
while e < #line and line:sub(e + 1, e + 1):match(sym_chars) do
|
||||
e = e + 1
|
||||
end
|
||||
local word = line:sub(s, e)
|
||||
if not word or word == "" then return nil, nil end
|
||||
|
||||
-- Must contain a / to be a qualified call
|
||||
local slash = word:find("/")
|
||||
if not slash then return nil, nil end
|
||||
|
||||
local ns = word:sub(1, slash - 1)
|
||||
local fn_name = word:sub(slash + 1)
|
||||
if ns == "" or fn_name == "" then return nil, nil end
|
||||
|
||||
return ns, fn_name
|
||||
end
|
||||
|
||||
--- Resolve a namespace name to a stub file path
|
||||
local function resolve_stub_file(stubs_dir, ns)
|
||||
-- Direct match (e.g., Process.clj, erlang.clj)
|
||||
local path = stubs_dir .. "/" .. ns .. ".clj"
|
||||
if vim.fn.filereadable(path) == 1 then return path end
|
||||
|
||||
-- erl_ prefix for case-insensitive collisions (e.g., erl_io.clj for :io)
|
||||
path = stubs_dir .. "/erl_" .. ns .. ".clj"
|
||||
if vim.fn.filereadable(path) == 1 then return path end
|
||||
|
||||
-- Subdirectory (e.g., clje/core.clj)
|
||||
path = stubs_dir .. "/clje/" .. ns .. ".clj"
|
||||
if vim.fn.filereadable(path) == 1 then return path end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Find a (defn ...) or (defmacro ...) form in the file lines and return its text
|
||||
local function find_defn(lines, fn_name)
|
||||
-- Escape special pattern chars in the function name
|
||||
local pat = fn_name:gsub("([%-%.%?%!%*%+%[%]%(%)%^%$%%])", "%%%1")
|
||||
|
||||
local start_idx = nil
|
||||
for i, line in ipairs(lines) do
|
||||
-- Match (defn name or (defmacro name at the start of a line
|
||||
if line:match("^%(defn%s+" .. pat .. "$")
|
||||
or line:match("^%(defn%s+" .. pat .. "%s")
|
||||
or line:match("^%(defmacro%s+" .. pat .. "$")
|
||||
or line:match("^%(defmacro%s+" .. pat .. "%s") then
|
||||
start_idx = i
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not start_idx then return nil end
|
||||
|
||||
-- Collect lines for this form by tracking paren depth
|
||||
local form_lines = {}
|
||||
local depth = 0
|
||||
for i = start_idx, #lines do
|
||||
local line = lines[i]
|
||||
table.insert(form_lines, line)
|
||||
for ch in line:gmatch(".") do
|
||||
if ch == "(" then depth = depth + 1
|
||||
elseif ch == ")" then depth = depth - 1 end
|
||||
end
|
||||
if depth <= 0 and i > start_idx then break end
|
||||
end
|
||||
|
||||
return form_lines
|
||||
end
|
||||
|
||||
--- Extract a vector [args] from a signature line, stripping outer parens and trailing junk
|
||||
local function extract_sig(s)
|
||||
-- Match the innermost [args] vector
|
||||
local vec = s:match("%[(.-)%]")
|
||||
if vec then
|
||||
return "[" .. vec .. "]"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Extract docstring and signatures from defn form lines for a cleaner display
|
||||
local function parse_defn(form_lines, ns, fn_name)
|
||||
local md = {}
|
||||
local docstring_parts = {}
|
||||
local signatures = {}
|
||||
local in_docstring = false
|
||||
|
||||
for i, line in ipairs(form_lines) do
|
||||
if i == 1 then goto continue end -- skip (defn name line
|
||||
|
||||
local trimmed = line:match("^%s*(.-)%s*$")
|
||||
|
||||
-- Detect docstring (starts and/or ends with ")
|
||||
if not in_docstring and trimmed:match('^"') then
|
||||
in_docstring = true
|
||||
-- Single-line docstring
|
||||
if trimmed:match('^"(.*)"$') then
|
||||
table.insert(docstring_parts, trimmed:match('^"(.*)"$'))
|
||||
in_docstring = false
|
||||
else
|
||||
table.insert(docstring_parts, trimmed:match('^"(.*)$'))
|
||||
end
|
||||
elseif in_docstring then
|
||||
if trimmed:match('"[%)]*$') then
|
||||
table.insert(docstring_parts, trimmed:match('^(.-)"'))
|
||||
in_docstring = false
|
||||
else
|
||||
table.insert(docstring_parts, trimmed)
|
||||
end
|
||||
elseif trimmed:match("%[") then
|
||||
-- Signature line containing [args]
|
||||
local sig = extract_sig(trimmed)
|
||||
if sig then
|
||||
table.insert(signatures, sig)
|
||||
end
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
-- Build markdown
|
||||
table.insert(md, "### `" .. ns .. "/" .. fn_name .. "`")
|
||||
table.insert(md, "")
|
||||
|
||||
if #signatures > 0 then
|
||||
table.insert(md, "```clojure")
|
||||
for _, sig in ipairs(signatures) do
|
||||
if sig == "[]" then
|
||||
table.insert(md, "(" .. ns .. "/" .. fn_name .. ")")
|
||||
else
|
||||
table.insert(md, "(" .. ns .. "/" .. fn_name .. " " .. sig .. ")")
|
||||
end
|
||||
end
|
||||
table.insert(md, "```")
|
||||
table.insert(md, "")
|
||||
end
|
||||
|
||||
if #docstring_parts > 0 then
|
||||
for _, part in ipairs(docstring_parts) do
|
||||
table.insert(md, part)
|
||||
end
|
||||
end
|
||||
|
||||
return md
|
||||
end
|
||||
|
||||
--- Main hover function — looks up Module/function in stubs
|
||||
function M.hover()
|
||||
local ns, fn_name = symbol_under_cursor()
|
||||
if not ns or not fn_name then
|
||||
vim.lsp.buf.hover()
|
||||
return
|
||||
end
|
||||
|
||||
local stubs_dir = find_stubs_dir()
|
||||
if not stubs_dir then
|
||||
vim.lsp.buf.hover()
|
||||
return
|
||||
end
|
||||
|
||||
local stub_file = resolve_stub_file(stubs_dir, ns)
|
||||
if not stub_file then
|
||||
vim.lsp.buf.hover()
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.fn.readfile(stub_file)
|
||||
if not lines or #lines == 0 then
|
||||
vim.lsp.buf.hover()
|
||||
return
|
||||
end
|
||||
|
||||
local form_lines = find_defn(lines, fn_name)
|
||||
if not form_lines then
|
||||
-- Function not found in stub, fall back to LSP
|
||||
vim.lsp.buf.hover()
|
||||
return
|
||||
end
|
||||
|
||||
local md = parse_defn(form_lines, ns, fn_name)
|
||||
|
||||
vim.lsp.util.open_floating_preview(md, "markdown", {
|
||||
border = "rounded",
|
||||
focus_id = "clje-hover",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user