1use anyhow::Result;
6use lsp_types::{
7 ClientCapabilities, CompletionItem, DocumentSymbol, Location, Position, Range,
8 ServerCapabilities, SymbolInformation, TextDocumentIdentifier, TextDocumentItem,
9};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use tracing::{info, warn};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct LspConfig {
17 pub command: String,
19 #[serde(default)]
21 pub args: Vec<String>,
22 pub root_uri: Option<String>,
24 #[serde(default)]
26 pub file_extensions: Vec<String>,
27 #[serde(default)]
29 pub initialization_options: Option<Value>,
30 #[serde(default = "default_timeout")]
32 pub timeout_ms: u64,
33}
34
35fn default_timeout() -> u64 {
36 30000
37}
38
39impl Default for LspConfig {
40 fn default() -> Self {
41 Self {
42 command: String::new(),
43 args: Vec::new(),
44 root_uri: None,
45 file_extensions: Vec::new(),
46 initialization_options: None,
47 timeout_ms: default_timeout(),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct JsonRpcRequest {
55 pub jsonrpc: String,
56 pub id: i64,
57 pub method: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub params: Option<Value>,
60}
61
62impl JsonRpcRequest {
63 pub fn new(id: i64, method: &str, params: Option<Value>) -> Self {
64 Self {
65 jsonrpc: "2.0".to_string(),
66 id,
67 method: method.to_string(),
68 params,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct JsonRpcResponse {
76 pub jsonrpc: String,
77 pub id: i64,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub result: Option<Value>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub error: Option<JsonRpcError>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct JsonRpcError {
87 pub code: i64,
88 pub message: String,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub data: Option<Value>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct JsonRpcNotification {
96 pub jsonrpc: String,
97 pub method: String,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub params: Option<Value>,
100}
101
102impl JsonRpcNotification {
103 pub fn new(method: &str, params: Option<Value>) -> Self {
104 Self {
105 jsonrpc: "2.0".to_string(),
106 method: method.to_string(),
107 params,
108 }
109 }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct InitializeParams {
116 pub process_id: Option<i64>,
117 pub client_info: ClientInfo,
118 pub locale: Option<String>,
119 pub root_path: Option<String>,
120 pub root_uri: Option<String>,
121 pub initialization_options: Option<Value>,
122 pub capabilities: ClientCapabilities,
123 pub trace: Option<String>,
124 pub workspace_folders: Option<Vec<WorkspaceFolder>>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ClientInfo {
129 pub name: String,
130 pub version: String,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct WorkspaceFolder {
135 pub uri: String,
136 pub name: String,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct InitializeResult {
143 pub capabilities: ServerCapabilities,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub server_info: Option<ServerInfo>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ServerInfo {
150 pub name: String,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub version: Option<String>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct DidOpenTextDocumentParams {
159 pub text_document: TextDocumentItem,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct DidCloseTextDocumentParams {
166 pub text_document: TextDocumentIdentifier,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct DidChangeTextDocumentParams {
173 pub text_document: VersionedTextDocumentIdentifier,
174 pub content_changes: Vec<TextDocumentContentChangeEvent>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct VersionedTextDocumentIdentifier {
180 pub uri: String,
181 pub version: i32,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct TextDocumentContentChangeEvent {
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub range: Option<Range>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub range_length: Option<u32>,
191 pub text: String,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct ReferenceContext {
198 pub include_declaration: bool,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct ReferenceParams {
205 pub text_document: TextDocumentIdentifier,
206 pub position: Position,
207 pub context: ReferenceContext,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct WorkspaceSymbolParams {
214 pub query: String,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219#[serde(tag = "type", rename_all = "camelCase")]
220pub enum LspActionResult {
221 Definition { locations: Vec<LocationInfo> },
223 References { locations: Vec<LocationInfo> },
225 Hover {
227 contents: String,
228 range: Option<RangeInfo>,
229 },
230 DocumentSymbols { symbols: Vec<SymbolInfo> },
232 WorkspaceSymbols { symbols: Vec<SymbolInfo> },
234 Implementation { locations: Vec<LocationInfo> },
236 Completion { items: Vec<CompletionItemInfo> },
238 Error { message: String },
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct LocationInfo {
245 pub uri: String,
246 pub range: RangeInfo,
247}
248
249impl From<Location> for LocationInfo {
250 fn from(loc: Location) -> Self {
251 Self {
252 uri: loc.uri.to_string(),
253 range: RangeInfo::from(loc.range),
254 }
255 }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct RangeInfo {
261 pub start: PositionInfo,
262 pub end: PositionInfo,
263}
264
265impl From<Range> for RangeInfo {
266 fn from(range: Range) -> Self {
267 Self {
268 start: PositionInfo::from(range.start),
269 end: PositionInfo::from(range.end),
270 }
271 }
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct PositionInfo {
277 pub line: u32,
278 pub character: u32,
279}
280
281impl From<Position> for PositionInfo {
282 fn from(pos: Position) -> Self {
283 Self {
284 line: pos.line,
285 character: pos.character,
286 }
287 }
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct SymbolInfo {
293 pub name: String,
294 #[serde(rename = "type")]
295 pub kind: String,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub detail: Option<String>,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub uri: Option<String>,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub range: Option<RangeInfo>,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub container_name: Option<String>,
304}
305
306impl From<DocumentSymbol> for SymbolInfo {
307 fn from(sym: DocumentSymbol) -> Self {
308 Self {
309 name: sym.name,
310 kind: format!("{:?}", sym.kind),
311 detail: sym.detail,
312 uri: None,
313 range: Some(RangeInfo::from(sym.range)),
314 container_name: None,
315 }
316 }
317}
318
319impl From<SymbolInformation> for SymbolInfo {
320 fn from(sym: SymbolInformation) -> Self {
321 Self {
322 name: sym.name,
323 kind: format!("{:?}", sym.kind),
324 detail: None,
325 uri: Some(sym.location.uri.to_string()),
326 range: Some(RangeInfo::from(sym.location.range)),
327 container_name: sym.container_name,
328 }
329 }
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct CompletionItemInfo {
335 pub label: String,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub kind: Option<String>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub detail: Option<String>,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub documentation: Option<String>,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 pub insert_text: Option<String>,
344}
345
346impl From<CompletionItem> for CompletionItemInfo {
347 fn from(item: CompletionItem) -> Self {
348 Self {
349 label: item.label,
350 kind: item.kind.map(|k| format!("{:?}", k)),
351 detail: item.detail,
352 documentation: item.documentation.map(|d| match d {
353 lsp_types::Documentation::String(s) => s,
354 lsp_types::Documentation::MarkupContent(mc) => mc.value,
355 }),
356 insert_text: item.insert_text,
357 }
358 }
359}
360
361pub fn get_language_server_config(language: &str) -> Option<LspConfig> {
363 match language {
364 "rust" => Some(LspConfig {
365 command: "rust-analyzer".to_string(),
366 args: vec![],
367 file_extensions: vec!["rs".to_string()],
368 ..Default::default()
369 }),
370 "typescript" | "javascript" => Some(LspConfig {
371 command: "typescript-language-server".to_string(),
372 args: vec!["--stdio".to_string()],
373 file_extensions: vec![
374 "ts".to_string(),
375 "tsx".to_string(),
376 "js".to_string(),
377 "jsx".to_string(),
378 ],
379 ..Default::default()
380 }),
381 "python" => Some(LspConfig {
382 command: "pylsp".to_string(),
383 args: vec![],
384 file_extensions: vec!["py".to_string()],
385 ..Default::default()
386 }),
387 "go" => Some(LspConfig {
388 command: "gopls".to_string(),
389 args: vec!["serve".to_string()],
390 file_extensions: vec!["go".to_string()],
391 ..Default::default()
392 }),
393 "c" | "cpp" | "c++" => Some(LspConfig {
394 command: "clangd".to_string(),
395 args: vec![],
396 file_extensions: vec![
397 "c".to_string(),
398 "cpp".to_string(),
399 "cc".to_string(),
400 "cxx".to_string(),
401 "h".to_string(),
402 "hpp".to_string(),
403 ],
404 ..Default::default()
405 }),
406 _ => None,
407 }
408}
409
410fn install_command_for(command: &str) -> Option<&'static [&'static str]> {
412 match command {
413 "rust-analyzer" => Some(&["rustup", "component", "add", "rust-analyzer"]),
414 "typescript-language-server" => Some(&[
415 "npm",
416 "install",
417 "-g",
418 "typescript-language-server",
419 "typescript",
420 ]),
421 "pylsp" => Some(&["pip", "install", "--user", "python-lsp-server"]),
422 "gopls" => Some(&["go", "install", "golang.org/x/tools/gopls@latest"]),
423 "clangd" => None, _ => None,
425 }
426}
427
428pub async fn ensure_server_installed(config: &LspConfig) -> Result<()> {
430 if which::which(&config.command).is_ok() {
432 return Ok(());
433 }
434
435 let Some(install_args) = install_command_for(&config.command) else {
436 return Err(anyhow::anyhow!(
437 "Language server '{}' not found and no auto-install available. \
438 Install it manually.",
439 config.command,
440 ));
441 };
442
443 info!(command = %config.command, "Language server not found, installing...");
444
445 let status = tokio::process::Command::new(install_args[0])
446 .args(&install_args[1..])
447 .stdout(std::process::Stdio::piped())
448 .stderr(std::process::Stdio::piped())
449 .status()
450 .await?;
451
452 if !status.success() {
453 return Err(anyhow::anyhow!(
454 "Failed to install '{}' (exit code {:?}). Install it manually.",
455 config.command,
456 status.code(),
457 ));
458 }
459
460 if which::which(&config.command).is_err() {
462 warn!(command = %config.command, "Install succeeded but binary still not found on PATH");
463 } else {
464 info!(command = %config.command, "Language server installed successfully");
465 }
466
467 Ok(())
468}
469
470pub fn detect_language_from_path(path: &str) -> Option<&'static str> {
472 let ext = path.rsplit('.').next()?;
473 match ext {
474 "rs" => Some("rust"),
475 "ts" | "tsx" => Some("typescript"),
476 "js" | "jsx" => Some("javascript"),
477 "py" => Some("python"),
478 "go" => Some("go"),
479 "c" => Some("c"),
480 "cpp" | "cc" | "cxx" => Some("cpp"),
481 "h" => Some("c"),
482 "hpp" => Some("cpp"),
483 _ => None,
484 }
485}