1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use fastrace::trace;
6use leta_config::Config;
7use leta_fs::{get_language_id, path_to_uri, read_file_content, uri_to_path};
8use leta_lsp::LspClient;
9use leta_servers::{get_server_env, get_server_for_file, get_server_for_language, ServerConfig};
10use serde_json::Value;
11use tokio::sync::{Mutex, RwLock};
12use tracing::{debug, info};
13
14#[derive(Clone)]
15pub struct OpenDocument {
16 _uri: String,
17 _version: i32,
18 pub content: String,
19 _language_id: String,
20}
21
22pub struct Workspace {
23 root: PathBuf,
24 server_config: &'static ServerConfig,
25 client: Option<Arc<LspClient>>,
26 open_documents: HashMap<String, OpenDocument>,
27 startup_stats: Option<leta_types::ServerStartupStats>,
28}
29
30impl Workspace {
31 pub fn new(root: PathBuf, server_config: &'static ServerConfig) -> Self {
32 Self {
33 root,
34 server_config,
35 client: None,
36 open_documents: HashMap::new(),
37 startup_stats: None,
38 }
39 }
40
41 pub fn startup_stats(&self) -> Option<leta_types::ServerStartupStats> {
42 self.startup_stats.clone()
43 }
44
45 pub fn client(&self) -> Option<Arc<LspClient>> {
46 self.client.clone()
47 }
48
49 pub fn server_name(&self) -> &str {
50 self.server_config.name
51 }
52
53 pub fn open_document_uris(&self) -> Vec<String> {
54 self.open_documents.keys().cloned().collect()
55 }
56
57 #[trace]
58 pub async fn start_server(&mut self) -> Result<leta_types::ServerStartupStats, String> {
59 let total_start = std::time::Instant::now();
60
61 if self.client.is_some() {
62 return Ok(leta_types::ServerStartupStats {
63 server_name: self.server_config.name.to_string(),
64 workspace_root: self.root.to_string_lossy().to_string(),
65 init_time_ms: 0,
66 ready_time_ms: 0,
67 total_time_ms: 0,
68 functions: Vec::new(),
69 });
70 }
71
72 info!(
73 "Starting {} for {}",
74 self.server_config.name,
75 self.root.display()
76 );
77
78 let env = get_server_env();
79 let init_options = self.get_init_options();
80
81 let cmd: Vec<&str> = self.server_config.command.to_vec();
82
83 let start_time = std::time::Instant::now();
84 match LspClient::start(&cmd, &self.root, self.server_config.name, env, init_options).await {
85 Ok(client) => {
86 let init_time = start_time.elapsed();
87
88 if self.server_config.name == "clangd" {
89 client.wait_for_indexing(60).await;
90 self.ensure_workspace_indexed(&client).await;
91 }
92
93 self.client = Some(client);
94 let total_time = total_start.elapsed();
95 let ready_time = std::time::Duration::ZERO;
96
97 info!(
98 "Server {} initialized and ready in {:?}",
99 self.server_config.name, total_time
100 );
101
102 let stats = leta_types::ServerStartupStats {
103 server_name: self.server_config.name.to_string(),
104 workspace_root: self.root.to_string_lossy().to_string(),
105 init_time_ms: init_time.as_millis() as u64,
106 ready_time_ms: ready_time.as_millis() as u64,
107 total_time_ms: total_time.as_millis() as u64,
108 functions: Vec::new(),
109 };
110 self.startup_stats = Some(stats.clone());
111 Ok(stats)
112 }
113 Err(e) => {
114 let mut msg = format!(
115 "Language server '{}' for {} failed to start: {}",
116 self.server_config.name,
117 self.server_config.languages.join(", "),
118 e
119 );
120 if let Some(install_cmd) = self.server_config.install_cmd {
121 msg.push_str(&format!(
122 "\n\nTo install {}, run:\n {}\n\nIf you just installed it, run `leta daemon restart` to pick up PATH changes.",
123 self.server_config.name, install_cmd
124 ));
125 }
126 Err(msg)
127 }
128 }
129 }
130
131 fn get_init_options(&self) -> Option<Value> {
132 if self.server_config.name == "gopls" {
133 Some(serde_json::json!({
134 "linksInHover": false,
135 }))
136 } else {
137 None
138 }
139 }
140
141 #[trace]
147 async fn ensure_workspace_indexed(&mut self, client: &Arc<LspClient>) {
148 let walkdir_start = std::time::Instant::now();
149 let source_extensions = [".c", ".h", ".cpp", ".hpp", ".cc", ".cxx", ".hxx"];
150 let exclude_dirs: std::collections::HashSet<&str> =
151 ["build", ".git", "node_modules"].into_iter().collect();
152
153 let mut files_to_index = Vec::new();
154 for entry in jwalk::WalkDir::new(&self.root).process_read_dir(
155 move |_depth, _path, _state, children| {
156 children.retain(|entry| {
157 entry
158 .as_ref()
159 .map(|e| {
160 let name = e.file_name().to_string_lossy();
161 !exclude_dirs.contains(name.as_ref())
162 })
163 .unwrap_or(false)
164 });
165 },
166 ) {
167 let Ok(entry) = entry else { continue };
168 if entry.file_type().is_file() {
169 let path = entry.path();
170 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
171 if source_extensions
172 .iter()
173 .any(|s| s.trim_start_matches('.') == ext)
174 {
175 files_to_index.push(path);
176 }
177 }
178 }
179 }
180 let walkdir_elapsed = walkdir_start.elapsed();
181
182 if files_to_index.is_empty() {
183 fastrace::local::LocalSpan::add_properties(|| {
184 [
185 ("files_found", "0".to_string()),
186 (
187 "walkdir_ms",
188 format!("{:.1}", walkdir_elapsed.as_secs_f64() * 1000.0),
189 ),
190 ]
191 });
192 return;
193 }
194
195 info!("Pre-indexing {} files for clangd", files_to_index.len());
196
197 let open_start = std::time::Instant::now();
198 for file_path in &files_to_index {
199 let _ = self.ensure_document_open(file_path).await;
200 }
201 let open_elapsed = open_start.elapsed();
202
203 client.wait_for_indexing(30).await;
204
205 info!(
206 "Pre-indexing complete, closing {} documents",
207 self.open_documents.len()
208 );
209
210 let close_start = std::time::Instant::now();
211 self.close_all_documents().await;
212 let close_elapsed = close_start.elapsed();
213
214 fastrace::local::LocalSpan::add_properties(|| {
215 [
216 ("files_found", files_to_index.len().to_string()),
217 (
218 "walkdir_ms",
219 format!("{:.1}", walkdir_elapsed.as_secs_f64() * 1000.0),
220 ),
221 (
222 "open_docs_ms",
223 format!("{:.1}", open_elapsed.as_secs_f64() * 1000.0),
224 ),
225 (
226 "close_docs_ms",
227 format!("{:.1}", close_elapsed.as_secs_f64() * 1000.0),
228 ),
229 ]
230 });
231 }
232
233 #[trace]
234 pub async fn stop_server(&mut self) {
235 if let Some(client) = self.client.take() {
236 info!("Stopping {}", self.server_config.name);
237 let _ = client.stop().await;
238 }
239 self.open_documents.clear();
240 }
241
242 pub async fn ensure_document_open(&mut self, path: &Path) -> Result<(), String> {
243 self.cleanup_deleted_documents().await;
244
245 let uri = path_to_uri(path);
246
247 if let Some(doc) = self.open_documents.get(&uri) {
248 let current_content = read_file_content(path).map_err(|e| e.to_string())?;
249 if current_content != doc.content {
250 self.close_document(path).await;
251 } else {
252 return Ok(());
253 }
254 }
255
256 let content = read_file_content(path).map_err(|e| e.to_string())?;
257 let language_id = get_language_id(path).to_string();
258
259 let doc = OpenDocument {
260 _uri: uri.clone(),
261 _version: 1,
262 content: content.clone(),
263 _language_id: language_id.clone(),
264 };
265
266 self.open_documents.insert(uri.clone(), doc);
267
268 if let Some(client) = &self.client {
269 let params = serde_json::json!({
270 "textDocument": {
271 "uri": uri,
272 "languageId": language_id,
273 "version": 1,
274 "text": content,
275 }
276 });
277 let _ = client
278 .send_notification("textDocument/didOpen", params)
279 .await;
280
281 if client.server_name() == "ruby-lsp" {
285 let symbol_params = serde_json::json!({
286 "textDocument": {"uri": uri}
287 });
288 let _ = client
289 .send_request_raw("textDocument/documentSymbol", symbol_params)
290 .await;
291 }
292 }
293
294 Ok(())
295 }
296
297 #[trace]
298 pub async fn close_document(&mut self, path: &Path) {
299 let uri = path_to_uri(path);
300 if self.open_documents.remove(&uri).is_none() {
301 return;
302 }
303
304 if let Some(client) = &self.client {
305 let params = serde_json::json!({
306 "textDocument": {"uri": uri}
307 });
308 let _ = client
309 .send_notification("textDocument/didClose", params)
310 .await;
311 }
312 }
313
314 #[trace]
315 pub async fn close_all_documents(&mut self) {
316 if let Some(client) = &self.client {
317 for uri in self.open_documents.keys() {
318 let params = serde_json::json!({
319 "textDocument": {"uri": uri}
320 });
321 let _ = client
322 .send_notification("textDocument/didClose", params)
323 .await;
324 }
325 }
326 self.open_documents.clear();
327 }
328
329 pub async fn cleanup_deleted_documents(&mut self) {
330 let deleted_uris: Vec<String> = self
331 .open_documents
332 .keys()
333 .filter(|uri| !uri_to_path(uri).exists())
334 .cloned()
335 .collect();
336
337 for uri in deleted_uris {
338 self.open_documents.remove(&uri);
339 if let Some(client) = &self.client {
340 let params = serde_json::json!({
341 "textDocument": {"uri": uri}
342 });
343 let _ = client
344 .send_notification("textDocument/didClose", params)
345 .await;
346 }
347 }
348 }
349}
350
351type StartupLockKey = (PathBuf, String);
352type StartupLocks = Mutex<HashMap<StartupLockKey, Arc<Mutex<()>>>>;
353
354pub struct Session {
355 workspaces: RwLock<HashMap<PathBuf, HashMap<String, Workspace>>>,
356 config: RwLock<Config>,
357 workspace_profiling: RwLock<Vec<leta_types::WorkspaceProfilingData>>,
358 startup_locks: StartupLocks,
359}
360
361impl Session {
362 pub fn new(config: Config) -> Self {
363 Self {
364 workspaces: RwLock::new(HashMap::new()),
365 config: RwLock::new(config),
366 workspace_profiling: RwLock::new(Vec::new()),
367 startup_locks: Mutex::new(HashMap::new()),
368 }
369 }
370
371 pub async fn add_workspace_profiling(&self, data: leta_types::WorkspaceProfilingData) {
372 let mut profiling = self.workspace_profiling.write().await;
373 profiling.retain(|p| p.workspace_root != data.workspace_root);
374 profiling.push(data);
375 }
376
377 pub async fn get_workspace_profiling(&self) -> Vec<leta_types::WorkspaceProfilingData> {
378 self.workspace_profiling.read().await.clone()
379 }
380
381 #[trace]
382 pub async fn config(&self) -> Config {
383 self.config.read().await.clone()
384 }
385
386 #[trace]
387 pub async fn get_or_create_workspace(
388 &self,
389 file_path: &Path,
390 workspace_root: &Path,
391 ) -> Result<WorkspaceHandle<'_>, String> {
392 let server_config = {
393 let config = self.config.read().await;
394 get_server_for_file(file_path, Some(&config))
395 .ok_or_else(|| format!("No language server found for {}", file_path.display()))?
396 };
397 self.get_or_create_workspace_for_server(workspace_root, server_config)
400 .await
401 }
402
403 #[trace]
404 pub async fn get_or_create_workspace_for_language(
405 &self,
406 language_id: &str,
407 workspace_root: &Path,
408 ) -> Result<WorkspaceHandle<'_>, String> {
409 let server_config = {
410 let config = self.config.read().await;
411 get_server_for_language(language_id, Some(&config))
412 .ok_or_else(|| format!("No language server found for language {}", language_id))?
413 };
414 self.get_or_create_workspace_for_server(workspace_root, server_config)
417 .await
418 }
419
420 #[trace]
421 async fn get_or_create_workspace_for_server(
422 &self,
423 workspace_root: &Path,
424 server_config: &'static ServerConfig,
425 ) -> Result<WorkspaceHandle<'_>, String> {
426 let workspace_root = workspace_root
427 .canonicalize()
428 .unwrap_or_else(|_| workspace_root.to_path_buf());
429
430 let startup_lock = {
432 let mut locks = self.startup_locks.lock().await;
433 let key = (workspace_root.clone(), server_config.name.to_string());
434 locks
435 .entry(key)
436 .or_insert_with(|| Arc::new(Mutex::new(())))
437 .clone()
438 };
439
440 let _startup_guard = startup_lock.lock().await;
442
443 let needs_create = {
445 let workspaces = self.workspaces.read().await;
446 if let Some(servers) = workspaces.get(&workspace_root) {
447 if let Some(ws) = servers.get(server_config.name) {
448 ws.client.is_none() } else {
450 true }
452 } else {
453 true }
455 };
456
457 if needs_create {
458 debug!(
459 "Starting {} for {}",
460 server_config.name,
461 workspace_root.display()
462 );
463 let mut new_workspace = Workspace::new(workspace_root.clone(), server_config);
464 new_workspace.start_server().await?;
465
466 let mut workspaces = self.workspaces.write().await;
468 let servers = workspaces
469 .entry(workspace_root.clone())
470 .or_insert_with(HashMap::new);
471 servers.insert(server_config.name.to_string(), new_workspace);
472 }
473
474 Ok(WorkspaceHandle {
475 session: self,
476 workspace_root,
477 server_name: server_config.name.to_string(),
478 })
479 }
480
481 #[allow(dead_code)]
482 #[trace]
483 pub async fn get_workspace_for_file(&self, file_path: &Path) -> Option<WorkspaceHandle<'_>> {
484 let file_path = file_path
485 .canonicalize()
486 .unwrap_or_else(|_| file_path.to_path_buf());
487 let config = self.config.read().await;
488 let server_config = get_server_for_file(&file_path, Some(&config))?;
489
490 let workspaces = self.workspaces.read().await;
491 for (root, servers) in workspaces.iter() {
492 if file_path.starts_with(root) && servers.contains_key(server_config.name) {
493 return Some(WorkspaceHandle {
494 session: self,
495 workspace_root: root.clone(),
496 server_name: server_config.name.to_string(),
497 });
498 }
499 }
500 None
501 }
502
503 #[trace]
504 pub async fn list_workspaces(&self) -> Vec<(String, String, Option<u32>, Vec<String>)> {
505 let workspaces = self.workspaces.read().await;
506 let mut result = Vec::new();
507
508 for (root, servers) in workspaces.iter() {
509 for (_, ws) in servers.iter() {
510 let server_pid = ws.client.as_ref().and_then(|c| c.pid());
511 result.push((
512 root.to_string_lossy().to_string(),
513 ws.server_name().to_string(),
514 server_pid,
515 ws.open_document_uris(),
516 ));
517 }
518 }
519
520 result
521 }
522
523 #[trace]
524 pub async fn restart_workspace(&self, root: &Path) -> Result<Vec<String>, String> {
525 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
526 let mut workspaces = self.workspaces.write().await;
527
528 let mut restarted = Vec::new();
529 if let Some(servers) = workspaces.get_mut(&root) {
530 for (name, workspace) in servers.iter_mut() {
531 workspace.stop_server().await;
532 workspace.start_server().await?;
533 restarted.push(name.clone());
534 }
535 }
536 Ok(restarted)
537 }
538
539 #[trace]
540 pub async fn remove_workspace(&self, root: &Path) -> Result<Vec<String>, String> {
541 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
542 let mut workspaces = self.workspaces.write().await;
543
544 let mut stopped = Vec::new();
545 if let Some(mut servers) = workspaces.remove(&root) {
546 for (name, mut workspace) in servers.drain() {
547 workspace.stop_server().await;
548 stopped.push(name);
549 }
550 }
551 Ok(stopped)
552 }
553
554 #[trace]
555 pub async fn close_all(&self) {
556 let mut workspaces = self.workspaces.write().await;
557 for (_, mut servers) in workspaces.drain() {
558 for (_, mut workspace) in servers.drain() {
559 workspace.stop_server().await;
560 }
561 }
562 }
563}
564
565pub struct WorkspaceHandle<'a> {
566 session: &'a Session,
567 workspace_root: PathBuf,
568 server_name: String,
569}
570
571impl<'a> WorkspaceHandle<'a> {
572 #[trace]
573 pub async fn client(&self) -> Option<Arc<LspClient>> {
574 let workspaces = self.session.workspaces.read().await;
575 workspaces
576 .get(&self.workspace_root)
577 .and_then(|servers| servers.get(&self.server_name))
578 .and_then(|ws| ws.client())
579 }
580
581 pub fn server_name(&self) -> &str {
582 &self.server_name
583 }
584
585 pub async fn get_startup_stats(&self) -> Option<leta_types::ServerStartupStats> {
586 let workspaces = self.session.workspaces.read().await;
587 workspaces
588 .get(&self.workspace_root)
589 .and_then(|servers| servers.get(&self.server_name))
590 .and_then(|ws| ws.startup_stats())
591 }
592
593 #[trace]
594 pub async fn wait_for_ready(&self, timeout_secs: u64) -> bool {
595 if let Some(client) = self.client().await {
596 client.wait_for_indexing(timeout_secs).await
597 } else {
598 false
599 }
600 }
601
602 #[trace]
603 pub async fn ensure_document_open(&self, path: &Path) -> Result<(), String> {
604 self.cleanup_deleted_documents().await;
605
606 let uri = path_to_uri(path);
607
608 let (needs_open, needs_reopen, client) = {
610 let workspaces = self.session.workspaces.read().await;
611 let workspace = workspaces
612 .get(&self.workspace_root)
613 .and_then(|servers| servers.get(&self.server_name))
614 .ok_or_else(|| "Workspace not found".to_string())?;
615
616 let client = workspace.client();
617
618 if let Some(doc) = workspace.open_documents.get(&uri) {
619 let current_content = read_file_content(path).map_err(|e| e.to_string())?;
620 if current_content != doc.content {
621 (false, true, client) } else {
623 (false, false, client) }
625 } else {
626 (true, false, client) }
628 };
629
630 if !needs_open && !needs_reopen {
631 return Ok(());
632 }
633
634 if needs_reopen {
636 self.close_document(path).await;
637 }
638
639 let content = read_file_content(path).map_err(|e| e.to_string())?;
641 let language_id = get_language_id(path).to_string();
642
643 {
645 let mut workspaces = self.session.workspaces.write().await;
646 let workspace = workspaces
647 .get_mut(&self.workspace_root)
648 .and_then(|servers| servers.get_mut(&self.server_name))
649 .ok_or_else(|| "Workspace not found".to_string())?;
650
651 let doc = OpenDocument {
652 _uri: uri.clone(),
653 _version: 1,
654 content: content.clone(),
655 _language_id: language_id.clone(),
656 };
657 workspace.open_documents.insert(uri.clone(), doc);
658 }
659
660 if let Some(client) = client {
662 let params = serde_json::json!({
663 "textDocument": {
664 "uri": uri,
665 "languageId": language_id,
666 "version": 1,
667 "text": content,
668 }
669 });
670 let _ = client
671 .send_notification("textDocument/didOpen", params)
672 .await;
673
674 if client.server_name() == "ruby-lsp" {
676 let symbol_params = serde_json::json!({
677 "textDocument": {"uri": uri}
678 });
679 let _ = client
680 .send_request_raw("textDocument/documentSymbol", symbol_params)
681 .await;
682 }
683 }
684
685 Ok(())
686 }
687
688 #[trace]
689 pub async fn close_document(&self, path: &Path) {
690 tracing::trace!("WorkspaceHandle::close_document acquiring write lock");
691 let mut workspaces = self.session.workspaces.write().await;
692 if let Some(servers) = workspaces.get_mut(&self.workspace_root) {
693 if let Some(workspace) = servers.get_mut(&self.server_name) {
694 workspace.close_document(path).await;
695 }
696 }
697 tracing::trace!("WorkspaceHandle::close_document releasing write lock");
698 }
699
700 #[trace]
701 pub async fn is_document_open(&self, path: &Path) -> bool {
702 let uri = path_to_uri(path);
703 let workspaces = self.session.workspaces.read().await;
704 if let Some(servers) = workspaces.get(&self.workspace_root) {
705 if let Some(workspace) = servers.get(&self.server_name) {
706 return workspace.open_documents.contains_key(&uri);
707 }
708 }
709 false
710 }
711
712 async fn cleanup_deleted_documents(&self) {
713 let mut workspaces = self.session.workspaces.write().await;
714 if let Some(servers) = workspaces.get_mut(&self.workspace_root) {
715 if let Some(workspace) = servers.get_mut(&self.server_name) {
716 workspace.cleanup_deleted_documents().await;
717 }
718 }
719 }
720
721 #[trace]
722 pub async fn notify_files_changed(
723 &self,
724 changes: &[(PathBuf, leta_lsp::lsp_types::FileChangeType)],
725 ) {
726 let client = match self.client().await {
727 Some(c) => c,
728 None => return,
729 };
730
731 let file_events: Vec<leta_lsp::lsp_types::FileEvent> = changes
732 .iter()
733 .map(|(path, change_type)| leta_lsp::lsp_types::FileEvent {
734 uri: path_to_uri(path).parse().unwrap(),
735 typ: *change_type,
736 })
737 .collect();
738
739 let params = leta_lsp::lsp_types::DidChangeWatchedFilesParams {
740 changes: file_events,
741 };
742
743 let _ = client
744 .send_notification("workspace/didChangeWatchedFiles", params)
745 .await;
746 }
747}