pmacs/builtin/menus/default.lua

143 lines
6.2 KiB
Lua

-- builtin/menus/default.lua --- default right-click context menu (Q#CM2/Q#CM3).
--
-- Items are registered with `pmacs.menu.item` (Rust registry). Each
-- names a command to invoke and declares visibility via a coarse
-- `context` tag (sugar) or a full `predicate`, evaluated against a
-- context table when the menu opens. `group`/`order` drive layout;
-- separators fall between groups.
--
-- `pmacs.menu.build()` (called from Rust at right-click) does the
-- resolution: filter visible items, group/sort, and return the rows.
local ed = pmacs.editor
-- The active buffer's live LSP attachment record, or nil --- guarded so
-- the menu never errors when LSP isn't configured/loaded, and never
-- triggers an attach just by opening.
local function lsp_attachment()
if pmacs.lsp == nil or pmacs.lsp.active_attachment == nil then
return nil
end
local ok, rec = pcall(pmacs.lsp.active_attachment)
if ok then return rec end
return nil
end
-- Whether the 0-based (line, col) falls within diagnostic `d`'s range.
local function diag_contains(d, line, col)
if line < d.start_line or line > d.end_line then return false end
if line == d.start_line and col < d.start_col then return false end
if line == d.end_line and col > d.end_col then return false end
return true
end
-- Evaluate a coarse `context` tag against the live context table
-- (Q#CM3). Sugar for a predicate; `symbol` needs a word-under-point and
-- an attached server, `diagnostic` needs a published diagnostic
-- spanning the cursor.
function pmacs.menu._context_eval(tag, cx)
if tag == "always" then
return true
elseif tag == "selection" then
return cx.has_selection
elseif tag == "symbol" then
return cx.word ~= nil and cx.attachment ~= nil
elseif tag == "diagnostic" then
if cx.attachment == nil then return false end
for _, d in ipairs(pmacs.diag.list(cx.attachment.uri)) do
if diag_contains(d, cx.line, cx.col) then return true end
end
return false
end
return false
end
-- Whether `it` is visible in context `cx`. A failing predicate hides
-- the item rather than aborting the whole menu.
local function item_visible(it, cx)
if it.predicate ~= nil then
local ok, vis = pcall(it.predicate, cx)
return ok and vis and true or false
elseif it.context ~= nil then
return pmacs.menu._context_eval(it.context, cx) and true or false
end
return true
end
-- Build the resolved, grouped, visibility-filtered rows for an open
-- menu (Q#CM3). Returns an array where each element is either
-- `{ separator = true }` or `{ label = ..., command = ... }`.
function pmacs.menu.build()
local cx = {
has_selection = ed.region() ~= nil,
word = ed.word_at_cursor(),
line = ed.cursor_line(),
col = ed.cursor_col(),
attachment = lsp_attachment(),
}
-- Filter to visible items, tagging insertion order for a stable sort.
local visible = {}
for i, it in ipairs(pmacs.menu._raw()) do
if item_visible(it, cx) then
it.__i = i
visible[#visible + 1] = it
end
end
-- Group order = first appearance in the registry; within a group,
-- sort by `order`, then by insertion for ties.
local gidx, next_g = {}, 1
for _, it in ipairs(visible) do
local g = it.group or ""
if gidx[g] == nil then
gidx[g] = next_g
next_g = next_g + 1
end
end
table.sort(visible, function(a, b)
local ga, gb = gidx[a.group or ""], gidx[b.group or ""]
if ga ~= gb then return ga < gb end
local oa, ob = a.order or 0, b.order or 0
if oa ~= ob then return oa < ob end
return a.__i < b.__i
end)
-- Emit rows, inserting a separator between distinct groups.
local rows, last_group = {}, nil
for _, it in ipairs(visible) do
local g = it.group or ""
if last_group ~= nil and g ~= last_group then
rows[#rows + 1] = { separator = true }
end
rows[#rows + 1] = { label = it.label, command = it.command }
last_group = g
end
return rows
end
-- Default items. The edit group adapts to the selection; the symbol
-- group appears on an identifier with a server attached; the diagnostic
-- group appears when a diagnostic spans the cursor; history is always
-- available. Group order follows registration order.
pmacs.menu.item { id = "edit.cut", label = "Cut", command = "edit.cut", context = "selection", group = "edit", order = 10 }
pmacs.menu.item { id = "edit.copy", label = "Copy", command = "edit.copy", context = "selection", group = "edit", order = 20 }
pmacs.menu.item { id = "edit.paste", label = "Paste", command = "edit.paste", context = "always", group = "edit", order = 30 }
pmacs.menu.item { id = "edit.select-all", label = "Select All", command = "edit.select-all", context = "always", group = "edit", order = 40 }
-- Symbol group (LSP). Shown when the cursor is on an identifier and a
-- language server is attached. Each invokes the existing async command,
-- which acts at the cursor (the right-click anchored it there).
pmacs.menu.item { id = "lsp.go-to-definition", label = "Go to Definition", command = "lsp.go-to-definition", context = "symbol", group = "symbol", order = 10 }
pmacs.menu.item { id = "lsp.find-references", label = "Find References", command = "lsp.find-references", context = "symbol", group = "symbol", order = 20 }
pmacs.menu.item { id = "lsp.rename", label = "Rename", command = "lsp.rename", context = "symbol", group = "symbol", order = 30 }
pmacs.menu.item { id = "lsp.hover", label = "Hover", command = "lsp.hover", context = "symbol", group = "symbol", order = 40 }
-- Diagnostic group (LSP). Shown when a diagnostic spans the cursor.
-- "Quick Fix" runs code actions at the point (Q#CM10 defers streaming
-- the individual fix titles into the menu).
pmacs.menu.item { id = "lsp.quick-fix", label = "Quick Fix", command = "lsp.code-actions", context = "diagnostic", group = "diagnostic", order = 10 }
pmacs.menu.item { id = "buffer.undo", label = "Undo", command = "buffer.undo", context = "always", group = "history", order = 10 }
pmacs.menu.item { id = "buffer.redo", label = "Redo", command = "buffer.redo", context = "always", group = "history", order = 20 }