Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions src/neovim/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ pub trait NeovimClientTrait: Sync {
client_name: &str,
query: &str,
) -> Result<WorkspaceSymbolResult, NeovimError>;

/// Get references for a symbol at a specific position
async fn lsp_references(
&self,
client_name: &str,
buffer_id: u64,
position: Position,
include_declaration: bool,
) -> Result<Vec<Location>, NeovimError>;
}

pub struct NeovimHandler<T> {
Expand Down Expand Up @@ -448,6 +457,21 @@ pub struct HoverParams {
pub position: Position,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReferenceParams {
pub text_document: TextDocumentIdentifier,
pub position: Position,
pub context: ReferenceContext,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReferenceContext {
/// Include the declaration of the current symbol.
pub include_declaration: bool,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct HoverResult {
/// The hover's content
Expand Down Expand Up @@ -1285,6 +1309,73 @@ where
}
}
}

#[instrument(skip(self))]
async fn lsp_references(
&self,
client_name: &str,
buffer_id: u64,
position: Position,
include_declaration: bool,
) -> Result<Vec<Location>, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;

match conn
.nvim
.execute_lua(
include_str!("lua/lsp_references.lua"),
vec![
Value::from(client_name), // client_name
Value::from(
serde_json::to_string(&ReferenceParams {
text_document: self
.lsp_make_text_document_params(buffer_id)
.await
.map_err(|e| {
NeovimError::Api(format!(
"Failed to make text document params: {e}"
))
})?,
position,
context: ReferenceContext {
include_declaration,
},
})
.unwrap(),
), // params
Value::from(1000), // timeout_ms
Value::from(buffer_id), // bufnr
],
)
.await
{
Ok(result) => {
debug!("LSP References retrieved successfully");
#[derive(Debug, serde::Deserialize)]
struct Result {
result: Option<Vec<Location>>,
}
let result: Result = match serde_json::from_str(result.as_str().unwrap()) {
Ok(d) => d,
Err(e) => {
debug!("Failed to parse references result: {}", e);
return Err(NeovimError::Api(format!(
"Failed to parse references result: {e}"
)));
}
};
Ok(result.result.unwrap_or_default())
}
Err(e) => {
debug!("Failed to get LSP references: {}", e);
Err(NeovimError::Api(format!(
"Failed to get LSP references: {e}"
)))
}
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -1372,4 +1463,28 @@ mod tests {
let json = serde_json::to_string(&params).unwrap();
assert!(json.contains("function"));
}

#[test]
fn test_reference_params_serialization() {
let params = ReferenceParams {
text_document: TextDocumentIdentifier {
uri: "file:///test.rs".to_string(),
version: Some(1),
},
position: Position {
line: 10,
character: 5,
},
context: ReferenceContext {
include_declaration: true,
},
};

let json = serde_json::to_string(&params).unwrap();
assert!(json.contains("textDocument"));
assert!(json.contains("position"));
assert!(json.contains("context"));
assert!(json.contains("includeDeclaration"));
assert!(json.contains("true"));
}
}
27 changes: 27 additions & 0 deletions src/neovim/lua/lsp_references.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
local clients = vim.lsp.get_clients()
local client_name, params_raw, timeout_ms, bufnr = unpack({ ... })
local client
for _, v in ipairs(clients) do
if v.name == client_name then
client = v
end
end
if client == nil then
return vim.json.encode({
err_msg = string.format("LSP client %s not found", vim.json.encode(client_name)),
})
end

local params = vim.json.decode(params_raw)
local result, err = client:request_sync("textDocument/references", params, timeout_ms, bufnr)
if err then
return vim.json.encode({
err_msg = string.format(
"LSP client %s request_sync error: %s",
vim.json.encode(client_name),
vim.json.encode(err)
),
})
end

return vim.json.encode(result)
1 change: 1 addition & 0 deletions src/server/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ async fn test_list_tools() -> Result<(), Box<dyn std::error::Error>> {
assert!(tool_names.contains(&"disconnect"));
assert!(tool_names.contains(&"list_buffers"));
assert!(tool_names.contains(&"lsp_clients"));
assert!(tool_names.contains(&"lsp_references"));

// Verify tool descriptions are present
for tool in &tools.tools {
Expand Down
38 changes: 38 additions & 0 deletions src/server/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ pub struct WorkspaceSymbolsParams {
pub query: String,
}

/// References parameters with connection context
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ReferencesParams {
/// Unique identifier for the target Neovim instance
pub connection_id: String,
/// Neovim Buffer ID
pub id: u64,
/// Lsp client name
pub lsp_client_name: String,
/// Symbol position in the buffer, line number starts from 0
pub line: u64,
/// Symbol position in the buffer, character number starts from 0
pub character: u64,
/// Include the declaration of the current symbol in the results
pub include_declaration: bool,
}

#[tool_router]
impl NeovimMcpServer {
#[tool(description = "Get available Neovim targets")]
Expand Down Expand Up @@ -325,6 +342,27 @@ impl NeovimMcpServer {
.await?;
Ok(CallToolResult::success(vec![Content::json(symbols)?]))
}

#[tool(description = "Get references for a symbol at a specific position")]
#[instrument(skip(self))]
pub async fn lsp_references(
&self,
Parameters(ReferencesParams {
connection_id,
id,
lsp_client_name,
line,
character,
include_declaration,
}): Parameters<ReferencesParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let position = Position { line, character };
let references = client
.lsp_references(&lsp_client_name, id, position, include_declaration)
.await?;
Ok(CallToolResult::success(vec![Content::json(references)?]))
}
}

/// Build tool router for NeovimMcpServer
Expand Down