feat: auto-LSP integration with didOpen/didChange and diagnostic enrichment
Wire LSP into the Read/Edit/Write tool flow so the agent automatically gets diagnostics after file operations: - lsp_transport: Add LspServerMessage enum, read_message() for handling both responses and server-initiated notifications, notification queue with drain_notifications(), send_request now handles interleaved publishDiagnostics without breaking - lsp_process: Add did_open(), did_change(), drain_diagnostics(), open file tracking (HashSet) and version counters for didChange, language_id_for_path() and severity_name() helpers - lsp_client: Add notify_file_open(), notify_file_change(), fetch_diagnostics_for_file() with best-effort graceful fallback, registry-level open file tracking, diagnostic caching - tools: Enrich run_read_file with didOpen + diagnostics, run_write_file and run_edit_file with didChange + diagnostics, format_diagnostic_appendix() for readable diagnostic output appended to tool results All enrichment is non-blocking: if no LSP server is available, tools work exactly as before. No errors propagate from the LSP layer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
86cfae8c10
commit
5fee7fdf39
|
|
@ -1,7 +1,7 @@
|
||||||
#![allow(clippy::should_implement_trait, clippy::must_use_candidate)]
|
#![allow(clippy::should_implement_trait, clippy::must_use_candidate)]
|
||||||
//! LSP (Language Server Protocol) client registry for tool dispatch.
|
//! LSP (Language Server Protocol) client registry for tool dispatch.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
|
@ -156,6 +156,7 @@ pub struct LspRegistry {
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct RegistryInner {
|
struct RegistryInner {
|
||||||
servers: HashMap<String, LspServerEntry>,
|
servers: HashMap<String, LspServerEntry>,
|
||||||
|
open_files: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LspRegistry {
|
impl LspRegistry {
|
||||||
|
|
@ -420,6 +421,139 @@ impl LspRegistry {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notify the LSP server that a file was opened and collect any diagnostics.
|
||||||
|
/// Best-effort: returns empty vec if no server is available.
|
||||||
|
pub fn notify_file_open(&self, path: &str, content: &str) -> Vec<LspDiagnostic> {
|
||||||
|
let Some(language) = Self::language_for_path(path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if already open
|
||||||
|
{
|
||||||
|
let inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
if inner.open_files.contains(path) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy-start the server
|
||||||
|
if self.start_server(&language).is_err() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the process handle and send didOpen
|
||||||
|
let process_arc = {
|
||||||
|
let inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
match inner.servers.get(&language).and_then(|e| e.process.clone()) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return Vec::new(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
if let Ok(mut process) = process_arc.lock() {
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build();
|
||||||
|
if let Ok(rt) = rt {
|
||||||
|
let _ = rt.block_on(process.did_open(path, content));
|
||||||
|
diagnostics = process.drain_diagnostics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache diagnostics in registry state
|
||||||
|
if !diagnostics.is_empty() {
|
||||||
|
let diag_path = path.to_owned();
|
||||||
|
let diags = diagnostics.clone();
|
||||||
|
let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
if let Some(entry) = inner.servers.get_mut(&language) {
|
||||||
|
// Replace diagnostics for this file (publishDiagnostics is full replacement)
|
||||||
|
entry.state.diagnostics.retain(|d| d.path != diag_path);
|
||||||
|
entry.state.diagnostics.extend(diags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark file as open
|
||||||
|
{
|
||||||
|
let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
inner.open_files.insert(path.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notify the LSP server that a file changed and collect any diagnostics.
|
||||||
|
/// Best-effort: returns empty vec if no server is available.
|
||||||
|
pub fn notify_file_change(&self, path: &str, content: &str) -> Vec<LspDiagnostic> {
|
||||||
|
let Some(language) = Self::language_for_path(path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the process handle
|
||||||
|
let process_arc = {
|
||||||
|
let inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
match inner.servers.get(&language).and_then(|e| e.process.clone()) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return Vec::new(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
if let Ok(mut process) = process_arc.lock() {
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build();
|
||||||
|
if let Ok(rt) = rt {
|
||||||
|
let _ = rt.block_on(process.did_change(path, content));
|
||||||
|
diagnostics = process.drain_diagnostics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace cached diagnostics for this file
|
||||||
|
if !diagnostics.is_empty() {
|
||||||
|
let diag_path = path.to_owned();
|
||||||
|
let diags = diagnostics.clone();
|
||||||
|
let mut inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
if let Some(entry) = inner.servers.get_mut(&language) {
|
||||||
|
entry.state.diagnostics.retain(|d| d.path != diag_path);
|
||||||
|
entry.state.diagnostics.extend(diags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch diagnostics for a file by draining pending server notifications
|
||||||
|
/// and returning cached diagnostics.
|
||||||
|
pub fn fetch_diagnostics_for_file(&self, path: &str) -> Vec<LspDiagnostic> {
|
||||||
|
let Some(language) = Self::language_for_path(path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drain pending notifications from the transport
|
||||||
|
let process_arc = {
|
||||||
|
let inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
inner.servers.get(&language).and_then(|e| e.process.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(process_arc) = process_arc {
|
||||||
|
if let Ok(mut process) = process_arc.lock() {
|
||||||
|
let new_diags = process.drain_diagnostics();
|
||||||
|
if !new_diags.is_empty() {
|
||||||
|
let diag_path = path.to_owned();
|
||||||
|
let mut inner =
|
||||||
|
self.inner.lock().expect("lsp registry lock poisoned");
|
||||||
|
if let Some(entry) = inner.servers.get_mut(&language) {
|
||||||
|
entry.state.diagnostics.retain(|d| d.path != diag_path);
|
||||||
|
entry.state.diagnostics.extend(new_diags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.get_diagnostics(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// Dispatch an LSP action and return a structured result.
|
/// Dispatch an LSP action and return a structured result.
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn dispatch(
|
pub fn dispatch(
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ pub fn command_exists_on_path(command: &str) -> bool {
|
||||||
Command::new(command)
|
Command::new(command)
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.output()
|
.output()
|
||||||
.map_or(false, |_| true)
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Discover LSP servers that are actually installed on the current system.
|
/// Discover LSP servers that are actually installed on the current system.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
//! LSP process manager: spawns language servers and drives the LSP lifecycle.
|
//! LSP process manager: spawns language servers and drives the LSP lifecycle.
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use crate::lsp_client::{
|
use crate::lsp_client::{
|
||||||
LspCompletionItem, LspHoverResult, LspLocation, LspServerStatus, LspSymbol,
|
LspCompletionItem, LspDiagnostic, LspHoverResult, LspLocation, LspServerStatus, LspSymbol,
|
||||||
};
|
};
|
||||||
use crate::lsp_transport::{LspTransport, LspTransportError};
|
use crate::lsp_transport::{LspTransport, LspTransportError};
|
||||||
|
|
||||||
|
|
@ -16,6 +17,8 @@ pub struct LspProcess {
|
||||||
root_uri: String,
|
root_uri: String,
|
||||||
capabilities: Option<JsonValue>,
|
capabilities: Option<JsonValue>,
|
||||||
status: LspServerStatus,
|
status: LspServerStatus,
|
||||||
|
open_files: HashSet<String>,
|
||||||
|
version_counter: HashMap<String, u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
|
@ -38,6 +41,8 @@ impl LspProcess {
|
||||||
root_uri: root_uri.clone(),
|
root_uri: root_uri.clone(),
|
||||||
capabilities: None,
|
capabilities: None,
|
||||||
status: LspServerStatus::Starting,
|
status: LspServerStatus::Starting,
|
||||||
|
open_files: HashSet::new(),
|
||||||
|
version_counter: HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
process.initialize(&canonical).await?;
|
process.initialize(&canonical).await?;
|
||||||
|
|
@ -279,6 +284,103 @@ impl LspProcess {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notify the server that a file was opened. Sends `textDocument/didOpen`.
|
||||||
|
/// No-op if the file is already tracked as open.
|
||||||
|
pub async fn did_open(&mut self, path: &str, content: &str) -> Result<(), LspProcessError> {
|
||||||
|
if self.open_files.contains(path) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let uri = path_to_uri(path);
|
||||||
|
let language_id = language_id_for_path(path);
|
||||||
|
let params = serde_json::json!({
|
||||||
|
"textDocument": {
|
||||||
|
"uri": uri,
|
||||||
|
"languageId": language_id,
|
||||||
|
"version": 0,
|
||||||
|
"text": content
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.transport
|
||||||
|
.send_notification("textDocument/didOpen", Some(params))
|
||||||
|
.await
|
||||||
|
.map_err(LspProcessError::Transport)?;
|
||||||
|
|
||||||
|
self.open_files.insert(path.to_owned());
|
||||||
|
self.version_counter.insert(path.to_owned(), 0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notify the server that a file's content changed. Sends `textDocument/didChange`.
|
||||||
|
pub async fn did_change(&mut self, path: &str, content: &str) -> Result<(), LspProcessError> {
|
||||||
|
let version = self.version_counter.get(path).map_or(1, |v| v + 1);
|
||||||
|
|
||||||
|
let uri = path_to_uri(path);
|
||||||
|
let params = serde_json::json!({
|
||||||
|
"textDocument": { "uri": uri, "version": version },
|
||||||
|
"contentChanges": [{ "text": content }]
|
||||||
|
});
|
||||||
|
|
||||||
|
self.transport
|
||||||
|
.send_notification("textDocument/didChange", Some(params))
|
||||||
|
.await
|
||||||
|
.map_err(LspProcessError::Transport)?;
|
||||||
|
|
||||||
|
self.version_counter.insert(path.to_owned(), version);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain queued server notifications and extract `publishDiagnostics`.
|
||||||
|
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||||
|
pub fn drain_diagnostics(&mut self) -> Vec<LspDiagnostic> {
|
||||||
|
let notifications = self.transport.drain_notifications();
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
for n in ¬ifications {
|
||||||
|
if n.method == "textDocument/publishDiagnostics" {
|
||||||
|
if let Some(params) = &n.params {
|
||||||
|
if let Some(uri) = params.get("uri").and_then(|v| v.as_str()) {
|
||||||
|
let path = uri_to_path(uri);
|
||||||
|
if let Some(diags) = params.get("diagnostics").and_then(|v| v.as_array())
|
||||||
|
{
|
||||||
|
for d in diags {
|
||||||
|
diagnostics.push(LspDiagnostic {
|
||||||
|
path: path.clone(),
|
||||||
|
line: d
|
||||||
|
.get("range")
|
||||||
|
.and_then(|r| r.get("start"))
|
||||||
|
.and_then(|s| s.get("line"))
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.map_or(0, |v| v as u32),
|
||||||
|
character: d
|
||||||
|
.get("range")
|
||||||
|
.and_then(|r| r.get("start"))
|
||||||
|
.and_then(|s| s.get("character"))
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.map_or(0, |v| v as u32),
|
||||||
|
severity: d
|
||||||
|
.get("severity")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.map_or_else(|| "error".to_owned(), severity_name),
|
||||||
|
message: d
|
||||||
|
.get("message")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_owned(),
|
||||||
|
source: d
|
||||||
|
.get("source")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(str::to_owned),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn status(&self) -> LspServerStatus {
|
pub fn status(&self) -> LspServerStatus {
|
||||||
self.status
|
self.status
|
||||||
|
|
@ -360,6 +462,39 @@ fn uri_to_path(uri: &str) -> String {
|
||||||
uri.strip_prefix("file://").unwrap_or(uri).to_owned()
|
uri.strip_prefix("file://").unwrap_or(uri).to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn language_id_for_path(path: &str) -> String {
|
||||||
|
let ext = std::path::Path::new(path)
|
||||||
|
.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
match ext {
|
||||||
|
"rs" => "rust",
|
||||||
|
"ts" => "typescript",
|
||||||
|
"tsx" => "typescriptreact",
|
||||||
|
"js" => "javascript",
|
||||||
|
"jsx" => "javascriptreact",
|
||||||
|
"py" => "python",
|
||||||
|
"go" => "go",
|
||||||
|
"java" => "java",
|
||||||
|
"c" | "h" => "c",
|
||||||
|
"cpp" | "hpp" | "cc" => "cpp",
|
||||||
|
"rb" => "ruby",
|
||||||
|
"lua" => "lua",
|
||||||
|
_ => ext,
|
||||||
|
}
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn severity_name(code: u64) -> String {
|
||||||
|
match code {
|
||||||
|
1 => "error".to_owned(),
|
||||||
|
2 => "warning".to_owned(),
|
||||||
|
3 => "info".to_owned(),
|
||||||
|
4 => "hint".to_owned(),
|
||||||
|
_ => format!("unknown({code})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_hover(value: &JsonValue) -> Option<LspHoverResult> {
|
fn parse_hover(value: &JsonValue) -> Option<LspHoverResult> {
|
||||||
let contents = value.get("contents")?;
|
let contents = value.get("contents")?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,14 @@ impl LspResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A message received from an LSP server — either a response to a request
|
||||||
|
/// or a server-initiated notification (e.g. `textDocument/publishDiagnostics`).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum LspServerMessage {
|
||||||
|
Response(LspResponse),
|
||||||
|
Notification(LspNotification),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LspTransportError {
|
pub enum LspTransportError {
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
|
|
@ -138,6 +146,7 @@ pub struct LspTransport {
|
||||||
stdout: BufReader<ChildStdout>,
|
stdout: BufReader<ChildStdout>,
|
||||||
next_id: u64,
|
next_id: u64,
|
||||||
request_timeout: Duration,
|
request_timeout: Duration,
|
||||||
|
pending_notifications: Vec<LspNotification>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LspTransport {
|
impl LspTransport {
|
||||||
|
|
@ -172,6 +181,7 @@ impl LspTransport {
|
||||||
stdout: BufReader::new(stdout),
|
stdout: BufReader::new(stdout),
|
||||||
next_id: 1,
|
next_id: 1,
|
||||||
request_timeout,
|
request_timeout,
|
||||||
|
pending_notifications: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,6 +203,7 @@ impl LspTransport {
|
||||||
stdout: BufReader::new(stdout),
|
stdout: BufReader::new(stdout),
|
||||||
next_id: 1,
|
next_id: 1,
|
||||||
request_timeout,
|
request_timeout,
|
||||||
|
pending_notifications: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,12 +246,27 @@ impl LspTransport {
|
||||||
|
|
||||||
let method_owned = method.to_string();
|
let method_owned = method.to_string();
|
||||||
let timeout_duration = self.request_timeout;
|
let timeout_duration = self.request_timeout;
|
||||||
let response = timeout(timeout_duration, self.read_response())
|
let response = match timeout(timeout_duration, async {
|
||||||
.await
|
loop {
|
||||||
.map_err(|_| LspTransportError::Timeout {
|
match self.read_message().await {
|
||||||
method: method_owned,
|
Ok(LspServerMessage::Response(r)) => break Ok(r),
|
||||||
timeout: timeout_duration,
|
Ok(LspServerMessage::Notification(n)) => {
|
||||||
})??;
|
self.pending_notifications.push(n);
|
||||||
|
}
|
||||||
|
Err(e) => break Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(inner) => inner,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(LspTransportError::Timeout {
|
||||||
|
method: method_owned,
|
||||||
|
timeout: timeout_duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
if response.jsonrpc != "2.0" {
|
if response.jsonrpc != "2.0" {
|
||||||
return Err(LspTransportError::InvalidResponse {
|
return Err(LspTransportError::InvalidResponse {
|
||||||
|
|
@ -266,8 +292,57 @@ impl LspTransport {
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a single message from the server, returning either a response or
|
||||||
|
/// a server-initiated notification (e.g. `publishDiagnostics`).
|
||||||
|
pub async fn read_message(&mut self) -> Result<LspServerMessage, LspTransportError> {
|
||||||
|
let payload = self.read_frame().await?;
|
||||||
|
let value: JsonValue = serde_json::from_slice(&payload).map_err(|error| {
|
||||||
|
LspTransportError::InvalidResponse {
|
||||||
|
method: "unknown".to_string(),
|
||||||
|
details: error.to_string(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Responses have an "id" field; notifications have "method" but no "id"
|
||||||
|
if value.get("id").is_some() {
|
||||||
|
let response: LspResponse = serde_json::from_value(value).map_err(|error| {
|
||||||
|
LspTransportError::InvalidResponse {
|
||||||
|
method: "unknown".to_string(),
|
||||||
|
details: format!("failed to parse response: {error}"),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(LspServerMessage::Response(response))
|
||||||
|
} else if value.get("method").is_some() {
|
||||||
|
let notification: LspNotification = serde_json::from_value(value).map_err(|error| {
|
||||||
|
LspTransportError::InvalidResponse {
|
||||||
|
method: "unknown".to_string(),
|
||||||
|
details: format!("failed to parse notification: {error}"),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(LspServerMessage::Notification(notification))
|
||||||
|
} else {
|
||||||
|
Err(LspTransportError::InvalidResponse {
|
||||||
|
method: "unknown".to_string(),
|
||||||
|
details: "message has neither 'id' nor 'method'".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a response from the server. Interleaved notifications are queued.
|
||||||
pub async fn read_response(&mut self) -> Result<LspResponse, LspTransportError> {
|
pub async fn read_response(&mut self) -> Result<LspResponse, LspTransportError> {
|
||||||
self.read_jsonrpc_message().await
|
loop {
|
||||||
|
match self.read_message().await? {
|
||||||
|
LspServerMessage::Response(r) => return Ok(r),
|
||||||
|
LspServerMessage::Notification(n) => {
|
||||||
|
self.pending_notifications.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain and return all queued server-initiated notifications.
|
||||||
|
pub fn drain_notifications(&mut self) -> Vec<LspNotification> {
|
||||||
|
std::mem::take(&mut self.pending_notifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn shutdown(&mut self) -> Result<(), LspTransportError> {
|
pub async fn shutdown(&mut self) -> Result<(), LspTransportError> {
|
||||||
|
|
@ -344,16 +419,6 @@ impl LspTransport {
|
||||||
|
|
||||||
Ok(payload)
|
Ok(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_jsonrpc_message<T: serde::de::DeserializeOwned>(
|
|
||||||
&mut self,
|
|
||||||
) -> Result<T, LspTransportError> {
|
|
||||||
let payload = self.read_frame().await?;
|
|
||||||
serde_json::from_slice(&payload).map_err(|error| LspTransportError::InvalidResponse {
|
|
||||||
method: "unknown".to_string(),
|
|
||||||
details: error.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue