219 lines
6.0 KiB
Lua
219 lines
6.0 KiB
Lua
-- 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
|