Skip to main content

aether_lspd/
protocol.rs

1use crate::language_catalog::LanguageId;
2use lsp_types::Uri;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::io;
6use std::path::PathBuf;
7
8#[doc = include_str!("docs/protocol.md")]
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub enum DaemonRequest {
11    Initialize(InitializeRequest),
12    LspCall {
13        client_id: i64,
14        method: String,
15        params: Value,
16    },
17    GetDiagnostics {
18        client_id: i64,
19        /// If None, return all cached diagnostics for the workspace
20        uri: Option<Uri>,
21    },
22    QueueDiagnosticRefresh {
23        client_id: i64,
24        uri: Uri,
25    },
26    Disconnect,
27    Ping,
28}
29
30/// Initialize request to set up LSP for a workspace
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct InitializeRequest {
33    pub workspace_root: PathBuf,
34    pub language: LanguageId,
35}
36
37/// LSP notification from client to server
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct LspNotification {
40    pub method: String,
41    pub params: Value,
42}
43
44/// Top-level daemon response
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum DaemonResponse {
47    Initialized,
48    Pong,
49    LspResult { client_id: i64, result: Result<Value, LspErrorResponse> },
50    Error(ProtocolError),
51}
52
53/// LSP error response
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct LspErrorResponse {
56    pub code: i32,
57    pub message: String,
58}
59
60/// Protocol-level error (not LSP error)
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ProtocolError {
63    pub message: String,
64    /// Optional `client_id` for correlating errors back to LSP requests
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub client_id: Option<i64>,
67}
68
69impl ProtocolError {
70    pub fn new(message: impl Into<String>) -> Self {
71        Self { message: message.into(), client_id: None }
72    }
73
74    pub fn with_client_id(message: impl Into<String>, client_id: i64) -> Self {
75        Self { message: message.into(), client_id: Some(client_id) }
76    }
77}
78
79/// Extract the document URI from an LSP request's params by method name.
80///
81/// Used by the daemon for auto-open: if the request targets a specific file,
82/// the daemon ensures the file is opened before forwarding the request.
83pub fn extract_document_uri(method: &str, params: &Value) -> Option<Uri> {
84    if !method.starts_with("textDocument/") {
85        return None;
86    }
87    params.pointer("/textDocument/uri").and_then(|v| v.as_str()).and_then(|s| s.parse().ok())
88}
89
90/// Maximum message size (16 MB)
91pub const MAX_MESSAGE_SIZE: u32 = 16 * 1024 * 1024;
92
93/// Error code for a request that exceeded the daemon's per-request timeout.
94/// The daemon kills and replaces the language server when this fires.
95///
96/// Daemon-synthesized codes live outside the ranges reserved by JSON-RPC
97/// (-32000..=-32768) and LSP (-32800..=-32899), so they can never collide with
98/// a code forwarded verbatim from a real language server.
99pub const LSP_REQUEST_TIMED_OUT: i32 = -33001;
100
101/// Error code for a request that failed because the language server process
102/// exited or its transport shut down before responding.
103pub const LSP_TRANSPORT_CLOSED: i32 = -33002;
104
105/// Read a length-prefixed frame from an async reader
106pub(crate) async fn read_frame<R, T>(reader: &mut R) -> io::Result<Option<T>>
107where
108    R: tokio::io::AsyncReadExt + Unpin,
109    T: for<'de> Deserialize<'de>,
110{
111    let mut len_buf = [0u8; 4];
112    match reader.read_exact(&mut len_buf).await {
113        Ok(_) => {}
114        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
115        Err(e) => return Err(e),
116    }
117
118    let len = u32::from_be_bytes(len_buf);
119
120    if len > MAX_MESSAGE_SIZE {
121        return Err(io::Error::new(io::ErrorKind::InvalidData, format!("Message too large: {len} bytes")));
122    }
123
124    let mut buf = vec![0u8; len as usize];
125    reader.read_exact(&mut buf).await?;
126
127    serde_json::from_slice(&buf).map(Some).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
128}
129
130/// Write a length-prefixed frame to an async writer
131pub(crate) async fn write_frame<W, T>(writer: &mut W, message: &T) -> io::Result<()>
132where
133    W: tokio::io::AsyncWriteExt + Unpin,
134    T: Serialize,
135{
136    let json = serde_json::to_vec(message)?;
137
138    if json.len() > MAX_MESSAGE_SIZE as usize {
139        return Err(io::Error::new(io::ErrorKind::InvalidData, format!("Message too large: {} bytes", json.len())));
140    }
141
142    let len = u32::try_from(json.len()).unwrap_or(u32::MAX);
143    writer.write_all(&len.to_be_bytes()).await?;
144    writer.write_all(&json).await?;
145    writer.flush().await
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_protocol_error_new() {
154        let err = ProtocolError::new("test error");
155        assert_eq!(err.message, "test error");
156    }
157
158    #[test]
159    fn test_daemon_request_lsp_call_roundtrip() {
160        let req = DaemonRequest::LspCall {
161            client_id: 42,
162            method: "textDocument/definition".to_string(),
163            params: serde_json::json!({
164                "textDocument": { "uri": "file:///test.rs" },
165                "position": { "line": 0, "character": 0 }
166            }),
167        };
168        let json = serde_json::to_string(&req).unwrap();
169        let decoded: DaemonRequest = serde_json::from_str(&json).unwrap();
170        match decoded {
171            DaemonRequest::LspCall { client_id, method, .. } => {
172                assert_eq!(client_id, 42);
173                assert_eq!(method, "textDocument/definition");
174            }
175            _ => panic!("Wrong variant"),
176        }
177    }
178
179    #[test]
180    fn test_extract_document_uri_definition() {
181        let params = serde_json::json!({
182            "textDocument": { "uri": "file:///src/main.rs" },
183            "position": { "line": 10, "character": 5 }
184        });
185        let uri = extract_document_uri("textDocument/definition", &params);
186        assert!(uri.is_some());
187        assert_eq!(uri.unwrap().as_str(), "file:///src/main.rs");
188    }
189
190    #[test]
191    fn test_extract_document_uri_references() {
192        let params = serde_json::json!({
193            "textDocument": { "uri": "file:///src/lib.rs" },
194            "position": { "line": 5, "character": 3 },
195            "context": { "includeDeclaration": true }
196        });
197        let uri = extract_document_uri("textDocument/references", &params);
198        assert!(uri.is_some());
199        assert_eq!(uri.unwrap().as_str(), "file:///src/lib.rs");
200    }
201
202    #[test]
203    fn test_extract_document_uri_document_symbol() {
204        let params = serde_json::json!({
205            "textDocument": { "uri": "file:///src/foo.rs" }
206        });
207        let uri = extract_document_uri("textDocument/documentSymbol", &params);
208        assert!(uri.is_some());
209        assert_eq!(uri.unwrap().as_str(), "file:///src/foo.rs");
210    }
211
212    #[test]
213    fn test_extract_document_uri_workspace_symbol_returns_none() {
214        let params = serde_json::json!({ "query": "Foo" });
215        let uri = extract_document_uri("workspace/symbol", &params);
216        assert!(uri.is_none());
217    }
218
219    #[test]
220    fn test_extract_document_uri_unknown_method_returns_none() {
221        let params = serde_json::json!({});
222        let uri = extract_document_uri("textDocument/unknown", &params);
223        assert!(uri.is_none());
224    }
225}