1use crate::project::ProjectRoot;
2use anyhow::{Context, Result, bail};
3use serde_json::{Value, json};
4use std::collections::HashMap;
5use std::io::{BufRead, BufReader};
6use std::path::Path;
7use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
8use std::thread;
9use std::time::{Duration, Instant};
10use url::Url;
11
12use super::commands::is_allowed_lsp_command;
13use super::protocol::{language_id_for_path, poll_readable, read_message, send_message};
14use super::registry::resolve_lsp_binary;
15use super::types::{
16 LspCodeActionRefactorPlan, LspCodeActionRefactorResult, LspCodeActionRequest, LspDiagnostic,
17 LspDiagnosticRequest, LspReference, LspRenamePlan, LspRenamePlanRequest, LspRenameRequest,
18 LspRequest, LspResolveTargetRequest, LspResolvedTarget, LspTypeHierarchyRequest,
19 LspWorkspaceEditTransaction, LspWorkspaceSymbol, LspWorkspaceSymbolRequest,
20};
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23struct SessionKey {
24 command: String,
25 args: Vec<String>,
26}
27
28#[derive(Debug, Clone)]
29struct OpenDocumentState {
30 version: i32,
31 text: String,
32}
33
34pub struct LspSessionPool {
35 project: ProjectRoot,
36 sessions: std::sync::Mutex<HashMap<SessionKey, LspSession>>,
37}
38
39pub(super) struct LspSession {
40 pub(super) project: ProjectRoot,
41 child: Child,
42 stdin: ChildStdin,
43 reader: BufReader<ChildStdout>,
44 next_request_id: u64,
45 documents: HashMap<String, OpenDocumentState>,
46 #[allow(dead_code)] stderr_buffer: std::sync::Arc<std::sync::Mutex<String>>,
48}
49
50fn ensure_session<'a>(
51 sessions: &'a mut HashMap<SessionKey, LspSession>,
52 project: &ProjectRoot,
53 command: &str,
54 args: &[String],
55) -> Result<&'a mut LspSession> {
56 if !is_allowed_lsp_command(command) {
57 bail!(
58 "Blocked: '{command}' is not a known LSP server. Only whitelisted LSP binaries are allowed."
59 );
60 }
61
62 let key = SessionKey {
63 command: command.to_owned(),
64 args: args.to_owned(),
65 };
66
67 if let Some(session) = sessions.get_mut(&key) {
69 match session.child.try_wait() {
70 Ok(Some(_status)) => {
71 sessions.remove(&key);
73 }
74 Ok(None) => {} Err(_) => {
76 sessions.remove(&key);
77 }
78 }
79 }
80
81 match sessions.entry(key) {
82 std::collections::hash_map::Entry::Occupied(e) => Ok(e.into_mut()),
83 std::collections::hash_map::Entry::Vacant(e) => {
84 let session = LspSession::start(project, command, args)?;
85 Ok(e.insert(session))
86 }
87 }
88}
89
90impl LspSessionPool {
91 pub fn new(project: ProjectRoot) -> Self {
92 Self {
93 project,
94 sessions: std::sync::Mutex::new(HashMap::new()),
95 }
96 }
97
98 pub fn reset(&self, project: ProjectRoot) -> Self {
100 self.sessions
102 .lock()
103 .unwrap_or_else(|p| p.into_inner())
104 .clear();
105 Self::new(project)
106 }
107
108 pub fn session_count(&self) -> usize {
109 self.sessions
110 .lock()
111 .unwrap_or_else(|p| p.into_inner())
112 .len()
113 }
114
115 pub fn find_referencing_symbols(&self, request: &LspRequest) -> Result<Vec<LspReference>> {
116 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
117 let session = ensure_session(
118 &mut sessions,
119 &self.project,
120 &request.command,
121 &request.args,
122 )?;
123 session.find_references(request)
124 }
125
126 pub fn get_diagnostics(&self, request: &LspDiagnosticRequest) -> Result<Vec<LspDiagnostic>> {
127 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
128 let session = ensure_session(
129 &mut sessions,
130 &self.project,
131 &request.command,
132 &request.args,
133 )?;
134 session.get_diagnostics(request)
135 }
136
137 pub fn search_workspace_symbols(
138 &self,
139 request: &LspWorkspaceSymbolRequest,
140 ) -> Result<Vec<LspWorkspaceSymbol>> {
141 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
142 let session = ensure_session(
143 &mut sessions,
144 &self.project,
145 &request.command,
146 &request.args,
147 )?;
148 session.search_workspace_symbols(request)
149 }
150
151 pub fn get_type_hierarchy(
152 &self,
153 request: &LspTypeHierarchyRequest,
154 ) -> Result<HashMap<String, Value>> {
155 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
156 let session = ensure_session(
157 &mut sessions,
158 &self.project,
159 &request.command,
160 &request.args,
161 )?;
162 session.get_type_hierarchy(request)
163 }
164
165 pub fn resolve_symbol_target(
166 &self,
167 request: &LspResolveTargetRequest,
168 ) -> Result<Vec<LspResolvedTarget>> {
169 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
170 let session = ensure_session(
171 &mut sessions,
172 &self.project,
173 &request.command,
174 &request.args,
175 )?;
176 session.resolve_symbol_target(request)
177 }
178
179 pub fn get_rename_plan(&self, request: &LspRenamePlanRequest) -> Result<LspRenamePlan> {
180 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
181 let session = ensure_session(
182 &mut sessions,
183 &self.project,
184 &request.command,
185 &request.args,
186 )?;
187 session.get_rename_plan(request)
188 }
189
190 pub fn rename_symbol(&self, request: &LspRenameRequest) -> Result<crate::rename::RenameResult> {
191 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
192 let session = ensure_session(
193 &mut sessions,
194 &self.project,
195 &request.command,
196 &request.args,
197 )?;
198 session.rename_symbol(request)
199 }
200
201 pub fn rename_symbol_transaction(
202 &self,
203 request: &LspRenameRequest,
204 ) -> Result<LspWorkspaceEditTransaction> {
205 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
206 let session = ensure_session(
207 &mut sessions,
208 &self.project,
209 &request.command,
210 &request.args,
211 )?;
212 session.rename_symbol_transaction(request)
213 }
214
215 pub fn code_action_refactor(
216 &self,
217 request: &LspCodeActionRequest,
218 ) -> Result<LspCodeActionRefactorResult> {
219 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
220 let session = ensure_session(
221 &mut sessions,
222 &self.project,
223 &request.command,
224 &request.args,
225 )?;
226 session.code_action_refactor(request)
227 }
228
229 pub fn code_action_refactor_plan(
230 &self,
231 request: &LspCodeActionRequest,
232 ) -> Result<LspCodeActionRefactorPlan> {
233 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
234 let session = ensure_session(
235 &mut sessions,
236 &self.project,
237 &request.command,
238 &request.args,
239 )?;
240 session.code_action_refactor_plan(request)
241 }
242}
243
244impl LspSession {
245 fn start(project: &ProjectRoot, command: &str, args: &[String]) -> Result<Self> {
246 let command_path = resolve_lsp_binary(command).unwrap_or_else(|| command.into());
247 let mut child = Command::new(&command_path)
248 .args(args)
249 .stdin(Stdio::piped())
250 .stdout(Stdio::piped())
251 .stderr(Stdio::piped())
252 .spawn()
253 .with_context(|| format!("failed to spawn LSP server {}", command_path.display()))?;
254
255 let stdin = child.stdin.take().context("failed to open LSP stdin")?;
256 let stdout = child.stdout.take().context("failed to open LSP stdout")?;
257
258 let stderr_buffer = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
260 if let Some(stderr) = child.stderr.take() {
261 let buf = std::sync::Arc::clone(&stderr_buffer);
262 thread::spawn(move || {
263 let mut reader = BufReader::new(stderr);
264 let mut line = String::new();
265 while reader.read_line(&mut line).unwrap_or(0) > 0 {
266 if let Ok(mut b) = buf.lock() {
267 if b.len() > 4096 {
268 let drain_to = b.len() - 2048;
269 b.drain(..drain_to);
270 }
271 b.push_str(&line);
272 }
273 line.clear();
274 }
275 });
276 }
277
278 let mut session = Self {
279 project: project.clone(),
280 child,
281 stdin,
282 reader: BufReader::new(stdout),
283 next_request_id: 1,
284 documents: HashMap::new(),
285 stderr_buffer,
286 };
287 session.initialize()?;
288 if let Some(grace) = configured_startup_grace() {
289 thread::sleep(grace);
290 }
291 Ok(session)
292 }
293
294 fn initialize(&mut self) -> Result<()> {
295 let id = self.next_id();
296 let root_uri = Url::from_directory_path(self.project.as_path())
297 .ok()
298 .map(|url| url.to_string());
299 self.send_request(
300 id,
301 "initialize",
302 json!({
303 "processId":null,
304 "rootUri": root_uri,
305 "capabilities":{
306 "workspace":{
307 "workspaceEdit":{
308 "documentChanges":true,
309 "resourceOperations":["create","rename","delete"],
310 "failureHandling":"textOnlyTransactional"
311 },
312 "symbol":{"dynamicRegistration":false}
313 },
314 "textDocument":{
315 "declaration":{"dynamicRegistration":false},
316 "definition":{"dynamicRegistration":false},
317 "implementation":{"dynamicRegistration":false},
318 "typeDefinition":{"dynamicRegistration":false},
319 "references":{"dynamicRegistration":false},
320 "rename":{"dynamicRegistration":false,"prepareSupport":true},
321 "diagnostic":{"dynamicRegistration":false},
322 "typeHierarchy":{"dynamicRegistration":false},
323 "codeAction":{
324 "dynamicRegistration":false,
325 "codeActionLiteralSupport":{
326 "codeActionKind":{
327 "valueSet":["quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite"]
328 }
329 },
330 "resolveSupport":{"properties":["edit","command"]}
331 }
332 }
333 },
334 "workspaceFolders":[
335 {
336 "uri": Url::from_directory_path(self.project.as_path()).ok().map(|url| url.to_string()),
337 "name": self.project.as_path().file_name().and_then(|n| n.to_str()).unwrap_or("workspace")
338 }
339 ]
340 }),
341 )?;
342 let _ = self.read_response_for_id(id)?;
343 self.send_notification("initialized", json!({}))?;
344 Ok(())
345 }
346
347 pub(super) fn prepare_document(&mut self, absolute_path: &Path) -> Result<(String, String)> {
348 let uri = Url::from_file_path(absolute_path).map_err(|_| {
349 anyhow::anyhow!("failed to build file uri for {}", absolute_path.display())
350 })?;
351 let uri_string = uri.to_string();
352 let source = std::fs::read_to_string(absolute_path)
353 .with_context(|| format!("failed to read {}", absolute_path.display()))?;
354 let language_id = language_id_for_path(absolute_path)?;
355 self.sync_document(&uri_string, language_id, &source)?;
356 Ok((uri_string, source))
357 }
358
359 fn sync_document(&mut self, uri: &str, language_id: &str, source: &str) -> Result<()> {
360 if let Some(state) = self.documents.get(uri)
361 && state.text == source
362 {
363 return Ok(());
364 }
365
366 if let Some(state) = self.documents.get_mut(uri) {
367 state.version += 1;
368 state.text = source.to_owned();
369 let version = state.version;
370 return self.send_notification(
371 "textDocument/didChange",
372 json!({
373 "textDocument":{"uri":uri,"version":version},
374 "contentChanges":[{"text":source}]
375 }),
376 );
377 }
378
379 self.documents.insert(
380 uri.to_owned(),
381 OpenDocumentState {
382 version: 1,
383 text: source.to_owned(),
384 },
385 );
386 self.send_notification(
387 "textDocument/didOpen",
388 json!({
389 "textDocument":{
390 "uri":uri,
391 "languageId":language_id,
392 "version":1,
393 "text":source
394 }
395 }),
396 )
397 }
398
399 pub(super) fn next_id(&mut self) -> u64 {
400 let id = self.next_request_id;
401 self.next_request_id += 1;
402 id
403 }
404
405 pub(super) fn send_request(&mut self, id: u64, method: &str, params: Value) -> Result<()> {
406 send_message(
407 &mut self.stdin,
408 &json!({
409 "jsonrpc":"2.0",
410 "id":id,
411 "method":method,
412 "params":params
413 }),
414 )
415 }
416
417 fn send_notification(&mut self, method: &str, params: Value) -> Result<()> {
418 send_message(
419 &mut self.stdin,
420 &json!({
421 "jsonrpc":"2.0",
422 "method":method,
423 "params":params
424 }),
425 )
426 }
427
428 pub(super) fn read_response_for_id(&mut self, expected_id: u64) -> Result<Value> {
429 let deadline = Instant::now() + Duration::from_secs(30);
430 let mut discarded = 0u32;
431 const MAX_DISCARDED: u32 = 500;
432
433 loop {
434 let remaining = deadline.saturating_duration_since(Instant::now());
435 if remaining.is_zero() {
436 bail!(
437 "LSP response timeout: no response for request id {expected_id} within 30s \
438 ({discarded} unrelated messages discarded)"
439 );
440 }
441 if discarded >= MAX_DISCARDED {
442 bail!(
443 "LSP response loop: discarded {MAX_DISCARDED} messages without finding id {expected_id}"
444 );
445 }
446
447 if !poll_readable(self.reader.get_ref(), remaining.min(Duration::from_secs(5))) {
449 continue; }
451
452 let message = read_message(&mut self.reader)?;
453 let matches_id = message
454 .get("id")
455 .and_then(Value::as_u64)
456 .map(|id| id == expected_id)
457 .unwrap_or(false);
458 if matches_id {
459 if let Some(error) = message.get("error") {
460 let code = error.get("code").and_then(Value::as_i64).unwrap_or(-1);
461 let error_message = error
462 .get("message")
463 .and_then(Value::as_str)
464 .unwrap_or("unknown LSP error");
465 bail!("LSP request failed ({code}): {error_message}");
466 }
467 return Ok(message);
468 }
469 discarded += 1;
470 }
471 }
472
473 fn shutdown(&mut self) -> Result<()> {
474 let id = self.next_id();
475 self.send_request(id, "shutdown", Value::Null)?;
476 let _ = self.read_response_for_id(id)?;
477 self.send_notification("exit", Value::Null)
478 }
479}
480
481fn configured_startup_grace() -> Option<Duration> {
482 let millis = std::env::var("CODELENS_LSP_STARTUP_GRACE_MS")
483 .ok()
484 .and_then(|value| value.trim().parse::<u64>().ok())
485 .unwrap_or(0)
486 .min(10_000);
487 (millis > 0).then(|| Duration::from_millis(millis))
488}
489
490impl Drop for LspSession {
491 fn drop(&mut self) {
492 let _ = self.shutdown();
493 let deadline = Instant::now() + Duration::from_millis(250);
494 while Instant::now() < deadline {
495 match self.child.try_wait() {
496 Ok(Some(_status)) => return,
497 Ok(None) => thread::sleep(Duration::from_millis(10)),
498 Err(_) => break,
499 }
500 }
501 let _ = self.child.kill();
502 let _ = self.child.wait();
503 }
504}