Files
setup/.config/nvim/after/plugin/hoogle.lua
Radu Macocian (admac) d50d665994 Added obsidian.nvim
2026-03-05 16:52:04 +01:00

260 lines
7.6 KiB
Lua

local pickers = require("telescope.pickers")
local finders = require("telescope.finders")
local conf = require("telescope.config").values
local previewers = require("telescope.previewers")
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")
local STOP_WORDS = {
["a"] = true,
["an"] = true,
["and"] = true,
["as"] = true,
["at"] = true,
["by"] = true,
["for"] = true,
["from"] = true,
["in"] = true,
["into"] = true,
["is"] = true,
["of"] = true,
["on"] = true,
["or"] = true,
["that"] = true,
["the"] = true,
["to"] = true,
["with"] = true,
}
local function parse_query_mode(raw_query)
local query = vim.trim(raw_query or "")
if query:match("^text:%s*") then
return "text", vim.trim((query:gsub("^text:%s*", "", 1)))
end
if query:match("^type:%s*") then
return "type", vim.trim((query:gsub("^type:%s*", "", 1)))
end
-- Heuristic:
-- - Queries that look like signatures/operators stay in type mode.
-- - Everything else defaults to text mode so plain-language search works.
local looks_like_type = query:match("::")
or query:match("->")
or query:match("=>")
or query:match("[%[%]()%{%}]")
or query:match("^%S+$")
return looks_like_type and "type" or "text", query
end
local function run_hoogle_query(query, count)
local cmd = { "hoogle", ("--count=%d"):format(count or 200), "--json", query }
local lines = vim.fn.systemlist(cmd)
if vim.v.shell_error ~= 0 then
return nil, "hoogle failed:\n" .. table.concat(lines, "\n")
end
local ok, parsed = pcall(vim.json.decode, table.concat(lines, "\n"))
if not ok or type(parsed) ~= "table" then
return nil, "hoogle returned unexpected output"
end
return parsed, nil
end
local function normalize_docs(docs)
return vim.trim((docs or ""):gsub("<.->", " "):gsub("%s+", " "):lower())
end
local function tokenize_text_query(query)
local tokens = {}
local seen = {}
for token in query:lower():gmatch("[%w_]+") do
if #token >= 3 and not STOP_WORDS[token] and not seen[token] then
seen[token] = true
table.insert(tokens, token)
end
end
return tokens
end
local function hoogle_text_search(query)
local tokens = tokenize_text_query(query)
if #tokens == 0 then
tokens = { query:lower() }
end
local ranked = {}
local order = {}
local function bump(entry, token, base_score)
local key = entry.url or entry.item or (entry.module and entry.module.name) or vim.inspect(entry)
local slot = ranked[key]
if not slot then
slot = { entry = entry, score = 0 }
ranked[key] = slot
table.insert(order, key)
end
local signature = (entry.item or ""):lower()
local docs = normalize_docs(entry.docs)
if signature:find(token, 1, true) then
slot.score = slot.score + base_score + 2
end
if docs:find(token, 1, true) then
slot.score = slot.score + base_score
end
end
local exact, err = run_hoogle_query(query, 200)
if exact then
for _, entry in ipairs(exact) do
bump(entry, query:lower(), 8)
end
end
for i = 1, math.min(#tokens, 6) do
local token = tokens[i]
local partial, partial_err = run_hoogle_query(token, 120)
if partial then
for _, entry in ipairs(partial) do
bump(entry, token, 3)
end
elseif not err then
err = partial_err
end
end
local items = {}
for _, key in ipairs(order) do
local slot = ranked[key]
if slot and slot.score > 0 then
table.insert(items, slot)
end
end
table.sort(items, function(a, b)
return a.score > b.score
end)
local out = {}
for _, slot in ipairs(items) do
table.insert(out, slot.entry)
end
if #out == 0 and err then
return nil, err
end
return out, nil
end
local function hoogle_picker(query)
if vim.fn.executable("hoogle") ~= 1 then
vim.notify("hoogle not found in PATH", vim.log.levels.ERROR)
return
end
local mode, normalized_query = parse_query_mode(query)
if normalized_query == "" then
return
end
local decoded, err
if mode == "text" then
decoded, err = hoogle_text_search(normalized_query)
else
decoded, err = run_hoogle_query(normalized_query, 200)
end
if not decoded then
vim.notify(err or "hoogle failed", vim.log.levels.ERROR)
return
end
local results = {}
for _, entry in ipairs(decoded) do
local signature = entry.item or ""
local docs = entry.docs or ""
local url = entry.url
local docs_one_line = vim.trim((docs:gsub("%s+", " ")))
if #docs_one_line > 140 then
docs_one_line = docs_one_line:sub(1, 137) .. "..."
end
local display = signature
if docs_one_line ~= "" then
display = display .. " - " .. docs_one_line
end
table.insert(results, {
signature = signature,
docs = docs,
url = url,
display = display,
})
end
pickers.new({}, {
prompt_title = ("Hoogle (%s): %s"):format(mode, normalized_query),
finder = finders.new_table({
results = results,
entry_maker = function(entry)
return {
value = entry,
display = entry.display,
ordinal = entry.signature .. " " .. (entry.docs or ""),
}
end,
}),
sorter = conf.generic_sorter({}),
previewer = previewers.new_buffer_previewer({
define_preview = function(self, entry)
local value = entry.value or {}
local preview_lines = { value.signature or "" }
if value.docs and value.docs ~= "" then
table.insert(preview_lines, "")
vim.list_extend(preview_lines, vim.split(value.docs, "\n", { plain = true }))
else
table.insert(preview_lines, "")
table.insert(preview_lines, "No documentation available.")
end
if value.url and value.url ~= "" then
table.insert(preview_lines, "")
table.insert(preview_lines, value.url)
end
vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, preview_lines)
end,
}),
attach_mappings = function(prompt_bufnr, map)
local function open_result()
local selection = action_state.get_selected_entry()
actions.close(prompt_bufnr)
if selection and selection.value and selection.value.url then
vim.ui.open(selection.value.url)
end
end
map("i", "<CR>", open_result)
map("n", "<CR>", open_result)
return true
end,
}):find()
end
vim.api.nvim_create_user_command("Hoogle", function(opts)
local query = opts.args ~= "" and opts.args or vim.fn.input("Hoogle > ")
if query == "" then return end
hoogle_picker(query)
end, { nargs = "*" })
vim.keymap.set("n", "<leader>hh", function()
vim.cmd("Hoogle " .. vim.fn.expand("<cword>"))
end, { desc = "Hoogle current word" })
vim.keymap.set("n", "<leader>ht", function()
vim.cmd("Hoogle " .. vim.fn.input("Hoogle type/text > "))
end, { desc = "Hoogle query" })