T M4.5: nav requests — references/declaration/typeDefinition/implementation
The "cheap batch" subset that is genuine template-fill: these four
return the exact `Location | Location[] | LocationLink[] | null`
shape `textDocument/definition` already parses, so no new parsing —
only a kind discriminator so they don't collide on (server, uri).
- src/locations.rs: a (server, uri, kind)-keyed store whose value
type is the reused crate::definition::DefinitionResponse. The
proven definition store + Lua API are untouched.
- lsp.rs: ResponseRoute::Locations { uri, kind }; one absorb arm; a
DRY request_locations helper + request_references /
request_declaration / request_type_definition /
request_implementation. references sends
context.includeDeclaration. Supersede keys derive from each
kind's distinct method, so the four don't cancel each other.
- Lua: _request_*_raw bindings + install_locations exposing
pmacs.references / .declaration / .type_definition /
.implementation ({ locations, clear }, mirroring pmacs.definition,
reusing definition_response_to_lua). lsp.lua Handle wrappers + a
lsp.find-references command bound to M-? (modeline summary;
references-list buffer is future UX, like the hover panel).
- Tests: locations.rs unit tests (kind labels distinct; keys don't
collide); e2e driving all four through the async bridge and
asserting each routes to its own kind slot (fake returns distinct
lines 11/21/31/41).
Scoped: documentSymbol / workspaceSymbol / documentHighlight return
different shapes (new parsing) — a separate follow-up, not crammed
in here.
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1232/0; m4_acceptance 65/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
deb9935ee0
commit
96780b547d
|
|
@ -255,6 +255,10 @@ pmacs.lsp.request_hover = wrap_request(pmacs.lsp._request_hover_raw)
|
|||
pmacs.lsp.request_signature_help = wrap_request(pmacs.lsp._request_signature_help_raw)
|
||||
pmacs.lsp.request_definition = wrap_request(pmacs.lsp._request_definition_raw)
|
||||
pmacs.lsp.request_formatting = wrap_request(pmacs.lsp._request_formatting_raw)
|
||||
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_type_definition = wrap_request(pmacs.lsp._request_type_definition_raw)
|
||||
pmacs.lsp.request_implementation = wrap_request(pmacs.lsp._request_implementation_raw)
|
||||
|
||||
-- Render an `:await()` failure into a modeline-friendly reason.
|
||||
-- `Handle:await()` raises `{ tag = "cancelled", ... }` when the
|
||||
|
|
@ -381,6 +385,38 @@ function pmacs.lsp.go_to_definition()
|
|||
end)
|
||||
end
|
||||
|
||||
function pmacs.lsp.find_references()
|
||||
local rec = attached_for_active()
|
||||
if not rec then
|
||||
pmacs.editor.set_status("LSP: no server for active buffer")
|
||||
return
|
||||
end
|
||||
local line = pmacs.editor.cursor_line()
|
||||
local col = pmacs.editor.cursor_col()
|
||||
pmacs.references.clear(rec.server, rec.uri)
|
||||
pmacs.async(function()
|
||||
local ok, err = pcall(function()
|
||||
pmacs.lsp.request_references(rec.server, rec.uri, line, col):await()
|
||||
end)
|
||||
if not ok then
|
||||
pmacs.editor.set_status("LSP: " .. lsp_await_error(err))
|
||||
return
|
||||
end
|
||||
local locs = pmacs.references.locations(rec.server, rec.uri)
|
||||
if not locs or #locs == 0 then
|
||||
pmacs.editor.set_status("LSP: no references found")
|
||||
return
|
||||
end
|
||||
-- v1 surfaces a modeline summary (count + first hit); a
|
||||
-- references list buffer is future UX work, like the hover panel.
|
||||
local first = locs[1]
|
||||
pmacs.editor.set_status(string.format(
|
||||
"LSP: %d reference%s; first at %s:%d:%d",
|
||||
#locs, (#locs == 1 and "" or "s"),
|
||||
first.uri, first.line + 1, first.col + 1))
|
||||
end)
|
||||
end
|
||||
|
||||
function pmacs.lsp.format_buffer()
|
||||
local rec = attached_for_active()
|
||||
if not rec then
|
||||
|
|
@ -489,11 +525,18 @@ pmacs.command.define {
|
|||
fn = pmacs.lsp.signature_help_at_cursor,
|
||||
}
|
||||
|
||||
pmacs.command.define {
|
||||
name = "lsp.find-references",
|
||||
description = "Find references to the symbol under the cursor (LSP).",
|
||||
fn = pmacs.lsp.find_references,
|
||||
}
|
||||
|
||||
-- Default chords. M-. follows the cross-editor convention for
|
||||
-- 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
|
||||
-- init.lua.
|
||||
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 = "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" }
|
||||
|
|
|
|||
|
|
@ -329,6 +329,39 @@ fn main() {
|
|||
});
|
||||
write_frame(&mut stdout, &resp);
|
||||
}
|
||||
// T M4.5 Location-shaped nav. Distinct line per method so
|
||||
// a test can confirm each routes into its own kind slot.
|
||||
(
|
||||
m @ ("textDocument/references"
|
||||
| "textDocument/declaration"
|
||||
| "textDocument/typeDefinition"
|
||||
| "textDocument/implementation"),
|
||||
Some(idv),
|
||||
) => {
|
||||
let uri = params
|
||||
.get("textDocument")
|
||||
.and_then(|t| t.get("uri"))
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let line = match m {
|
||||
"textDocument/references" => 11,
|
||||
"textDocument/declaration" => 21,
|
||||
"textDocument/typeDefinition" => 31,
|
||||
_ => 41, // implementation
|
||||
};
|
||||
let resp = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": idv,
|
||||
"result": [{
|
||||
"uri": uri,
|
||||
"range": {
|
||||
"start": { "line": line, "character": 2 },
|
||||
"end": { "line": line, "character": 6 }
|
||||
}
|
||||
}]
|
||||
});
|
||||
write_frame(&mut stdout, &resp);
|
||||
}
|
||||
("textDocument/formatting", Some(idv)) => {
|
||||
// Synthetic two-edit reply: trim leading whitespace on
|
||||
// line 0 and append a semicolon at line 3, col 7.
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ pub mod instance_render;
|
|||
pub mod key;
|
||||
pub mod keymap_stack;
|
||||
pub mod keymap_tree;
|
||||
pub mod locations;
|
||||
pub mod lockfile;
|
||||
pub mod lsp;
|
||||
pub mod lsp_status;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
// locations.rs --- T M4.5: Location-shaped navigation requests.
|
||||
|
||||
//! Shared store for the navigation requests whose response shape is
|
||||
//! identical to `textDocument/definition`
|
||||
//! (`Location | Location[] | LocationLink[] | null`): `references`,
|
||||
//! `declaration`, `typeDefinition`, `implementation`. Parsing is
|
||||
//! reused verbatim from [`crate::definition::DefinitionResponse`];
|
||||
//! this module only adds a `kind` discriminator so the four request
|
||||
//! types don't collide on `(server, uri)` the way they would in the
|
||||
//! existing single-purpose definition store.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::definition::DefinitionResponse;
|
||||
|
||||
/// Which Location-shaped request a stored response answers.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub enum LocationKind {
|
||||
/// `textDocument/references`.
|
||||
References,
|
||||
/// `textDocument/declaration`.
|
||||
Declaration,
|
||||
/// `textDocument/typeDefinition`.
|
||||
TypeDefinition,
|
||||
/// `textDocument/implementation`.
|
||||
Implementation,
|
||||
}
|
||||
|
||||
impl LocationKind {
|
||||
/// Stable lowercase label — the `pmacs.lsp.*` Lua surface name,
|
||||
/// and the supersede-/store-key discriminator.
|
||||
#[must_use]
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
LocationKind::References => "references",
|
||||
LocationKind::Declaration => "declaration",
|
||||
LocationKind::TypeDefinition => "type_definition",
|
||||
LocationKind::Implementation => "implementation",
|
||||
}
|
||||
}
|
||||
|
||||
/// The LSP request method name.
|
||||
#[must_use]
|
||||
pub fn method(self) -> &'static str {
|
||||
match self {
|
||||
LocationKind::References => "textDocument/references",
|
||||
LocationKind::Declaration => "textDocument/declaration",
|
||||
LocationKind::TypeDefinition => "textDocument/typeDefinition",
|
||||
LocationKind::Implementation => "textDocument/implementation",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Key into [`LocationsStore`].
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub struct LocationsKey {
|
||||
/// Decimal LSP server id.
|
||||
pub server: String,
|
||||
/// Document URI.
|
||||
pub uri: String,
|
||||
/// Which request this entry answers.
|
||||
pub kind: LocationKind,
|
||||
}
|
||||
|
||||
impl LocationsKey {
|
||||
/// Construct a key.
|
||||
#[must_use]
|
||||
pub fn new(server: impl Into<String>, uri: impl Into<String>, kind: LocationKind) -> Self {
|
||||
Self {
|
||||
server: server.into(),
|
||||
uri: uri.into(),
|
||||
kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-`(server, uri, kind)` Location-list state. The value type is
|
||||
/// [`DefinitionResponse`] — same shape, same parser.
|
||||
#[derive(Default)]
|
||||
pub struct LocationsStore {
|
||||
by_key: HashMap<LocationsKey, DefinitionResponse>,
|
||||
}
|
||||
|
||||
impl LocationsStore {
|
||||
/// Empty store.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Replace the response at `key`.
|
||||
pub fn set(&mut self, key: LocationsKey, response: DefinitionResponse) {
|
||||
self.by_key.insert(key, response);
|
||||
}
|
||||
|
||||
/// Drop the entry at `key`.
|
||||
pub fn clear(&mut self, key: &LocationsKey) {
|
||||
self.by_key.remove(key);
|
||||
}
|
||||
|
||||
/// Look up the entry at `key`.
|
||||
#[must_use]
|
||||
pub fn get(&self, key: &LocationsKey) -> Option<&DefinitionResponse> {
|
||||
self.by_key.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Cheaply-cloneable shared handle.
|
||||
pub type SharedLocationsStore = Arc<Mutex<LocationsStore>>;
|
||||
|
||||
/// Build a fresh shared store.
|
||||
#[must_use]
|
||||
pub fn make_shared_store() -> SharedLocationsStore {
|
||||
Arc::new(Mutex::new(LocationsStore::new()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::definition::DefinitionLocation;
|
||||
|
||||
#[test]
|
||||
fn kind_labels_and_methods_are_distinct() {
|
||||
let kinds = [
|
||||
LocationKind::References,
|
||||
LocationKind::Declaration,
|
||||
LocationKind::TypeDefinition,
|
||||
LocationKind::Implementation,
|
||||
];
|
||||
let mut labels: Vec<&str> = kinds.iter().map(|k| k.label()).collect();
|
||||
labels.sort_unstable();
|
||||
labels.dedup();
|
||||
assert_eq!(labels.len(), 4, "labels must be unique");
|
||||
for k in kinds {
|
||||
assert!(k.method().starts_with("textDocument/"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_keys_do_not_collide() {
|
||||
let mut s = LocationsStore::new();
|
||||
let mk = |kind| LocationsKey::new("1", "file:///a", kind);
|
||||
let one = |line| DefinitionResponse {
|
||||
locations: vec![DefinitionLocation {
|
||||
uri: "file:///t".into(),
|
||||
line,
|
||||
col: 0,
|
||||
}],
|
||||
};
|
||||
s.set(mk(LocationKind::References), one(1));
|
||||
s.set(mk(LocationKind::Implementation), one(2));
|
||||
// Same (server, uri) but different kind ⇒ independent entries.
|
||||
assert_eq!(
|
||||
s.get(&mk(LocationKind::References)).unwrap().locations[0].line,
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
s.get(&mk(LocationKind::Implementation)).unwrap().locations[0].line,
|
||||
2
|
||||
);
|
||||
s.clear(&mk(LocationKind::References));
|
||||
assert!(s.get(&mk(LocationKind::References)).is_none());
|
||||
assert!(s.get(&mk(LocationKind::Implementation)).is_some());
|
||||
}
|
||||
}
|
||||
142
src/lsp.rs
142
src/lsp.rs
|
|
@ -745,6 +745,10 @@ pub struct LspManager {
|
|||
/// T M4.12 definition store. Populated when a
|
||||
/// `textDocument/definition` response lands.
|
||||
definition_store: crate::definition::SharedDefinitionStore,
|
||||
/// T M4.5: references / declaration / typeDefinition /
|
||||
/// implementation. Same Location shape as `definition`, keyed
|
||||
/// additionally by kind so the four don't collide.
|
||||
locations_store: crate::locations::SharedLocationsStore,
|
||||
/// T M4.12 formatting store. Populated when a
|
||||
/// `textDocument/formatting` response lands.
|
||||
formatting_store: crate::formatting::SharedFormattingStore,
|
||||
|
|
@ -805,6 +809,15 @@ enum ResponseRoute {
|
|||
/// Absorb response into [`crate::formatting::FormattingStore`] at
|
||||
/// `(server, uri)`.
|
||||
Formatting { uri: String },
|
||||
/// Absorb a Location-shaped nav response (references / declaration
|
||||
/// / typeDefinition / implementation) into
|
||||
/// [`crate::locations::LocationsStore`] at `(server, uri, kind)`.
|
||||
Locations {
|
||||
/// Document URI.
|
||||
uri: String,
|
||||
/// Which nav request this answers.
|
||||
kind: crate::locations::LocationKind,
|
||||
},
|
||||
}
|
||||
|
||||
impl ResponseRoute {
|
||||
|
|
@ -816,7 +829,8 @@ impl ResponseRoute {
|
|||
| ResponseRoute::Hover { uri }
|
||||
| ResponseRoute::Signature { uri }
|
||||
| ResponseRoute::Definition { uri }
|
||||
| ResponseRoute::Formatting { uri } => uri,
|
||||
| ResponseRoute::Formatting { uri }
|
||||
| ResponseRoute::Locations { uri, .. } => uri,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -886,6 +900,7 @@ impl LspManager {
|
|||
hover_store: crate::hover::make_shared_store(),
|
||||
signature_store: crate::signature::make_shared_store(),
|
||||
definition_store: crate::definition::make_shared_store(),
|
||||
locations_store: crate::locations::make_shared_store(),
|
||||
formatting_store: crate::formatting::make_shared_store(),
|
||||
pending_routes: HashMap::new(),
|
||||
status_tracker: crate::lsp_status::LspStatusTracker::new(),
|
||||
|
|
@ -926,6 +941,13 @@ impl LspManager {
|
|||
self.definition_store.clone()
|
||||
}
|
||||
|
||||
/// Shared locations store — references / declaration /
|
||||
/// typeDefinition / implementation (T M4.5).
|
||||
#[must_use]
|
||||
pub fn locations_store(&self) -> crate::locations::SharedLocationsStore {
|
||||
self.locations_store.clone()
|
||||
}
|
||||
|
||||
/// Shared formatting store (T M4.12).
|
||||
#[must_use]
|
||||
pub fn formatting_store(&self) -> crate::formatting::SharedFormattingStore {
|
||||
|
|
@ -1461,6 +1483,113 @@ impl LspManager {
|
|||
Ok(job_id)
|
||||
}
|
||||
|
||||
/// Shared body for the Location-shaped nav requests. `extra` is
|
||||
/// merged into the params object (only `references` uses it, for
|
||||
/// `context.includeDeclaration`). The supersede key derives from
|
||||
/// the kind's distinct method, so the four requests don't cancel
|
||||
/// one another. Returns the async-runtime [`JobId`].
|
||||
fn request_locations(
|
||||
&mut self,
|
||||
sid: LspServerId,
|
||||
uri: impl Into<String>,
|
||||
line: u32,
|
||||
col: u32,
|
||||
kind: crate::locations::LocationKind,
|
||||
extra: Option<Value>,
|
||||
) -> Result<JobId, String> {
|
||||
let uri = uri.into();
|
||||
let mut params = json!({
|
||||
"textDocument": { "uri": uri.clone() },
|
||||
"position": self.outbound_position(sid, &uri, line, col)
|
||||
});
|
||||
if let Some(Value::Object(ex)) = extra
|
||||
&& let Some(obj) = params.as_object_mut()
|
||||
{
|
||||
for (k, v) in ex {
|
||||
obj.insert(k, v);
|
||||
}
|
||||
}
|
||||
let method = kind.method();
|
||||
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::Locations { uri, kind });
|
||||
Ok(job_id)
|
||||
}
|
||||
|
||||
/// Send `textDocument/references` (with `includeDeclaration`).
|
||||
/// Returns the async-runtime [`JobId`].
|
||||
pub fn request_references(
|
||||
&mut self,
|
||||
sid: LspServerId,
|
||||
uri: impl Into<String>,
|
||||
line: u32,
|
||||
col: u32,
|
||||
) -> Result<JobId, String> {
|
||||
self.request_locations(
|
||||
sid,
|
||||
uri,
|
||||
line,
|
||||
col,
|
||||
crate::locations::LocationKind::References,
|
||||
Some(json!({ "context": { "includeDeclaration": true } })),
|
||||
)
|
||||
}
|
||||
|
||||
/// Send `textDocument/declaration`. Returns the [`JobId`].
|
||||
pub fn request_declaration(
|
||||
&mut self,
|
||||
sid: LspServerId,
|
||||
uri: impl Into<String>,
|
||||
line: u32,
|
||||
col: u32,
|
||||
) -> Result<JobId, String> {
|
||||
self.request_locations(
|
||||
sid,
|
||||
uri,
|
||||
line,
|
||||
col,
|
||||
crate::locations::LocationKind::Declaration,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Send `textDocument/typeDefinition`. Returns the [`JobId`].
|
||||
pub fn request_type_definition(
|
||||
&mut self,
|
||||
sid: LspServerId,
|
||||
uri: impl Into<String>,
|
||||
line: u32,
|
||||
col: u32,
|
||||
) -> Result<JobId, String> {
|
||||
self.request_locations(
|
||||
sid,
|
||||
uri,
|
||||
line,
|
||||
col,
|
||||
crate::locations::LocationKind::TypeDefinition,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Send `textDocument/implementation`. Returns the [`JobId`].
|
||||
pub fn request_implementation(
|
||||
&mut self,
|
||||
sid: LspServerId,
|
||||
uri: impl Into<String>,
|
||||
line: u32,
|
||||
col: u32,
|
||||
) -> Result<JobId, String> {
|
||||
self.request_locations(
|
||||
sid,
|
||||
uri,
|
||||
line,
|
||||
col,
|
||||
crate::locations::LocationKind::Implementation,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Send `textDocument/formatting` for `uri` with `tab_size` /
|
||||
/// `insert_spaces` formatting options. The response is absorbed
|
||||
/// into the formatting store at `(sid, uri)`. Returns the
|
||||
|
|
@ -2013,6 +2142,17 @@ impl LspManager {
|
|||
.expect("definition store mutex poisoned");
|
||||
guard.set(key, resp);
|
||||
}
|
||||
ResponseRoute::Locations { uri, kind } => {
|
||||
// Same Location parser as definition; only the store
|
||||
// key carries the kind discriminator.
|
||||
let resp = crate::definition::DefinitionResponse::from_lsp_value(result);
|
||||
let key = crate::locations::LocationsKey::new(server_key, uri.clone(), *kind);
|
||||
let mut guard = self
|
||||
.locations_store
|
||||
.lock()
|
||||
.expect("locations store mutex poisoned");
|
||||
guard.set(key, resp);
|
||||
}
|
||||
ResponseRoute::Formatting { uri } => {
|
||||
let resp = crate::formatting::FormattingResponse::from_lsp_value(result);
|
||||
let key = crate::formatting::FormattingKey::new(server_key, uri.clone());
|
||||
|
|
|
|||
|
|
@ -7200,6 +7200,70 @@ pub fn install_lsp(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> {
|
|||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
let m = manager.clone();
|
||||
lsp_mod.set(
|
||||
"_request_references_raw",
|
||||
lua.create_function(
|
||||
move |_, (id, uri, line, col): (LspServerIdLua, String, u32, u32)| {
|
||||
let job_id = m
|
||||
.borrow_mut()
|
||||
.request_references(id.0, uri, line, col)
|
||||
.map_err(mlua::Error::external)?;
|
||||
Ok(job_id)
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
let m = manager.clone();
|
||||
lsp_mod.set(
|
||||
"_request_declaration_raw",
|
||||
lua.create_function(
|
||||
move |_, (id, uri, line, col): (LspServerIdLua, String, u32, u32)| {
|
||||
let job_id = m
|
||||
.borrow_mut()
|
||||
.request_declaration(id.0, uri, line, col)
|
||||
.map_err(mlua::Error::external)?;
|
||||
Ok(job_id)
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
let m = manager.clone();
|
||||
lsp_mod.set(
|
||||
"_request_type_definition_raw",
|
||||
lua.create_function(
|
||||
move |_, (id, uri, line, col): (LspServerIdLua, String, u32, u32)| {
|
||||
let job_id = m
|
||||
.borrow_mut()
|
||||
.request_type_definition(id.0, uri, line, col)
|
||||
.map_err(mlua::Error::external)?;
|
||||
Ok(job_id)
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
let m = manager.clone();
|
||||
lsp_mod.set(
|
||||
"_request_implementation_raw",
|
||||
lua.create_function(
|
||||
move |_, (id, uri, line, col): (LspServerIdLua, String, u32, u32)| {
|
||||
let job_id = m
|
||||
.borrow_mut()
|
||||
.request_implementation(id.0, uri, line, col)
|
||||
.map_err(mlua::Error::external)?;
|
||||
Ok(job_id)
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
let m = manager.clone();
|
||||
lsp_mod.set(
|
||||
|
|
@ -7453,6 +7517,7 @@ pub fn make_lsp_manager(
|
|||
install_hover(lua, &manager)?;
|
||||
install_signature(lua, &manager)?;
|
||||
install_definition(lua, &manager)?;
|
||||
install_locations(lua, &manager)?;
|
||||
install_formatting(lua, &manager)?;
|
||||
Ok(manager)
|
||||
}
|
||||
|
|
@ -8252,6 +8317,7 @@ use crate::completion::{CompletionItem, CompletionItemKind, CompletionKey, Compl
|
|||
use crate::definition::{DefinitionKey, DefinitionLocation, DefinitionResponse};
|
||||
use crate::formatting::{FormattingKey, FormattingResponse, TextEdit};
|
||||
use crate::hover::{Hover, HoverKey};
|
||||
use crate::locations::{LocationKind, LocationsKey};
|
||||
use crate::signature::{Signature, SignatureHelp, SignatureKey, SignatureParameter};
|
||||
|
||||
fn completion_item_to_lua(lua: &Lua, item: &CompletionItem) -> mlua::Result<Table> {
|
||||
|
|
@ -8672,6 +8738,52 @@ pub fn install_definition(lua: &Lua, manager: &SharedLspManager) -> mlua::Result
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Install `pmacs.references` / `.declaration` / `.type_definition`
|
||||
/// / `.implementation`, each `{ locations(sid,uri), clear(sid,uri) }`,
|
||||
/// mirroring `pmacs.definition` (same Location-list shape, hence the
|
||||
/// reused `definition_response_to_lua`). T M4.5.
|
||||
pub fn install_locations(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> {
|
||||
let pmacs: Table = lua.globals().get("pmacs")?;
|
||||
for kind in [
|
||||
LocationKind::References,
|
||||
LocationKind::Declaration,
|
||||
LocationKind::TypeDefinition,
|
||||
LocationKind::Implementation,
|
||||
] {
|
||||
let m = lua.create_table()?;
|
||||
{
|
||||
let mgr = manager.clone();
|
||||
m.set(
|
||||
"locations",
|
||||
lua.create_function(move |lua, (id, uri): (LspServerIdLua, String)| {
|
||||
let store_handle = mgr.borrow().locations_store();
|
||||
let guard = store_handle.lock().expect("locations store mutex poisoned");
|
||||
let key = LocationsKey::new(id.0.raw().to_string(), uri, kind);
|
||||
if let Some(r) = guard.get(&key) {
|
||||
Ok(Value::Table(definition_response_to_lua(lua, r)?))
|
||||
} else {
|
||||
Ok(Value::Table(lua.create_table_with_capacity(0, 0)?))
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
{
|
||||
let mgr = manager.clone();
|
||||
m.set(
|
||||
"clear",
|
||||
lua.create_function(move |_, (id, uri): (LspServerIdLua, String)| {
|
||||
let store_handle = mgr.borrow().locations_store();
|
||||
let mut guard = store_handle.lock().expect("locations store mutex poisoned");
|
||||
guard.clear(&LocationsKey::new(id.0.raw().to_string(), uri, kind));
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
pmacs.set(kind.label(), m)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn text_edit_to_lua(lua: &Lua, edit: &TextEdit) -> mlua::Result<Table> {
|
||||
let t = lua.create_table_with_capacity(0, 5)?;
|
||||
t.set("start_line", edit.start_line)?;
|
||||
|
|
|
|||
|
|
@ -3963,3 +3963,55 @@ fn m4_5_workspace_configuration_answered_from_settings() {
|
|||
from the spec settings; got {got:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// T M4.5 nav batch: references / declaration / typeDefinition /
|
||||
/// implementation each await end-to-end through the async bridge and
|
||||
/// land in their *own* kind-keyed slot (no collision on
|
||||
/// `(server, uri)` — the reason for the dedicated locations store).
|
||||
/// The fake returns a distinct line per method (11/21/31/41), so the
|
||||
/// per-kind Lua surfaces must read back exactly those.
|
||||
#[test]
|
||||
fn m4_5_location_nav_requests_route_by_kind() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
spawn_lsp_and_init(&mut state, None);
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
.load(
|
||||
"local uri='file:///n.rs'
|
||||
pmacs.lsp.did_open(_G._lsp, uri, 1, 'fn x() {}\\n')
|
||||
_G._done=false
|
||||
pmacs.async(function()
|
||||
pmacs.lsp.request_references(_G._lsp, uri, 0, 3):await()
|
||||
pmacs.lsp.request_declaration(_G._lsp, uri, 0, 3):await()
|
||||
pmacs.lsp.request_type_definition(_G._lsp, uri, 0, 3):await()
|
||||
pmacs.lsp.request_implementation(_G._lsp, uri, 0, 3):await()
|
||||
local function l(t)
|
||||
local x = t.locations(_G._lsp, uri)
|
||||
return (x and x[1] and x[1].line) or -1
|
||||
end
|
||||
_G._refs = l(pmacs.references)
|
||||
_G._decl = l(pmacs.declaration)
|
||||
_G._tdef = l(pmacs.type_definition)
|
||||
_G._impl = l(pmacs.implementation)
|
||||
_G._done = true
|
||||
end)",
|
||||
)
|
||||
.exec()
|
||||
.expect("dispatch nav coroutine");
|
||||
assert!(
|
||||
pump_lua_flag(&mut state, "_G._done", 5),
|
||||
"nav coroutine never completed"
|
||||
);
|
||||
let (refs, decl, tdef, impl_): (i64, i64, i64, i64) = state
|
||||
.lua_host
|
||||
.lua()
|
||||
.load("return _G._refs, _G._decl, _G._tdef, _G._impl")
|
||||
.eval()
|
||||
.expect("read kind lines");
|
||||
assert_eq!(refs, 11, "references must route to its own slot");
|
||||
assert_eq!(decl, 21, "declaration 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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue