T M4.5: documentSymbol / workspace symbol / documentHighlight
Completes the read-only LSP feature set (everything except edits). Same request→store→Lua pattern as the nav batch; these three need new parsing (new response shapes), so they were deliberately split from the Location-family PR (#16). - src/symbol.rs: one flat Symbol type for both symbol requests. from_lsp_value handles BOTH LSP shapes — hierarchical DocumentSymbol[] (flattened with depth + parent chain) and flat SymbolInformation[]/WorkspaceSymbol[] (location.uri, range optional for WorkspaceSymbol). Scope-keyed (Document(uri) vs Workspace(query)) so an outline and a query don't collide. - src/document_highlight.rs: range + DocumentHighlightKind (absent defaults to Text=1 per spec), (server,uri)-keyed. - lsp.rs: three ResponseRoute variants + absorb arms + request methods. documentSymbol/documentHighlight ranges convert via the requested-doc codec; workspace/symbol results are cross-file → route uri "" → non-destructive passthrough (same rule as cross-file definition). - Lua: raw bindings + pmacs.document_symbol / .workspace_symbol / .document_highlight read surfaces (the new LSP Symbol is aliased to avoid the pre-existing project_index::Symbol name clash); lsp.lua wrappers + an lsp.document-symbols command on C-c o (modeline summary; outline buffer is future UX). - Tests: 6 parser unit tests (hierarchical depth/parent, flat SymbolInformation, range-less WorkspaceSymbol, highlight kind default, scope non-collision) + an e2e driving all three through the async bridge asserting shape correctness. Also includes a pre-existing rustfmt normalization of the #15 semantic-frontend files (protocol.rs / semantic_client.rs / semantic_render.rs) — main was not rustfmt-clean there after the #15 merge; bundled here per operator decision so the fmt gate is green. Gate: fmt clean; clippy --all-targets -D warnings clean; lib 1257/0; m4_acceptance 66/0; m9_1 18/0; m8_1/m8_9/m8_10 green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2f20c8a82f
commit
80c0679a5b
|
|
@ -259,6 +259,9 @@ pmacs.lsp.request_references = wrap_request(pmacs.lsp._request_references_raw)
|
||||||
pmacs.lsp.request_declaration = wrap_request(pmacs.lsp._request_declaration_raw)
|
pmacs.lsp.request_declaration = wrap_request(pmacs.lsp._request_declaration_raw)
|
||||||
pmacs.lsp.request_type_definition = wrap_request(pmacs.lsp._request_type_definition_raw)
|
pmacs.lsp.request_type_definition = wrap_request(pmacs.lsp._request_type_definition_raw)
|
||||||
pmacs.lsp.request_implementation = wrap_request(pmacs.lsp._request_implementation_raw)
|
pmacs.lsp.request_implementation = wrap_request(pmacs.lsp._request_implementation_raw)
|
||||||
|
pmacs.lsp.request_document_symbol = wrap_request(pmacs.lsp._request_document_symbol_raw)
|
||||||
|
pmacs.lsp.request_workspace_symbol = wrap_request(pmacs.lsp._request_workspace_symbol_raw)
|
||||||
|
pmacs.lsp.request_document_highlight = wrap_request(pmacs.lsp._request_document_highlight_raw)
|
||||||
|
|
||||||
-- Render an `:await()` failure into a modeline-friendly reason.
|
-- Render an `:await()` failure into a modeline-friendly reason.
|
||||||
-- `Handle:await()` raises `{ tag = "cancelled", ... }` when the
|
-- `Handle:await()` raises `{ tag = "cancelled", ... }` when the
|
||||||
|
|
@ -417,6 +420,37 @@ function pmacs.lsp.find_references()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function pmacs.lsp.document_symbols()
|
||||||
|
local rec = attached_for_active()
|
||||||
|
if not rec then
|
||||||
|
pmacs.editor.set_status("LSP: no server for active buffer")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
pmacs.document_symbol.clear(rec.server, rec.uri)
|
||||||
|
pmacs.async(function()
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
pmacs.lsp.request_document_symbol(rec.server, rec.uri):await()
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
pmacs.editor.set_status("LSP: " .. lsp_await_error(err))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local syms = pmacs.document_symbol.symbols(rec.server, rec.uri)
|
||||||
|
if not syms or #syms == 0 then
|
||||||
|
pmacs.editor.set_status("LSP: no symbols")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- v1 modeline summary (count + first symbol); a structured
|
||||||
|
-- outline buffer driven off this store is future UX work, like
|
||||||
|
-- the references list and hover panel.
|
||||||
|
local first = syms[1]
|
||||||
|
pmacs.editor.set_status(string.format(
|
||||||
|
"LSP: %d symbol%s; first '%s' at %d:%d",
|
||||||
|
#syms, (#syms == 1 and "" or "s"),
|
||||||
|
first.name, first.line + 1, first.col + 1))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
function pmacs.lsp.format_buffer()
|
function pmacs.lsp.format_buffer()
|
||||||
local rec = attached_for_active()
|
local rec = attached_for_active()
|
||||||
if not rec then
|
if not rec then
|
||||||
|
|
@ -531,12 +565,19 @@ pmacs.command.define {
|
||||||
fn = pmacs.lsp.find_references,
|
fn = pmacs.lsp.find_references,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pmacs.command.define {
|
||||||
|
name = "lsp.document-symbols",
|
||||||
|
description = "List the symbols (outline) of the active buffer (LSP).",
|
||||||
|
fn = pmacs.lsp.document_symbols,
|
||||||
|
}
|
||||||
|
|
||||||
-- Default chords. M-. follows the cross-editor convention for
|
-- Default chords. M-. follows the cross-editor convention for
|
||||||
-- go-to-definition; the others sit on `C-c` to keep printable letters
|
-- go-to-definition; the others sit on `C-c` to keep printable letters
|
||||||
-- self-inserting. The user can override or unbind any of these from
|
-- self-inserting. The user can override or unbind any of these from
|
||||||
-- init.lua.
|
-- init.lua.
|
||||||
pmacs.keymap.bind { scope = "global", sequence = "M-.", command = "lsp.go-to-definition" }
|
pmacs.keymap.bind { scope = "global", sequence = "M-.", command = "lsp.go-to-definition" }
|
||||||
pmacs.keymap.bind { scope = "global", sequence = "M-?", command = "lsp.find-references" }
|
pmacs.keymap.bind { scope = "global", sequence = "M-?", command = "lsp.find-references" }
|
||||||
|
pmacs.keymap.bind { scope = "global", sequence = "C-c o", command = "lsp.document-symbols" }
|
||||||
pmacs.keymap.bind { scope = "global", sequence = "C-c h", command = "lsp.hover" }
|
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 s", command = "lsp.signature-help" }
|
||||||
pmacs.keymap.bind { scope = "global", sequence = "C-c f", command = "lsp.format-buffer" }
|
pmacs.keymap.bind { scope = "global", sequence = "C-c f", command = "lsp.format-buffer" }
|
||||||
|
|
|
||||||
|
|
@ -387,6 +387,54 @@ fn main() {
|
||||||
});
|
});
|
||||||
write_frame(&mut stdout, &resp);
|
write_frame(&mut stdout, &resp);
|
||||||
}
|
}
|
||||||
|
// T M4.5 symbols/highlight. documentSymbol returns the
|
||||||
|
// *hierarchical* DocumentSymbol shape (exercises tree
|
||||||
|
// flatten + depth + parent); workspace/symbol the flat
|
||||||
|
// SymbolInformation shape (exercises location.uri);
|
||||||
|
// documentHighlight a two-occurrence list.
|
||||||
|
("textDocument/documentSymbol", Some(idv)) => {
|
||||||
|
let resp = serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": idv,
|
||||||
|
"result": [{
|
||||||
|
"name": "Outer", "kind": 5,
|
||||||
|
"range": { "start": { "line": 1, "character": 0 }, "end": { "line": 9, "character": 0 } },
|
||||||
|
"selectionRange": { "start": { "line": 1, "character": 6 }, "end": { "line": 1, "character": 11 } },
|
||||||
|
"children": [{
|
||||||
|
"name": "inner", "kind": 6,
|
||||||
|
"range": { "start": { "line": 3, "character": 2 }, "end": { "line": 5, "character": 2 } },
|
||||||
|
"selectionRange": { "start": { "line": 3, "character": 7 }, "end": { "line": 3, "character": 12 } }
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
write_frame(&mut stdout, &resp);
|
||||||
|
}
|
||||||
|
("workspace/symbol", Some(idv)) => {
|
||||||
|
let resp = serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": idv,
|
||||||
|
"result": [{
|
||||||
|
"name": "WsThing", "kind": 12,
|
||||||
|
"location": {
|
||||||
|
"uri": "file:///ws.rs",
|
||||||
|
"range": { "start": { "line": 7, "character": 3 }, "end": { "line": 7, "character": 10 } }
|
||||||
|
},
|
||||||
|
"containerName": "modw"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
write_frame(&mut stdout, &resp);
|
||||||
|
}
|
||||||
|
("textDocument/documentHighlight", Some(idv)) => {
|
||||||
|
let resp = serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": idv,
|
||||||
|
"result": [
|
||||||
|
{ "range": { "start": { "line": 2, "character": 4 }, "end": { "line": 2, "character": 9 } }, "kind": 2 },
|
||||||
|
{ "range": { "start": { "line": 6, "character": 0 }, "end": { "line": 6, "character": 5 } } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
write_frame(&mut stdout, &resp);
|
||||||
|
}
|
||||||
(_, Some(idv)) => {
|
(_, Some(idv)) => {
|
||||||
// Generic echo response.
|
// Generic echo response.
|
||||||
let resp = serde_json::json!({
|
let resp = serde_json::json!({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
// document_highlight.rs --- T M4.5: textDocument/documentHighlight.
|
||||||
|
|
||||||
|
//! Per-`(server, uri)` store for `textDocument/documentHighlight`:
|
||||||
|
//! the ranges in the current document that refer to the same symbol
|
||||||
|
//! as the cursor (the basis for "highlight all occurrences"). Same
|
||||||
|
//! request/store/Lua shape as the other M4.5 features; the inbound
|
||||||
|
//! position codec rewrites the ranges to byte offsets before this
|
||||||
|
//! parses, so consumers stay byte-uniform.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// One highlighted occurrence. `kind` is the raw LSP
|
||||||
|
/// `DocumentHighlightKind` (1 = Text, 2 = Read, 3 = Write); absent
|
||||||
|
/// defaults to 1 (Text) per spec.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Highlight {
|
||||||
|
/// Zero-based start line.
|
||||||
|
pub start_line: u32,
|
||||||
|
/// Zero-based start column.
|
||||||
|
pub start_col: u32,
|
||||||
|
/// Zero-based end line.
|
||||||
|
pub end_line: u32,
|
||||||
|
/// Zero-based end column.
|
||||||
|
pub end_col: u32,
|
||||||
|
/// `DocumentHighlightKind` (1 Text / 2 Read / 3 Write).
|
||||||
|
pub kind: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed response: zero or more occurrences in source order.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct DocumentHighlightResponse {
|
||||||
|
/// Occurrences the server returned.
|
||||||
|
pub highlights: Vec<Highlight>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentHighlightResponse {
|
||||||
|
/// Parse `DocumentHighlight[] | null`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_lsp_value(v: &Value) -> Self {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
for item in arr {
|
||||||
|
let Some(range) = item.get("range") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let (Some(start), Some(end)) = (range.get("start"), range.get("end")) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let g = |p: &Value, k: &str| p.get(k).and_then(Value::as_u64).map(|n| n as u32);
|
||||||
|
let (Some(sl), Some(sc), Some(el), Some(ec)) = (
|
||||||
|
g(start, "line"),
|
||||||
|
g(start, "character"),
|
||||||
|
g(end, "line"),
|
||||||
|
g(end, "character"),
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
out.push(Highlight {
|
||||||
|
start_line: sl,
|
||||||
|
start_col: sc,
|
||||||
|
end_line: el,
|
||||||
|
end_col: ec,
|
||||||
|
kind: item.get("kind").and_then(Value::as_i64).unwrap_or(1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self { highlights: out }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True iff the server returned nothing.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.highlights.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Key into [`DocumentHighlightStore`].
|
||||||
|
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
|
||||||
|
pub struct DocumentHighlightKey {
|
||||||
|
/// Decimal LSP server id.
|
||||||
|
pub server: String,
|
||||||
|
/// Document URI.
|
||||||
|
pub uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentHighlightKey {
|
||||||
|
/// Construct a key.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(server: impl Into<String>, uri: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
server: server.into(),
|
||||||
|
uri: uri.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-`(server, uri)` highlight state.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DocumentHighlightStore {
|
||||||
|
by_key: HashMap<DocumentHighlightKey, DocumentHighlightResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentHighlightStore {
|
||||||
|
/// Empty store.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the response at `key`.
|
||||||
|
pub fn set(&mut self, key: DocumentHighlightKey, response: DocumentHighlightResponse) {
|
||||||
|
self.by_key.insert(key, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop the entry at `key`.
|
||||||
|
pub fn clear(&mut self, key: &DocumentHighlightKey) {
|
||||||
|
self.by_key.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up the entry at `key`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, key: &DocumentHighlightKey) -> Option<&DocumentHighlightResponse> {
|
||||||
|
self.by_key.get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cheaply-cloneable shared handle.
|
||||||
|
pub type SharedDocumentHighlightStore = Arc<Mutex<DocumentHighlightStore>>;
|
||||||
|
|
||||||
|
/// Build a fresh shared store.
|
||||||
|
#[must_use]
|
||||||
|
pub fn make_shared_store() -> SharedDocumentHighlightStore {
|
||||||
|
Arc::new(Mutex::new(DocumentHighlightStore::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_highlights_and_defaults_kind_to_text() {
|
||||||
|
let v = json!([
|
||||||
|
{
|
||||||
|
"range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 5 } },
|
||||||
|
"kind": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"range": { "start": { "line": 4, "character": 0 }, "end": { "line": 4, "character": 3 } }
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
let r = DocumentHighlightResponse::from_lsp_value(&v);
|
||||||
|
assert_eq!(r.highlights.len(), 2);
|
||||||
|
assert_eq!(r.highlights[0].kind, 3); // Write
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
r.highlights[0].start_line,
|
||||||
|
r.highlights[0].start_col,
|
||||||
|
r.highlights[0].end_col
|
||||||
|
),
|
||||||
|
(1, 2, 5)
|
||||||
|
);
|
||||||
|
assert_eq!(r.highlights[1].kind, 1); // absent ⇒ Text
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn null_is_empty_and_store_round_trips() {
|
||||||
|
assert!(DocumentHighlightResponse::from_lsp_value(&Value::Null).is_empty());
|
||||||
|
let mut s = DocumentHighlightStore::new();
|
||||||
|
let key = DocumentHighlightKey::new("1", "file:///a");
|
||||||
|
s.set(
|
||||||
|
key.clone(),
|
||||||
|
DocumentHighlightResponse {
|
||||||
|
highlights: vec![Highlight {
|
||||||
|
start_line: 0,
|
||||||
|
start_col: 0,
|
||||||
|
end_line: 0,
|
||||||
|
end_col: 1,
|
||||||
|
kind: 2,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(s.get(&key).unwrap().highlights.len(), 1);
|
||||||
|
s.clear(&key);
|
||||||
|
assert!(s.get(&key).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,7 @@ pub mod daemon;
|
||||||
pub mod daemon_attach;
|
pub mod daemon_attach;
|
||||||
pub mod definition;
|
pub mod definition;
|
||||||
pub mod diag;
|
pub mod diag;
|
||||||
|
pub mod document_highlight;
|
||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod editor_core;
|
pub mod editor_core;
|
||||||
pub mod file_io;
|
pub mod file_io;
|
||||||
|
|
@ -100,6 +101,7 @@ pub mod semantic_client;
|
||||||
pub mod semantic_render;
|
pub mod semantic_render;
|
||||||
pub mod signature;
|
pub mod signature;
|
||||||
pub mod socket_path;
|
pub mod socket_path;
|
||||||
|
pub mod symbol;
|
||||||
pub mod syntax;
|
pub mod syntax;
|
||||||
pub mod text_view;
|
pub mod text_view;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
|
|
|
||||||
137
src/lsp.rs
137
src/lsp.rs
|
|
@ -749,6 +749,10 @@ pub struct LspManager {
|
||||||
/// implementation. Same Location shape as `definition`, keyed
|
/// implementation. Same Location shape as `definition`, keyed
|
||||||
/// additionally by kind so the four don't collide.
|
/// additionally by kind so the four don't collide.
|
||||||
locations_store: crate::locations::SharedLocationsStore,
|
locations_store: crate::locations::SharedLocationsStore,
|
||||||
|
/// T M4.5: documentSymbol / workspace symbol, scope-keyed.
|
||||||
|
symbol_store: crate::symbol::SharedSymbolStore,
|
||||||
|
/// T M4.5: textDocument/documentHighlight, per `(server, uri)`.
|
||||||
|
document_highlight_store: crate::document_highlight::SharedDocumentHighlightStore,
|
||||||
/// T M4.12 formatting store. Populated when a
|
/// T M4.12 formatting store. Populated when a
|
||||||
/// `textDocument/formatting` response lands.
|
/// `textDocument/formatting` response lands.
|
||||||
formatting_store: crate::formatting::SharedFormattingStore,
|
formatting_store: crate::formatting::SharedFormattingStore,
|
||||||
|
|
@ -818,6 +822,26 @@ enum ResponseRoute {
|
||||||
/// Which nav request this answers.
|
/// Which nav request this answers.
|
||||||
kind: crate::locations::LocationKind,
|
kind: crate::locations::LocationKind,
|
||||||
},
|
},
|
||||||
|
/// Absorb a `textDocument/documentSymbol` response into
|
||||||
|
/// [`crate::symbol::SymbolStore`] at `(server, document uri)`.
|
||||||
|
DocumentSymbol {
|
||||||
|
/// The requested document URI (also the codec doc key, and
|
||||||
|
/// the fallback URI for `DocumentSymbol` items which carry
|
||||||
|
/// none).
|
||||||
|
uri: String,
|
||||||
|
},
|
||||||
|
/// Absorb a `workspace/symbol` response into
|
||||||
|
/// [`crate::symbol::SymbolStore`] at `(server, query)`.
|
||||||
|
WorkspaceSymbol {
|
||||||
|
/// The query string this answers.
|
||||||
|
query: String,
|
||||||
|
},
|
||||||
|
/// Absorb a `textDocument/documentHighlight` response into
|
||||||
|
/// [`crate::document_highlight::DocumentHighlightStore`].
|
||||||
|
DocumentHighlight {
|
||||||
|
/// Document URI.
|
||||||
|
uri: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseRoute {
|
impl ResponseRoute {
|
||||||
|
|
@ -830,7 +854,14 @@ impl ResponseRoute {
|
||||||
| ResponseRoute::Signature { uri }
|
| ResponseRoute::Signature { uri }
|
||||||
| ResponseRoute::Definition { uri }
|
| ResponseRoute::Definition { uri }
|
||||||
| ResponseRoute::Formatting { uri }
|
| ResponseRoute::Formatting { uri }
|
||||||
| ResponseRoute::Locations { uri, .. } => uri,
|
| ResponseRoute::Locations { uri, .. }
|
||||||
|
| ResponseRoute::DocumentSymbol { uri }
|
||||||
|
| ResponseRoute::DocumentHighlight { uri } => uri,
|
||||||
|
// workspace/symbol results span arbitrary files we have
|
||||||
|
// not cached — no doc to convert against, so the inbound
|
||||||
|
// codec must pass coordinates through untouched (same
|
||||||
|
// non-destructive rule as cross-file definition).
|
||||||
|
ResponseRoute::WorkspaceSymbol { .. } => "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -901,6 +932,8 @@ impl LspManager {
|
||||||
signature_store: crate::signature::make_shared_store(),
|
signature_store: crate::signature::make_shared_store(),
|
||||||
definition_store: crate::definition::make_shared_store(),
|
definition_store: crate::definition::make_shared_store(),
|
||||||
locations_store: crate::locations::make_shared_store(),
|
locations_store: crate::locations::make_shared_store(),
|
||||||
|
symbol_store: crate::symbol::make_shared_store(),
|
||||||
|
document_highlight_store: crate::document_highlight::make_shared_store(),
|
||||||
formatting_store: crate::formatting::make_shared_store(),
|
formatting_store: crate::formatting::make_shared_store(),
|
||||||
pending_routes: HashMap::new(),
|
pending_routes: HashMap::new(),
|
||||||
status_tracker: crate::lsp_status::LspStatusTracker::new(),
|
status_tracker: crate::lsp_status::LspStatusTracker::new(),
|
||||||
|
|
@ -948,6 +981,20 @@ impl LspManager {
|
||||||
self.locations_store.clone()
|
self.locations_store.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shared symbol store — documentSymbol / workspace symbol (T M4.5).
|
||||||
|
#[must_use]
|
||||||
|
pub fn symbol_store(&self) -> crate::symbol::SharedSymbolStore {
|
||||||
|
self.symbol_store.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared documentHighlight store (T M4.5).
|
||||||
|
#[must_use]
|
||||||
|
pub fn document_highlight_store(
|
||||||
|
&self,
|
||||||
|
) -> crate::document_highlight::SharedDocumentHighlightStore {
|
||||||
|
self.document_highlight_store.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Shared formatting store (T M4.12).
|
/// Shared formatting store (T M4.12).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn formatting_store(&self) -> crate::formatting::SharedFormattingStore {
|
pub fn formatting_store(&self) -> crate::formatting::SharedFormattingStore {
|
||||||
|
|
@ -1590,6 +1637,63 @@ impl LspManager {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send `textDocument/documentSymbol` (no position — whole doc).
|
||||||
|
/// Returns the async-runtime [`JobId`].
|
||||||
|
pub fn request_document_symbol(
|
||||||
|
&mut self,
|
||||||
|
sid: LspServerId,
|
||||||
|
uri: impl Into<String>,
|
||||||
|
) -> Result<JobId, String> {
|
||||||
|
let uri = uri.into();
|
||||||
|
let params = json!({ "textDocument": { "uri": uri.clone() } });
|
||||||
|
let method = "textDocument/documentSymbol";
|
||||||
|
let req_id = self.send_request(sid, method, params)?;
|
||||||
|
let job_id = self.register_awaiter(sid, req_id, method, &uri);
|
||||||
|
self.pending_routes
|
||||||
|
.insert((sid, req_id), ResponseRoute::DocumentSymbol { uri });
|
||||||
|
Ok(job_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send `workspace/symbol` for `query`. Returns the [`JobId`].
|
||||||
|
pub fn request_workspace_symbol(
|
||||||
|
&mut self,
|
||||||
|
sid: LspServerId,
|
||||||
|
query: impl Into<String>,
|
||||||
|
) -> Result<JobId, String> {
|
||||||
|
let query = query.into();
|
||||||
|
let params = json!({ "query": query.clone() });
|
||||||
|
let method = "workspace/symbol";
|
||||||
|
let req_id = self.send_request(sid, method, params)?;
|
||||||
|
// The query stands in for the doc URI in the supersede key:
|
||||||
|
// a newer query for the same string supersedes the prior.
|
||||||
|
let job_id = self.register_awaiter(sid, req_id, method, &query);
|
||||||
|
self.pending_routes
|
||||||
|
.insert((sid, req_id), ResponseRoute::WorkspaceSymbol { query });
|
||||||
|
Ok(job_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send `textDocument/documentHighlight` at `(line, col)`.
|
||||||
|
/// Returns the [`JobId`].
|
||||||
|
pub fn request_document_highlight(
|
||||||
|
&mut self,
|
||||||
|
sid: LspServerId,
|
||||||
|
uri: impl Into<String>,
|
||||||
|
line: u32,
|
||||||
|
col: u32,
|
||||||
|
) -> Result<JobId, String> {
|
||||||
|
let uri = uri.into();
|
||||||
|
let params = json!({
|
||||||
|
"textDocument": { "uri": uri.clone() },
|
||||||
|
"position": self.outbound_position(sid, &uri, line, col)
|
||||||
|
});
|
||||||
|
let method = "textDocument/documentHighlight";
|
||||||
|
let req_id = self.send_request(sid, method, params)?;
|
||||||
|
let job_id = self.register_awaiter(sid, req_id, method, &uri);
|
||||||
|
self.pending_routes
|
||||||
|
.insert((sid, req_id), ResponseRoute::DocumentHighlight { uri });
|
||||||
|
Ok(job_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Send `textDocument/formatting` for `uri` with `tab_size` /
|
/// Send `textDocument/formatting` for `uri` with `tab_size` /
|
||||||
/// `insert_spaces` formatting options. The response is absorbed
|
/// `insert_spaces` formatting options. The response is absorbed
|
||||||
/// into the formatting store at `(sid, uri)`. Returns the
|
/// into the formatting store at `(sid, uri)`. Returns the
|
||||||
|
|
@ -2153,6 +2257,37 @@ impl LspManager {
|
||||||
.expect("locations store mutex poisoned");
|
.expect("locations store mutex poisoned");
|
||||||
guard.set(key, resp);
|
guard.set(key, resp);
|
||||||
}
|
}
|
||||||
|
ResponseRoute::DocumentSymbol { uri } => {
|
||||||
|
// DocumentSymbol items carry no URI — they belong to
|
||||||
|
// the requested document; pass it as the fallback.
|
||||||
|
let resp = crate::symbol::SymbolResponse::from_lsp_value(result, uri);
|
||||||
|
let key = crate::symbol::SymbolKey::document(server_key, uri.clone());
|
||||||
|
let mut guard = self
|
||||||
|
.symbol_store
|
||||||
|
.lock()
|
||||||
|
.expect("symbol store mutex poisoned");
|
||||||
|
guard.set(key, resp);
|
||||||
|
}
|
||||||
|
ResponseRoute::WorkspaceSymbol { query } => {
|
||||||
|
let resp = crate::symbol::SymbolResponse::from_lsp_value(result, "");
|
||||||
|
let key = crate::symbol::SymbolKey::workspace(server_key, query.clone());
|
||||||
|
let mut guard = self
|
||||||
|
.symbol_store
|
||||||
|
.lock()
|
||||||
|
.expect("symbol store mutex poisoned");
|
||||||
|
guard.set(key, resp);
|
||||||
|
}
|
||||||
|
ResponseRoute::DocumentHighlight { uri } => {
|
||||||
|
let resp =
|
||||||
|
crate::document_highlight::DocumentHighlightResponse::from_lsp_value(result);
|
||||||
|
let key =
|
||||||
|
crate::document_highlight::DocumentHighlightKey::new(server_key, uri.clone());
|
||||||
|
let mut guard = self
|
||||||
|
.document_highlight_store
|
||||||
|
.lock()
|
||||||
|
.expect("document highlight store mutex poisoned");
|
||||||
|
guard.set(key, resp);
|
||||||
|
}
|
||||||
ResponseRoute::Formatting { uri } => {
|
ResponseRoute::Formatting { uri } => {
|
||||||
let resp = crate::formatting::FormattingResponse::from_lsp_value(result);
|
let resp = crate::formatting::FormattingResponse::from_lsp_value(result);
|
||||||
let key = crate::formatting::FormattingKey::new(server_key, uri.clone());
|
let key = crate::formatting::FormattingKey::new(server_key, uri.clone());
|
||||||
|
|
|
||||||
|
|
@ -7264,6 +7264,50 @@ pub fn install_lsp(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let m = manager.clone();
|
||||||
|
lsp_mod.set(
|
||||||
|
"_request_document_symbol_raw",
|
||||||
|
lua.create_function(move |_, (id, uri): (LspServerIdLua, String)| {
|
||||||
|
let job_id = m
|
||||||
|
.borrow_mut()
|
||||||
|
.request_document_symbol(id.0, uri)
|
||||||
|
.map_err(mlua::Error::external)?;
|
||||||
|
Ok(job_id)
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let m = manager.clone();
|
||||||
|
lsp_mod.set(
|
||||||
|
"_request_workspace_symbol_raw",
|
||||||
|
lua.create_function(move |_, (id, query): (LspServerIdLua, String)| {
|
||||||
|
let job_id = m
|
||||||
|
.borrow_mut()
|
||||||
|
.request_workspace_symbol(id.0, query)
|
||||||
|
.map_err(mlua::Error::external)?;
|
||||||
|
Ok(job_id)
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let m = manager.clone();
|
||||||
|
lsp_mod.set(
|
||||||
|
"_request_document_highlight_raw",
|
||||||
|
lua.create_function(
|
||||||
|
move |_, (id, uri, line, col): (LspServerIdLua, String, u32, u32)| {
|
||||||
|
let job_id = m
|
||||||
|
.borrow_mut()
|
||||||
|
.request_document_highlight(id.0, uri, line, col)
|
||||||
|
.map_err(mlua::Error::external)?;
|
||||||
|
Ok(job_id)
|
||||||
|
},
|
||||||
|
)?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let m = manager.clone();
|
let m = manager.clone();
|
||||||
lsp_mod.set(
|
lsp_mod.set(
|
||||||
|
|
@ -7518,6 +7562,8 @@ pub fn make_lsp_manager(
|
||||||
install_signature(lua, &manager)?;
|
install_signature(lua, &manager)?;
|
||||||
install_definition(lua, &manager)?;
|
install_definition(lua, &manager)?;
|
||||||
install_locations(lua, &manager)?;
|
install_locations(lua, &manager)?;
|
||||||
|
install_symbol(lua, &manager)?;
|
||||||
|
install_document_highlight(lua, &manager)?;
|
||||||
install_formatting(lua, &manager)?;
|
install_formatting(lua, &manager)?;
|
||||||
Ok(manager)
|
Ok(manager)
|
||||||
}
|
}
|
||||||
|
|
@ -8315,10 +8361,12 @@ pub fn install_diag(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> {
|
||||||
|
|
||||||
use crate::completion::{CompletionItem, CompletionItemKind, CompletionKey, CompletionTriggers};
|
use crate::completion::{CompletionItem, CompletionItemKind, CompletionKey, CompletionTriggers};
|
||||||
use crate::definition::{DefinitionKey, DefinitionLocation, DefinitionResponse};
|
use crate::definition::{DefinitionKey, DefinitionLocation, DefinitionResponse};
|
||||||
|
use crate::document_highlight::{DocumentHighlightKey, Highlight};
|
||||||
use crate::formatting::{FormattingKey, FormattingResponse, TextEdit};
|
use crate::formatting::{FormattingKey, FormattingResponse, TextEdit};
|
||||||
use crate::hover::{Hover, HoverKey};
|
use crate::hover::{Hover, HoverKey};
|
||||||
use crate::locations::{LocationKind, LocationsKey};
|
use crate::locations::{LocationKind, LocationsKey};
|
||||||
use crate::signature::{Signature, SignatureHelp, SignatureKey, SignatureParameter};
|
use crate::signature::{Signature, SignatureHelp, SignatureKey, SignatureParameter};
|
||||||
|
use crate::symbol::{Symbol as LspSymbol, SymbolKey};
|
||||||
|
|
||||||
fn completion_item_to_lua(lua: &Lua, item: &CompletionItem) -> mlua::Result<Table> {
|
fn completion_item_to_lua(lua: &Lua, item: &CompletionItem) -> mlua::Result<Table> {
|
||||||
let t = lua.create_table_with_capacity(0, 7)?;
|
let t = lua.create_table_with_capacity(0, 7)?;
|
||||||
|
|
@ -8784,6 +8832,147 @@ pub fn install_locations(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn symbol_to_lua(lua: &Lua, s: &LspSymbol) -> mlua::Result<Table> {
|
||||||
|
let t = lua.create_table_with_capacity(0, 7)?;
|
||||||
|
t.set("name", s.name.as_str())?;
|
||||||
|
t.set("kind", s.kind)?;
|
||||||
|
t.set("uri", s.uri.as_str())?;
|
||||||
|
t.set("line", s.line)?;
|
||||||
|
t.set("col", s.col)?;
|
||||||
|
t.set("depth", s.depth)?;
|
||||||
|
if let Some(c) = &s.container {
|
||||||
|
t.set("container", c.as_str())?;
|
||||||
|
}
|
||||||
|
Ok(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_to_lua(lua: &Lua, h: &Highlight) -> mlua::Result<Table> {
|
||||||
|
let t = lua.create_table_with_capacity(0, 5)?;
|
||||||
|
t.set("start_line", h.start_line)?;
|
||||||
|
t.set("start_col", h.start_col)?;
|
||||||
|
t.set("end_line", h.end_line)?;
|
||||||
|
t.set("end_col", h.end_col)?;
|
||||||
|
t.set("kind", h.kind)?;
|
||||||
|
Ok(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install `pmacs.document_symbol` (`symbols(sid,uri)` / `clear`) and
|
||||||
|
/// `pmacs.workspace_symbol` (`symbols(sid,query)` / `clear`) over the
|
||||||
|
/// scope-keyed symbol store. T M4.5.
|
||||||
|
pub fn install_symbol(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> {
|
||||||
|
let pmacs: Table = lua.globals().get("pmacs")?;
|
||||||
|
|
||||||
|
let doc = lua.create_table()?;
|
||||||
|
{
|
||||||
|
let mgr = manager.clone();
|
||||||
|
doc.set(
|
||||||
|
"symbols",
|
||||||
|
lua.create_function(move |lua, (id, uri): (LspServerIdLua, String)| {
|
||||||
|
let store = mgr.borrow().symbol_store();
|
||||||
|
let guard = store.lock().expect("symbol store mutex poisoned");
|
||||||
|
let key = SymbolKey::document(id.0.raw().to_string(), uri);
|
||||||
|
let out = lua.create_table()?;
|
||||||
|
if let Some(r) = guard.get(&key) {
|
||||||
|
for (i, s) in r.symbols.iter().enumerate() {
|
||||||
|
out.set(i + 1, symbol_to_lua(lua, s)?)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Value::Table(out))
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mgr = manager.clone();
|
||||||
|
doc.set(
|
||||||
|
"clear",
|
||||||
|
lua.create_function(move |_, (id, uri): (LspServerIdLua, String)| {
|
||||||
|
let store = mgr.borrow().symbol_store();
|
||||||
|
let mut guard = store.lock().expect("symbol store mutex poisoned");
|
||||||
|
guard.clear(&SymbolKey::document(id.0.raw().to_string(), uri));
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
pmacs.set("document_symbol", doc)?;
|
||||||
|
|
||||||
|
let ws = lua.create_table()?;
|
||||||
|
{
|
||||||
|
let mgr = manager.clone();
|
||||||
|
ws.set(
|
||||||
|
"symbols",
|
||||||
|
lua.create_function(move |lua, (id, query): (LspServerIdLua, String)| {
|
||||||
|
let store = mgr.borrow().symbol_store();
|
||||||
|
let guard = store.lock().expect("symbol store mutex poisoned");
|
||||||
|
let key = SymbolKey::workspace(id.0.raw().to_string(), query);
|
||||||
|
let out = lua.create_table()?;
|
||||||
|
if let Some(r) = guard.get(&key) {
|
||||||
|
for (i, s) in r.symbols.iter().enumerate() {
|
||||||
|
out.set(i + 1, symbol_to_lua(lua, s)?)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Value::Table(out))
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mgr = manager.clone();
|
||||||
|
ws.set(
|
||||||
|
"clear",
|
||||||
|
lua.create_function(move |_, (id, query): (LspServerIdLua, String)| {
|
||||||
|
let store = mgr.borrow().symbol_store();
|
||||||
|
let mut guard = store.lock().expect("symbol store mutex poisoned");
|
||||||
|
guard.clear(&SymbolKey::workspace(id.0.raw().to_string(), query));
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
pmacs.set("workspace_symbol", ws)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install `pmacs.document_highlight` (`highlights(sid,uri)` /
|
||||||
|
/// `clear`). T M4.5.
|
||||||
|
pub fn install_document_highlight(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> {
|
||||||
|
let pmacs: Table = lua.globals().get("pmacs")?;
|
||||||
|
let m = lua.create_table()?;
|
||||||
|
{
|
||||||
|
let mgr = manager.clone();
|
||||||
|
m.set(
|
||||||
|
"highlights",
|
||||||
|
lua.create_function(move |lua, (id, uri): (LspServerIdLua, String)| {
|
||||||
|
let store = mgr.borrow().document_highlight_store();
|
||||||
|
let guard = store
|
||||||
|
.lock()
|
||||||
|
.expect("document highlight store mutex poisoned");
|
||||||
|
let key = DocumentHighlightKey::new(id.0.raw().to_string(), uri);
|
||||||
|
let out = lua.create_table()?;
|
||||||
|
if let Some(r) = guard.get(&key) {
|
||||||
|
for (i, h) in r.highlights.iter().enumerate() {
|
||||||
|
out.set(i + 1, highlight_to_lua(lua, h)?)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Value::Table(out))
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mgr = manager.clone();
|
||||||
|
m.set(
|
||||||
|
"clear",
|
||||||
|
lua.create_function(move |_, (id, uri): (LspServerIdLua, String)| {
|
||||||
|
let store = mgr.borrow().document_highlight_store();
|
||||||
|
let mut guard = store
|
||||||
|
.lock()
|
||||||
|
.expect("document highlight store mutex poisoned");
|
||||||
|
guard.clear(&DocumentHighlightKey::new(id.0.raw().to_string(), uri));
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
pmacs.set("document_highlight", m)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn text_edit_to_lua(lua: &Lua, edit: &TextEdit) -> mlua::Result<Table> {
|
fn text_edit_to_lua(lua: &Lua, edit: &TextEdit) -> mlua::Result<Table> {
|
||||||
let t = lua.create_table_with_capacity(0, 5)?;
|
let t = lua.create_table_with_capacity(0, 5)?;
|
||||||
t.set("start_line", edit.start_line)?;
|
t.set("start_line", edit.start_line)?;
|
||||||
|
|
|
||||||
|
|
@ -3943,13 +3943,19 @@ mod tests {
|
||||||
buffer_id: bid,
|
buffer_id: bid,
|
||||||
items: vec![BlockAdornment {
|
items: vec![BlockAdornment {
|
||||||
at: 64,
|
at: 64,
|
||||||
replaces: Some(ByteRange { start: 64, end: 256 }),
|
replaces: Some(ByteRange {
|
||||||
|
start: 64,
|
||||||
|
end: 256,
|
||||||
|
}),
|
||||||
content: AdornmentContent::Resource { handle: 1 },
|
content: AdornmentContent::Resource { handle: 1 },
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
InstanceMessage::FoldState {
|
InstanceMessage::FoldState {
|
||||||
buffer_id: bid,
|
buffer_id: bid,
|
||||||
folds: vec![ByteRange { start: 100, end: 400 }],
|
folds: vec![ByteRange {
|
||||||
|
start: 100,
|
||||||
|
end: 400,
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
InstanceMessage::ResourceOffer {
|
InstanceMessage::ResourceOffer {
|
||||||
handle: 1,
|
handle: 1,
|
||||||
|
|
|
||||||
|
|
@ -332,10 +332,12 @@ impl SemanticClient {
|
||||||
/// declared viewport, or no styling there).
|
/// declared viewport, or no styling there).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn effective_style_at(&self, buffer_id: BufferId, byte: u64) -> Style {
|
pub fn effective_style_at(&self, buffer_id: BufferId, byte: u64) -> Style {
|
||||||
self.styles.get(&buffer_id).map_or_else(Style::default, |m| {
|
self.styles
|
||||||
m.items_at(byte)
|
.get(&buffer_id)
|
||||||
.fold(Style::default(), |acc, s| merge_styles(acc, s.style))
|
.map_or_else(Style::default, |m| {
|
||||||
})
|
m.items_at(byte)
|
||||||
|
.fold(Style::default(), |acc, s| merge_styles(acc, s.style))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The decoration kinds covering `byte`, in instance order
|
/// The decoration kinds covering `byte`, in instance order
|
||||||
|
|
@ -343,9 +345,9 @@ impl SemanticClient {
|
||||||
/// selection and a diagnostic).
|
/// selection and a diagnostic).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn decoration_kinds_at(&self, buffer_id: BufferId, byte: u64) -> Vec<DecorationKind> {
|
pub fn decoration_kinds_at(&self, buffer_id: BufferId, byte: u64) -> Vec<DecorationKind> {
|
||||||
self.decos.get(&buffer_id).map_or_else(Vec::new, |m| {
|
self.decos
|
||||||
m.items_at(byte).map(|d| d.kind).collect()
|
.get(&buffer_id)
|
||||||
})
|
.map_or_else(Vec::new, |m| m.items_at(byte).map(|d| d.kind).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconstructed styling tile ranges for `buffer_id` — for
|
/// Reconstructed styling tile ranges for `buffer_id` — for
|
||||||
|
|
|
||||||
|
|
@ -246,11 +246,7 @@ impl SemanticRenderState {
|
||||||
/// frontend already owns (it has `CursorByte`) — emitting it would
|
/// frontend already owns (it has `CursorByte`) — emitting it would
|
||||||
/// couple a visual-motion concern to the instance, against the
|
/// couple a visual-motion concern to the instance, against the
|
||||||
/// contract boundary.
|
/// contract boundary.
|
||||||
fn scoped_decorations(
|
fn scoped_decorations(&self, state: &EditorState, vp: &DeclaredViewport) -> Vec<Decoration> {
|
||||||
&self,
|
|
||||||
state: &EditorState,
|
|
||||||
vp: &DeclaredViewport,
|
|
||||||
) -> Vec<Decoration> {
|
|
||||||
let core = state.core.borrow();
|
let core = state.core.borrow();
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
|
@ -585,7 +581,10 @@ mod tests {
|
||||||
msgs.iter().find_map(|m| match m {
|
msgs.iter().find_map(|m| match m {
|
||||||
InstanceMessage::Decorations { full, segments, .. } => Some((
|
InstanceMessage::Decorations { full, segments, .. } => Some((
|
||||||
*full,
|
*full,
|
||||||
segments.iter().flat_map(|s| s.decorations.clone()).collect(),
|
segments
|
||||||
|
.iter()
|
||||||
|
.flat_map(|s| s.decorations.clone())
|
||||||
|
.collect(),
|
||||||
)),
|
)),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
|
|
@ -655,7 +654,14 @@ mod tests {
|
||||||
let state = empty_state();
|
let state = empty_state();
|
||||||
let mut s = local();
|
let mut s = local();
|
||||||
let buffer_id = active_buffer(&state);
|
let buffer_id = active_buffer(&state);
|
||||||
s.set_viewport(buffer_id, ByteRange { start: 0, end: 4096 }, 0);
|
s.set_viewport(
|
||||||
|
buffer_id,
|
||||||
|
ByteRange {
|
||||||
|
start: 0,
|
||||||
|
end: 4096,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
// Empty scratch: no spans, no selection, no diagnostics — but
|
// Empty scratch: no spans, no selection, no diagnostics — but
|
||||||
// the first frame is a `full` resync for both families (the
|
// the first frame is a `full` resync for both families (the
|
||||||
|
|
@ -745,11 +751,21 @@ mod tests {
|
||||||
|
|
||||||
// Declaring a different on-screen range forces a full resync:
|
// Declaring a different on-screen range forces a full resync:
|
||||||
// prior styling/decorations are positioned for the old window.
|
// prior styling/decorations are positioned for the old window.
|
||||||
s.set_viewport(buffer_id, ByteRange { start: 200, end: 264 }, 0);
|
s.set_viewport(
|
||||||
|
buffer_id,
|
||||||
|
ByteRange {
|
||||||
|
start: 200,
|
||||||
|
end: 264,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
let msgs = s.render_frame(&state);
|
let msgs = s.render_frame(&state);
|
||||||
let (style_full, _) = style_segments(&msgs).expect("StyleSpans");
|
let (style_full, _) = style_segments(&msgs).expect("StyleSpans");
|
||||||
let (deco_full, _) = decorations_of(&msgs).expect("Decorations");
|
let (deco_full, _) = decorations_of(&msgs).expect("Decorations");
|
||||||
assert!(style_full && deco_full, "viewport jump must be a full resync");
|
assert!(
|
||||||
|
style_full && deco_full,
|
||||||
|
"viewport jump must be a full resync"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,311 @@
|
||||||
|
// symbol.rs --- T M4.5: documentSymbol / workspace symbol.
|
||||||
|
|
||||||
|
//! Shared store for `textDocument/documentSymbol` and
|
||||||
|
//! `workspace/symbol`. Both ultimately describe "a named program
|
||||||
|
//! entity at a location", so one flat [`Symbol`] type serves both;
|
||||||
|
//! the only differences are (a) the request response can arrive in
|
||||||
|
//! two LSP shapes — hierarchical `DocumentSymbol[]` or flat
|
||||||
|
//! `SymbolInformation[]` / `WorkspaceSymbol[]` — handled by
|
||||||
|
//! [`SymbolResponse::from_lsp_value`], and (b) the store is keyed by
|
||||||
|
//! a [`SymbolScope`] so a per-document outline and a workspace query
|
||||||
|
//! don't collide.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// One symbol, flattened. `kind` is the raw LSP `SymbolKind` integer
|
||||||
|
/// (1..=26); consumers map it to a label. Coordinates are LSP-native
|
||||||
|
/// (the inbound position codec rewrites them to byte offsets before
|
||||||
|
/// this parses, for the document-symbol case where they belong to
|
||||||
|
/// the requested doc; workspace-symbol locations are cross-file and
|
||||||
|
/// pass through unconverted).
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Symbol {
|
||||||
|
/// Symbol name.
|
||||||
|
pub name: String,
|
||||||
|
/// Raw LSP `SymbolKind` (1..=26).
|
||||||
|
pub kind: i64,
|
||||||
|
/// Containing document URI.
|
||||||
|
pub uri: String,
|
||||||
|
/// Zero-based line of the symbol's name.
|
||||||
|
pub line: u32,
|
||||||
|
/// Zero-based column of the symbol's name.
|
||||||
|
pub col: u32,
|
||||||
|
/// `containerName` (flat shapes) or the parent chain joined with
|
||||||
|
/// `::` (hierarchical), if any.
|
||||||
|
pub container: Option<String>,
|
||||||
|
/// Nesting depth in a hierarchical `DocumentSymbol` tree; 0 for
|
||||||
|
/// flat shapes.
|
||||||
|
pub depth: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed symbol response: a flat, source-ordered list.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct SymbolResponse {
|
||||||
|
/// Symbols in document/source order, parents before children.
|
||||||
|
pub symbols: Vec<Symbol>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_start(v: &Value, key: &str) -> Option<(u32, u32)> {
|
||||||
|
let start = v.get(key)?.get("start")?;
|
||||||
|
Some((
|
||||||
|
start.get("line")?.as_u64()? as u32,
|
||||||
|
start.get("character")?.as_u64()? as u32,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolResponse {
|
||||||
|
/// Parse `DocumentSymbol[] | SymbolInformation[] |
|
||||||
|
/// WorkspaceSymbol[] | null`. `default_uri` is the requested
|
||||||
|
/// document — used for `DocumentSymbol`, which carries no URI
|
||||||
|
/// (its positions are relative to the requested document).
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_lsp_value(v: &Value, default_uri: &str) -> Self {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
for item in arr {
|
||||||
|
if item.get("location").is_some() {
|
||||||
|
// SymbolInformation / WorkspaceSymbol (flat).
|
||||||
|
Self::push_flat(item, &mut out);
|
||||||
|
} else {
|
||||||
|
// DocumentSymbol (hierarchical).
|
||||||
|
Self::push_hier(item, default_uri, None, 0, &mut out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self { symbols: out }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_flat(item: &Value, out: &mut Vec<Symbol>) {
|
||||||
|
let Some(name) = item.get("name").and_then(Value::as_str) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let kind = item.get("kind").and_then(Value::as_i64).unwrap_or(0);
|
||||||
|
let loc = item.get("location");
|
||||||
|
let uri = loc
|
||||||
|
.and_then(|l| l.get("uri"))
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_owned();
|
||||||
|
// WorkspaceSymbol may carry `location: { uri }` with no range.
|
||||||
|
let (line, col) = loc.and_then(|l| range_start(l, "range")).unwrap_or((0, 0));
|
||||||
|
let container = item
|
||||||
|
.get("containerName")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(ToOwned::to_owned);
|
||||||
|
out.push(Symbol {
|
||||||
|
name: name.to_owned(),
|
||||||
|
kind,
|
||||||
|
uri,
|
||||||
|
line,
|
||||||
|
col,
|
||||||
|
container,
|
||||||
|
depth: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_hier(item: &Value, uri: &str, parent: Option<&str>, depth: u32, out: &mut Vec<Symbol>) {
|
||||||
|
let Some(name) = item.get("name").and_then(Value::as_str) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let kind = item.get("kind").and_then(Value::as_i64).unwrap_or(0);
|
||||||
|
// `selectionRange` is the name; `range` the whole body.
|
||||||
|
let (line, col) = range_start(item, "selectionRange")
|
||||||
|
.or_else(|| range_start(item, "range"))
|
||||||
|
.unwrap_or((0, 0));
|
||||||
|
out.push(Symbol {
|
||||||
|
name: name.to_owned(),
|
||||||
|
kind,
|
||||||
|
uri: uri.to_owned(),
|
||||||
|
line,
|
||||||
|
col,
|
||||||
|
container: parent.map(ToOwned::to_owned),
|
||||||
|
depth,
|
||||||
|
});
|
||||||
|
if let Some(children) = item.get("children").and_then(Value::as_array) {
|
||||||
|
let child_parent = match parent {
|
||||||
|
Some(p) => format!("{p}::{name}"),
|
||||||
|
None => name.to_owned(),
|
||||||
|
};
|
||||||
|
for c in children {
|
||||||
|
Self::push_hier(c, uri, Some(&child_parent), depth + 1, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True iff the server returned no symbols.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.symbols.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a stored [`SymbolResponse`] answers.
|
||||||
|
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
|
||||||
|
pub enum SymbolScope {
|
||||||
|
/// `textDocument/documentSymbol` for a document URI.
|
||||||
|
Document(String),
|
||||||
|
/// `workspace/symbol` for a query string.
|
||||||
|
Workspace(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Key into [`SymbolStore`].
|
||||||
|
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
|
||||||
|
pub struct SymbolKey {
|
||||||
|
/// Decimal LSP server id.
|
||||||
|
pub server: String,
|
||||||
|
/// Document URI or workspace query.
|
||||||
|
pub scope: SymbolScope,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolKey {
|
||||||
|
/// Key for a document outline.
|
||||||
|
#[must_use]
|
||||||
|
pub fn document(server: impl Into<String>, uri: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
server: server.into(),
|
||||||
|
scope: SymbolScope::Document(uri.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Key for a workspace query.
|
||||||
|
#[must_use]
|
||||||
|
pub fn workspace(server: impl Into<String>, query: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
server: server.into(),
|
||||||
|
scope: SymbolScope::Workspace(query.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-`(server, scope)` symbol state.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SymbolStore {
|
||||||
|
by_key: HashMap<SymbolKey, SymbolResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolStore {
|
||||||
|
/// Empty store.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the response at `key`.
|
||||||
|
pub fn set(&mut self, key: SymbolKey, response: SymbolResponse) {
|
||||||
|
self.by_key.insert(key, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop the entry at `key`.
|
||||||
|
pub fn clear(&mut self, key: &SymbolKey) {
|
||||||
|
self.by_key.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up the entry at `key`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, key: &SymbolKey) -> Option<&SymbolResponse> {
|
||||||
|
self.by_key.get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cheaply-cloneable shared handle.
|
||||||
|
pub type SharedSymbolStore = Arc<Mutex<SymbolStore>>;
|
||||||
|
|
||||||
|
/// Build a fresh shared store.
|
||||||
|
#[must_use]
|
||||||
|
pub fn make_shared_store() -> SharedSymbolStore {
|
||||||
|
Arc::new(Mutex::new(SymbolStore::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_hierarchical_document_symbols_with_depth_and_parent() {
|
||||||
|
let v = json!([{
|
||||||
|
"name": "Outer", "kind": 5,
|
||||||
|
"range": { "start": { "line": 0, "character": 0 }, "end": { "line": 9, "character": 0 } },
|
||||||
|
"selectionRange": { "start": { "line": 0, "character": 6 }, "end": { "line": 0, "character": 11 } },
|
||||||
|
"children": [{
|
||||||
|
"name": "method", "kind": 6,
|
||||||
|
"range": { "start": { "line": 2, "character": 2 }, "end": { "line": 4, "character": 2 } },
|
||||||
|
"selectionRange": { "start": { "line": 2, "character": 7 }, "end": { "line": 2, "character": 13 } }
|
||||||
|
}]
|
||||||
|
}]);
|
||||||
|
let r = SymbolResponse::from_lsp_value(&v, "file:///m.rs");
|
||||||
|
assert_eq!(r.symbols.len(), 2);
|
||||||
|
assert_eq!(r.symbols[0].name, "Outer");
|
||||||
|
assert_eq!(r.symbols[0].depth, 0);
|
||||||
|
assert_eq!((r.symbols[0].line, r.symbols[0].col), (0, 6)); // selectionRange
|
||||||
|
assert_eq!(r.symbols[0].uri, "file:///m.rs"); // DocumentSymbol carries none
|
||||||
|
assert_eq!(r.symbols[1].name, "method");
|
||||||
|
assert_eq!(r.symbols[1].depth, 1);
|
||||||
|
assert_eq!(r.symbols[1].container.as_deref(), Some("Outer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_flat_symbol_information() {
|
||||||
|
let v = json!([{
|
||||||
|
"name": "Thing", "kind": 23,
|
||||||
|
"location": {
|
||||||
|
"uri": "file:///a.rs",
|
||||||
|
"range": { "start": { "line": 4, "character": 8 }, "end": { "line": 4, "character": 13 } }
|
||||||
|
},
|
||||||
|
"containerName": "modx"
|
||||||
|
}]);
|
||||||
|
let r = SymbolResponse::from_lsp_value(&v, "file:///ignored");
|
||||||
|
assert_eq!(r.symbols.len(), 1);
|
||||||
|
assert_eq!(r.symbols[0].uri, "file:///a.rs"); // from location, not default
|
||||||
|
assert_eq!((r.symbols[0].line, r.symbols[0].col), (4, 8));
|
||||||
|
assert_eq!(r.symbols[0].container.as_deref(), Some("modx"));
|
||||||
|
assert_eq!(r.symbols[0].depth, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_symbol_without_range_defaults_to_origin() {
|
||||||
|
// WorkspaceSymbol permits `location: { uri }` with no range.
|
||||||
|
let v = json!([{ "name": "Z", "kind": 12, "location": { "uri": "file:///z.go" } }]);
|
||||||
|
let r = SymbolResponse::from_lsp_value(&v, "");
|
||||||
|
assert_eq!(r.symbols.len(), 1);
|
||||||
|
assert_eq!(r.symbols[0].uri, "file:///z.go");
|
||||||
|
assert_eq!((r.symbols[0].line, r.symbols[0].col), (0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn null_is_empty_and_scopes_do_not_collide() {
|
||||||
|
assert!(SymbolResponse::from_lsp_value(&Value::Null, "x").is_empty());
|
||||||
|
let mut s = SymbolStore::new();
|
||||||
|
let one = |n: &str| SymbolResponse {
|
||||||
|
symbols: vec![Symbol {
|
||||||
|
name: n.into(),
|
||||||
|
kind: 1,
|
||||||
|
uri: "u".into(),
|
||||||
|
line: 0,
|
||||||
|
col: 0,
|
||||||
|
container: None,
|
||||||
|
depth: 0,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
s.set(SymbolKey::document("1", "file:///a"), one("doc"));
|
||||||
|
s.set(SymbolKey::workspace("1", "file:///a"), one("ws"));
|
||||||
|
assert_eq!(
|
||||||
|
s.get(&SymbolKey::document("1", "file:///a"))
|
||||||
|
.unwrap()
|
||||||
|
.symbols[0]
|
||||||
|
.name,
|
||||||
|
"doc"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
s.get(&SymbolKey::workspace("1", "file:///a"))
|
||||||
|
.unwrap()
|
||||||
|
.symbols[0]
|
||||||
|
.name,
|
||||||
|
"ws"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4015,3 +4015,67 @@ fn m4_5_location_nav_requests_route_by_kind() {
|
||||||
assert_eq!(tdef, 31, "typeDefinition must route to its own slot");
|
assert_eq!(tdef, 31, "typeDefinition must route to its own slot");
|
||||||
assert_eq!(impl_, 41, "implementation must route to its own slot");
|
assert_eq!(impl_, 41, "implementation must route to its own slot");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// T M4.5 symbols/highlight: documentSymbol (hierarchical → flattened
|
||||||
|
/// with depth + parent), workspace/symbol (flat, location.uri), and
|
||||||
|
/// documentHighlight (range + kind, default 1) each await end-to-end
|
||||||
|
/// and land in their store with the right shape.
|
||||||
|
#[test]
|
||||||
|
fn m4_5_symbols_and_highlight_round_trip() {
|
||||||
|
use pmacs::editor::EditorState;
|
||||||
|
let mut state = EditorState::new();
|
||||||
|
spawn_lsp_and_init(&mut state, None);
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(
|
||||||
|
"local uri='file:///s.rs'
|
||||||
|
pmacs.lsp.did_open(_G._lsp, uri, 1, 'mod m {}\\n')
|
||||||
|
_G._done=false
|
||||||
|
pmacs.async(function()
|
||||||
|
pmacs.lsp.request_document_symbol(_G._lsp, uri):await()
|
||||||
|
pmacs.lsp.request_workspace_symbol(_G._lsp, 'q'):await()
|
||||||
|
pmacs.lsp.request_document_highlight(_G._lsp, uri, 0, 4):await()
|
||||||
|
local ds = pmacs.document_symbol.symbols(_G._lsp, uri)
|
||||||
|
local ws = pmacs.workspace_symbol.symbols(_G._lsp, 'q')
|
||||||
|
local dh = pmacs.document_highlight.highlights(_G._lsp, uri)
|
||||||
|
_G._ds_n = ds and #ds or 0
|
||||||
|
_G._ds1 = ds and ds[1] and ds[1].name or ''
|
||||||
|
_G._ds2 = ds and ds[2] and ds[2].name or ''
|
||||||
|
_G._ds2d = ds and ds[2] and ds[2].depth or -1
|
||||||
|
_G._ds2c = ds and ds[2] and ds[2].container or ''
|
||||||
|
_G._ws_uri = ws and ws[1] and ws[1].uri or ''
|
||||||
|
_G._ws_ctr = ws and ws[1] and ws[1].container or ''
|
||||||
|
_G._dh_n = dh and #dh or 0
|
||||||
|
_G._dh1k = dh and dh[1] and dh[1].kind or -1
|
||||||
|
_G._dh2k = dh and dh[2] and dh[2].kind or -1
|
||||||
|
_G._done=true
|
||||||
|
end)",
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.expect("dispatch symbols/highlight coroutine");
|
||||||
|
assert!(
|
||||||
|
pump_lua_flag(&mut state, "_G._done", 5),
|
||||||
|
"symbols/highlight coroutine never completed"
|
||||||
|
);
|
||||||
|
let lua = state.lua_host.lua();
|
||||||
|
let g = |k: &str| -> String {
|
||||||
|
lua.load(format!("return tostring(_G.{k})"))
|
||||||
|
.eval()
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
assert_eq!(g("_ds_n"), "2", "documentSymbol flattens parent+child");
|
||||||
|
assert_eq!(g("_ds1"), "Outer");
|
||||||
|
assert_eq!(g("_ds2"), "inner");
|
||||||
|
assert_eq!(g("_ds2d"), "1", "child depth is 1");
|
||||||
|
assert_eq!(g("_ds2c"), "Outer", "child container is the parent");
|
||||||
|
assert_eq!(
|
||||||
|
g("_ws_uri"),
|
||||||
|
"file:///ws.rs",
|
||||||
|
"workspace symbol location.uri"
|
||||||
|
);
|
||||||
|
assert_eq!(g("_ws_ctr"), "modw");
|
||||||
|
assert_eq!(g("_dh_n"), "2");
|
||||||
|
assert_eq!(g("_dh1k"), "2", "explicit DocumentHighlightKind (Read)");
|
||||||
|
assert_eq!(g("_dh2k"), "1", "absent kind defaults to Text(1)");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue