Adds diagnostic navigation to the TUI/editor surface. Reuses the
existing `pmacs.diag.next` / `previous` walkers (which already wrap
around) and the cross-file jump ring so `M-,` returns from a
diagnostic jump just like an LSP definition jump.
Surface:
* `pmacs.command.define { name = "diag.next" / "diag.previous" }`
* `pmacs.keymap.bind { sequence = "M-g n" / "M-g p" }` — Emacs's
`next-error` / `previous-error` chord.
The command walks the diag store for the active buffer's attached URI,
falls back to a status-line message ("no LSP server" / "no diagnostics
in buffer") rather than faulting when there's nothing to jump to. On a
hit it pushes the jump ring, moves the cursor via `pmacs.editor` motion
primitives (so every overlay observer sees the navigation), and sets a
status line of the form `diag (warning): ...`.
Test verifies the commands are registered, bindings exist, and the
no-server status path lands.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c414954820
commit
304e54089f
|
|
@ -1503,3 +1503,48 @@ pmacs.keymap.bind { scope = "global", sequence = "C-c y", command = "lsp.semanti
|
|||
pmacs.keymap.bind { scope = "global", sequence = "C-c h", command = "lsp.hover" }
|
||||
pmacs.keymap.bind { scope = "global", sequence = "C-c s", command = "lsp.signature-help" }
|
||||
pmacs.keymap.bind { scope = "global", sequence = "C-c f", command = "lsp.format-buffer" }
|
||||
|
||||
-- Diagnostic navigation (task #23, M4.6 surface) -----------------------------
|
||||
--
|
||||
-- Emacs's `M-g n` / `M-g p` jump between compile/next-error locations. We
|
||||
-- reuse the chord for LSP diagnostics: walk the diag store for the active
|
||||
-- buffer's URI and move the cursor to the next/previous diagnostic. Wraps
|
||||
-- around (`pmacs.diag.next`'s default), so repeated taps cycle.
|
||||
local function navigate_diagnostic(direction)
|
||||
local rec = attached_for_active()
|
||||
if not rec then
|
||||
pmacs.editor.set_status("diag: no LSP server for active buffer")
|
||||
return
|
||||
end
|
||||
local line = pmacs.editor.cursor_line()
|
||||
local col = pmacs.editor.cursor_col()
|
||||
local found
|
||||
if direction == "next" then
|
||||
found = pmacs.diag.next(rec.uri, line, col)
|
||||
else
|
||||
found = pmacs.diag.previous(rec.uri, line, col)
|
||||
end
|
||||
if not found then
|
||||
pmacs.editor.set_status("diag: no diagnostics in buffer")
|
||||
return
|
||||
end
|
||||
pmacs.editor.push_jump()
|
||||
move_active_cursor_to(found.start_line, found.start_col)
|
||||
pmacs.editor.set_status(string.format("diag (%s): %s",
|
||||
found.severity or "?", found.message or ""))
|
||||
end
|
||||
|
||||
pmacs.command.define {
|
||||
name = "diag.next",
|
||||
description = "Jump to the next diagnostic in the active buffer (wraps).",
|
||||
fn = function() navigate_diagnostic("next") end,
|
||||
}
|
||||
|
||||
pmacs.command.define {
|
||||
name = "diag.previous",
|
||||
description = "Jump to the previous diagnostic in the active buffer (wraps).",
|
||||
fn = function() navigate_diagnostic("previous") end,
|
||||
}
|
||||
|
||||
pmacs.keymap.bind { scope = "global", sequence = "M-g n", command = "diag.next" }
|
||||
pmacs.keymap.bind { scope = "global", sequence = "M-g p", command = "diag.previous" }
|
||||
|
|
|
|||
|
|
@ -1754,6 +1754,69 @@ fn m4_6_lua_surface_reads_diagnostics() {
|
|||
);
|
||||
}
|
||||
|
||||
/// Task #23 follow-up: `lsp.lua` registers `diag.next` / `diag.previous`
|
||||
/// commands and binds them under `M-g n` / `M-g p`. The bindings cover
|
||||
/// the most-common Emacs convention for navigate-to-next-error.
|
||||
#[test]
|
||||
fn m4_6_diag_navigate_commands_and_bindings_are_registered() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let state = EditorState::new();
|
||||
let lua = state.lua_host.lua();
|
||||
|
||||
let commands: Vec<String> = lua
|
||||
.load("return pmacs.command.list()")
|
||||
.eval()
|
||||
.expect("command.list");
|
||||
assert!(
|
||||
commands.iter().any(|c| c == "diag.next"),
|
||||
"diag.next must be registered; got: {commands:?}"
|
||||
);
|
||||
assert!(
|
||||
commands.iter().any(|c| c == "diag.previous"),
|
||||
"diag.previous must be registered; got: {commands:?}"
|
||||
);
|
||||
|
||||
// Each binding is a row `{ scope, sequence, command }`. Project
|
||||
// those into a sortable string set so we can assert specific
|
||||
// bindings exist regardless of insertion order.
|
||||
let bindings: Vec<String> = lua
|
||||
.load(
|
||||
r"
|
||||
local out = {}
|
||||
for _, e in ipairs(pmacs.keymap.list()) do
|
||||
table.insert(out, e.sequence .. '=>' .. e.command)
|
||||
end
|
||||
return out
|
||||
",
|
||||
)
|
||||
.eval()
|
||||
.expect("keymap.list");
|
||||
assert!(
|
||||
bindings.iter().any(|b| b == "M-g n=>diag.next"),
|
||||
"M-g n must bind to diag.next; got: {bindings:?}"
|
||||
);
|
||||
assert!(
|
||||
bindings.iter().any(|b| b == "M-g p=>diag.previous"),
|
||||
"M-g p must bind to diag.previous; got: {bindings:?}"
|
||||
);
|
||||
|
||||
// Without an LSP attachment, the command should surface a status
|
||||
// message rather than fault or jump anywhere. (The scratch buffer
|
||||
// has no file path → no URI → `attached_for_active` returns nil.)
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
.load("pmacs.command.invoke('diag.next')")
|
||||
.exec()
|
||||
.expect("diag.next invoke");
|
||||
let status = state.core.borrow().status.clone();
|
||||
assert!(
|
||||
status.contains("no LSP server") || status.contains("no diagnostics"),
|
||||
"expected a diag-related status, got: {status:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Task #23: `pmacs.diag._attach_view` pushes a `DiagnosticView` onto
|
||||
/// the active window's overlay stack so the TUI grid renderer paints
|
||||
/// diagnostic underlines. Verifies the binding lands and that the
|
||||
|
|
|
|||
Loading…
Reference in New Issue