Skip to main content

leta_lib/
lib.rs

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
32/// Fast workspace ensure: checks an in-memory HashSet first (zero-cost after
33/// first call per path), only calls handle_add_workspace on cache miss.
34pub 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}