1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3use std::sync::{Arc, OnceLock};
4
5use anyhow::{anyhow, Result};
6use tokio::sync::OnceCell;
7
8use leta_cache::LmdbCache;
9use leta_config::Config;
10use leta_daemon::handlers::{handle_add_workspace, handle_remove_workspace};
11use leta_daemon::handlers::{
12 handle_calls, handle_declaration, handle_files, handle_graph, handle_grep,
13 handle_implementations, handle_move_file, handle_references, handle_rename,
14 handle_resolve_symbol, handle_show, handle_subtypes, handle_supertypes, HandlerContext,
15};
16use leta_daemon::session::Session;
17use leta_output::*;
18use leta_types::*;
19
20struct State {
21 ctx: HandlerContext,
22 config: Config,
23}
24
25static STATE: OnceCell<State> = OnceCell::const_new();
26static ENSURED_WORKSPACES: OnceLock<std::sync::Mutex<HashSet<PathBuf>>> = OnceLock::new();
27
28fn ensured_workspaces() -> &'static std::sync::Mutex<HashSet<PathBuf>> {
29 ENSURED_WORKSPACES.get_or_init(|| std::sync::Mutex::new(HashSet::new()))
30}
31
32pub async fn ensure_workspace(path: &Path) -> Result<()> {
35 let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
36
37 {
38 let set = ensured_workspaces().lock().unwrap();
39 if set.contains(&canonical) {
40 return Ok(());
41 }
42 }
43
44 workspace_add(path).await?;
45
46 {
47 let mut set = ensured_workspaces().lock().unwrap();
48 set.insert(canonical);
49 }
50
51 Ok(())
52}
53
54async fn get_state() -> Result<&'static State> {
55 STATE
56 .get_or_try_init(|| async {
57 let mut config = Config::load().map_err(|e| anyhow!("{}", e))?;
58 config.cleanup_stale_workspace_roots();
59
60 let cache_dir = leta_config::get_cache_dir();
61 std::fs::create_dir_all(&cache_dir)?;
62
63 let hover_cache = LmdbCache::new(
64 &cache_dir.join("hover_cache.lmdb"),
65 config.daemon.hover_cache_size,
66 )
67 .map_err(|e| anyhow!("{}", e))?;
68 let symbol_cache = LmdbCache::new(
69 &cache_dir.join("symbol_cache.lmdb"),
70 config.daemon.symbol_cache_size,
71 )
72 .map_err(|e| anyhow!("{}", e))?;
73
74 let session = Arc::new(Session::new(config.clone()));
75 let ctx = HandlerContext::new(session, Arc::new(hover_cache), Arc::new(symbol_cache));
76
77 Ok(State { ctx, config })
78 })
79 .await
80}
81
82fn get_workspace_root(config: &Config, working_dir: &Path) -> Result<PathBuf> {
83 config
84 .get_best_workspace_root(working_dir, Some(working_dir))
85 .ok_or_else(|| {
86 anyhow!(
87 "No workspace found for {}\nRun: leta workspace add",
88 working_dir.display()
89 )
90 })
91}
92
93fn get_workspace_root_for_path(
94 config: &Config,
95 path: &Path,
96 working_dir: &Path,
97) -> Result<PathBuf> {
98 let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
99 config
100 .get_best_workspace_root(&path, Some(working_dir))
101 .ok_or_else(|| {
102 anyhow!(
103 "No workspace found for {}\nRun: leta workspace add",
104 path.display()
105 )
106 })
107}
108
109async fn resolve(symbol: &str, workspace_root: &Path) -> Result<ResolveSymbolResult> {
110 let state = get_state().await?;
111 let params = ResolveSymbolParams {
112 workspace_root: workspace_root.to_string_lossy().to_string(),
113 symbol_path: symbol.to_string(),
114 };
115 let result = handle_resolve_symbol(&state.ctx, params)
116 .await
117 .map_err(|e| anyhow!("{}", e))?;
118
119 if let Some(error) = &result.error {
120 let mut msg = error.clone();
121 if let Some(matches) = &result.matches {
122 for m in matches {
123 let container = m
124 .container
125 .as_ref()
126 .map(|c| format!(" in {}", c))
127 .unwrap_or_default();
128 let kind = format!("[{}] ", m.kind);
129 let detail = m
130 .detail
131 .as_ref()
132 .map(|d| format!(" ({})", d))
133 .unwrap_or_default();
134 let ref_str = m.reference.as_deref().unwrap_or("");
135 msg.push_str(&format!("\n {}", ref_str));
136 msg.push_str(&format!(
137 "\n {}:{} {}{}{}{}",
138 m.path, m.line, kind, m.name, detail, container
139 ));
140 }
141 if let Some(total) = result.total_matches {
142 let shown = matches.len() as u32;
143 if total > shown {
144 msg.push_str(&format!("\n ... and {} more", total - shown));
145 }
146 }
147 }
148 return Err(anyhow!("{}", msg));
149 }
150
151 Ok(result)
152}
153
154pub async fn show(working_dir: &Path, symbol: &str, context: u32, head: u32) -> Result<String> {
155 let state = get_state().await?;
156 let workspace_root = get_workspace_root(&state.config, working_dir)?;
157 let resolved = resolve(symbol, &workspace_root).await?;
158
159 let params = ShowParams {
160 workspace_root: workspace_root.to_string_lossy().to_string(),
161 path: resolved.path.unwrap_or_default(),
162 line: resolved.line.unwrap_or(0),
163 column: resolved.column.unwrap_or(0),
164 context,
165 head: if head > 0 { Some(head) } else { None },
166 symbol_name: Some(symbol.to_string()),
167 symbol_kind: resolved.kind,
168 range_start_line: resolved.range_start_line,
169 range_end_line: resolved.range_end_line,
170 direct_location: true,
171 };
172 let result = handle_show(&state.ctx, params)
173 .await
174 .map_err(|e| anyhow!("{}", e))?;
175
176 Ok(format_show_result(&result, head))
177}
178
179pub async fn refs(working_dir: &Path, symbol: &str, context: u32, head: u32) -> Result<String> {
180 let state = get_state().await?;
181 let workspace_root = get_workspace_root(&state.config, working_dir)?;
182 let resolved = resolve(symbol, &workspace_root).await?;
183
184 let params = ReferencesParams {
185 workspace_root: workspace_root.to_string_lossy().to_string(),
186 path: resolved.path.unwrap_or_default(),
187 line: resolved.line.unwrap_or(0),
188 column: resolved.column.unwrap_or(0),
189 context,
190 head,
191 };
192 let result = handle_references(&state.ctx, params)
193 .await
194 .map_err(|e| anyhow!("{}", e))?;
195
196 let command_base = format!("leta refs \"{}\"", symbol);
197 Ok(format_references_result(&result, head, &command_base))
198}
199
200pub struct GrepOptions {
201 pub path_pattern: Option<String>,
202 pub kinds: Option<Vec<String>>,
203 pub case_sensitive: bool,
204 pub exclude_patterns: Vec<String>,
205 pub head: u32,
206}
207
208pub async fn grep(working_dir: &Path, pattern: &str, options: GrepOptions) -> Result<String> {
209 let state = get_state().await?;
210 let workspace_root = get_workspace_root(&state.config, working_dir)?;
211
212 let head = options.head;
213 let params = GrepParams {
214 workspace_root: workspace_root.to_string_lossy().to_string(),
215 pattern: pattern.to_string(),
216 kinds: options.kinds,
217 case_sensitive: options.case_sensitive,
218 path_pattern: options.path_pattern,
219 exclude_patterns: options.exclude_patterns,
220 limit: head,
221 };
222 let result = handle_grep(&state.ctx, params)
223 .await
224 .map_err(|e| anyhow!("{}", e))?;
225
226 let command_base = format!("leta grep \"{}\"", pattern);
227 Ok(format_grep_result(&result, head, &command_base))
228}
229
230pub async fn files(
231 working_dir: &Path,
232 subpath: Option<&Path>,
233 exclude_patterns: Vec<String>,
234 include_patterns: Vec<String>,
235 filter_pattern: Option<&str>,
236 head: u32,
237) -> Result<String> {
238 let state = get_state().await?;
239 let (workspace_root, resolved_subpath) = if let Some(path) = subpath {
240 let target = path.canonicalize()?;
241 let wr = get_workspace_root_for_path(&state.config, &target, working_dir)?;
242 (wr, Some(target.to_string_lossy().to_string()))
243 } else {
244 (get_workspace_root(&state.config, working_dir)?, None)
245 };
246
247 let params = FilesParams {
248 workspace_root: workspace_root.to_string_lossy().to_string(),
249 subpath: resolved_subpath,
250 exclude_patterns,
251 include_patterns,
252 filter_pattern: filter_pattern.map(String::from),
253 head,
254 };
255 let result = handle_files(&state.ctx, params)
256 .await
257 .map_err(|e| anyhow!("{}", e))?;
258
259 let command_base = "leta files".to_string();
260 Ok(format_files_result(&result, head, &command_base))
261}
262
263pub async fn calls(
264 working_dir: &Path,
265 from: Option<&str>,
266 to: Option<&str>,
267 max_depth: u32,
268 include_non_workspace: bool,
269 head: u32,
270) -> Result<String> {
271 if from.is_none() && to.is_none() {
272 return Err(anyhow!("At least one of --from or --to must be specified"));
273 }
274
275 let state = get_state().await?;
276 let workspace_root = get_workspace_root(&state.config, working_dir)?;
277 let ws = workspace_root.to_string_lossy().to_string();
278
279 let mut params = CallsParams {
280 workspace_root: ws,
281 mode: CallsMode::Outgoing,
282 from_path: None,
283 from_line: None,
284 from_column: None,
285 from_symbol: None,
286 to_path: None,
287 to_line: None,
288 to_column: None,
289 to_symbol: None,
290 max_depth,
291 include_non_workspace,
292 head,
293 };
294
295 if let (Some(from_sym), Some(to_sym)) = (from, to) {
296 let from_r = resolve(from_sym, &workspace_root).await?;
297 let to_r = resolve(to_sym, &workspace_root).await?;
298 params.mode = CallsMode::Path;
299 params.from_path = from_r.path;
300 params.from_line = from_r.line;
301 params.from_column = from_r.column;
302 params.from_symbol = Some(from_sym.to_string());
303 params.to_path = to_r.path;
304 params.to_line = to_r.line;
305 params.to_column = to_r.column;
306 params.to_symbol = Some(to_sym.to_string());
307 } else if let Some(from_sym) = from {
308 let r = resolve(from_sym, &workspace_root).await?;
309 params.mode = CallsMode::Outgoing;
310 params.from_path = r.path;
311 params.from_line = r.line;
312 params.from_column = r.column;
313 params.from_symbol = Some(from_sym.to_string());
314 } else if let Some(to_sym) = to {
315 let r = resolve(to_sym, &workspace_root).await?;
316 params.mode = CallsMode::Incoming;
317 params.to_path = r.path;
318 params.to_line = r.line;
319 params.to_column = r.column;
320 params.to_symbol = Some(to_sym.to_string());
321 }
322
323 let result = handle_calls(&state.ctx, params)
324 .await
325 .map_err(|e| anyhow!("{}", e))?;
326
327 let command_base = "leta calls".to_string();
328 Ok(format_calls_result(&result, head, &command_base))
329}
330
331pub struct GraphOptions {
332 pub include_non_workspace: bool,
333 pub include_tests: bool,
334 pub exclude_patterns: Vec<String>,
335 pub include_patterns: Vec<String>,
336}
337
338pub async fn graph(working_dir: &Path, options: GraphOptions) -> Result<String> {
339 let state = get_state().await?;
340 let workspace_root = get_workspace_root(&state.config, working_dir)?;
341
342 let params = GraphParams {
343 workspace_root: workspace_root.to_string_lossy().to_string(),
344 include_non_workspace: options.include_non_workspace,
345 exclude_patterns: options.exclude_patterns,
346 include_patterns: options.include_patterns,
347 include_tests: options.include_tests,
348 };
349
350 let result = handle_graph(&state.ctx, params)
351 .await
352 .map_err(|e| anyhow!("{}", e))?;
353
354 if let Some(error) = &result.error {
355 return Err(anyhow!("{}", error));
356 }
357
358 Ok(format_graph_result(&result, false))
359}
360
361pub async fn declaration(
362 working_dir: &Path,
363 symbol: &str,
364 context: u32,
365 head: u32,
366) -> Result<String> {
367 let state = get_state().await?;
368 let workspace_root = get_workspace_root(&state.config, working_dir)?;
369 let resolved = resolve(symbol, &workspace_root).await?;
370
371 let params = DeclarationParams {
372 workspace_root: workspace_root.to_string_lossy().to_string(),
373 path: resolved.path.unwrap_or_default(),
374 line: resolved.line.unwrap_or(0),
375 column: resolved.column.unwrap_or(0),
376 context,
377 head,
378 };
379 let result = handle_declaration(&state.ctx, params)
380 .await
381 .map_err(|e| anyhow!("{}", e))?;
382
383 let command_base = format!("leta declaration \"{}\"", symbol);
384 Ok(format_declaration_result(&result, head, &command_base))
385}
386
387pub async fn implementations(
388 working_dir: &Path,
389 symbol: &str,
390 context: u32,
391 head: u32,
392) -> Result<String> {
393 let state = get_state().await?;
394 let workspace_root = get_workspace_root(&state.config, working_dir)?;
395 let resolved = resolve(symbol, &workspace_root).await?;
396
397 let params = ImplementationsParams {
398 workspace_root: workspace_root.to_string_lossy().to_string(),
399 path: resolved.path.unwrap_or_default(),
400 line: resolved.line.unwrap_or(0),
401 column: resolved.column.unwrap_or(0),
402 context,
403 head,
404 };
405 let result = handle_implementations(&state.ctx, params)
406 .await
407 .map_err(|e| anyhow!("{}", e))?;
408
409 let command_base = format!("leta implementations \"{}\"", symbol);
410 Ok(format_implementations_result(&result, head, &command_base))
411}
412
413pub async fn subtypes(working_dir: &Path, symbol: &str, context: u32, head: u32) -> Result<String> {
414 let state = get_state().await?;
415 let workspace_root = get_workspace_root(&state.config, working_dir)?;
416 let resolved = resolve(symbol, &workspace_root).await?;
417
418 let params = SubtypesParams {
419 workspace_root: workspace_root.to_string_lossy().to_string(),
420 path: resolved.path.unwrap_or_default(),
421 line: resolved.line.unwrap_or(0),
422 column: resolved.column.unwrap_or(0),
423 context,
424 head,
425 };
426 let result = handle_subtypes(&state.ctx, params)
427 .await
428 .map_err(|e| anyhow!("{}", e))?;
429
430 let command_base = format!("leta subtypes \"{}\"", symbol);
431 Ok(format_subtypes_result(&result, head, &command_base))
432}
433
434pub async fn supertypes(
435 working_dir: &Path,
436 symbol: &str,
437 context: u32,
438 head: u32,
439) -> Result<String> {
440 let state = get_state().await?;
441 let workspace_root = get_workspace_root(&state.config, working_dir)?;
442 let resolved = resolve(symbol, &workspace_root).await?;
443
444 let params = SupertypesParams {
445 workspace_root: workspace_root.to_string_lossy().to_string(),
446 path: resolved.path.unwrap_or_default(),
447 line: resolved.line.unwrap_or(0),
448 column: resolved.column.unwrap_or(0),
449 context,
450 head,
451 };
452 let result = handle_supertypes(&state.ctx, params)
453 .await
454 .map_err(|e| anyhow!("{}", e))?;
455
456 let command_base = format!("leta supertypes \"{}\"", symbol);
457 Ok(format_supertypes_result(&result, head, &command_base))
458}
459
460pub async fn rename(working_dir: &Path, symbol: &str, new_name: &str) -> Result<String> {
461 let state = get_state().await?;
462 let workspace_root = get_workspace_root(&state.config, working_dir)?;
463 let resolved = resolve(symbol, &workspace_root).await?;
464
465 let params = RenameParams {
466 workspace_root: workspace_root.to_string_lossy().to_string(),
467 path: resolved.path.unwrap_or_default(),
468 line: resolved.line.unwrap_or(0),
469 column: resolved.column.unwrap_or(0),
470 new_name: new_name.to_string(),
471 };
472 let result = handle_rename(&state.ctx, params)
473 .await
474 .map_err(|e| anyhow!("{}", e))?;
475
476 Ok(format_rename_result(&result))
477}
478
479pub async fn mv(working_dir: &Path, old_path: &str, new_path: &str) -> Result<String> {
480 let state = get_state().await?;
481 let old = PathBuf::from(old_path).canonicalize()?;
482 let new = working_dir.join(new_path);
483 let workspace_root = get_workspace_root_for_path(&state.config, &old, working_dir)?;
484
485 let params = MoveFileParams {
486 workspace_root: workspace_root.to_string_lossy().to_string(),
487 old_path: old.to_string_lossy().to_string(),
488 new_path: new.to_string_lossy().to_string(),
489 };
490 let result = handle_move_file(&state.ctx, params)
491 .await
492 .map_err(|e| anyhow!("{}", e))?;
493
494 Ok(format_move_file_result(&result))
495}
496
497pub async fn workspace_add(path: &Path) -> Result<String> {
498 let state = get_state().await?;
499 let workspace_root = path.canonicalize()?;
500
501 let params = AddWorkspaceParams {
502 workspace_root: workspace_root.to_string_lossy().to_string(),
503 };
504 let result = handle_add_workspace(&state.ctx, params)
505 .await
506 .map_err(|e| anyhow!("{}", e))?;
507
508 if result.added {
509 Ok(format!("Added workspace: {}", result.workspace_root))
510 } else {
511 Ok(format!(
512 "Workspace already added: {}",
513 result.workspace_root
514 ))
515 }
516}
517
518pub async fn workspace_remove(working_dir: &Path, path: Option<&Path>) -> Result<String> {
519 let state = get_state().await?;
520 let workspace_root = if let Some(p) = path {
521 p.canonicalize()?
522 } else {
523 get_workspace_root(&state.config, working_dir)?
524 };
525
526 let params = RemoveWorkspaceParams {
527 workspace_root: workspace_root.to_string_lossy().to_string(),
528 };
529 let _result = handle_remove_workspace(&state.ctx, params)
530 .await
531 .map_err(|e| anyhow!("{}", e))?;
532
533 Ok(format!("Removed workspace: {}", workspace_root.display()))
534}