Skip to main content

oak_resolver/
lib.rs

1#![warn(missing_docs)]
2//! Symbol and module resolution for the Oak language framework.
3//!
4//! This crate provides traits and implementations for resolving module imports
5//! and managing global symbol tables across a workspace.
6
7use dashmap::DashMap;
8use oak_symbols::SymbolInformation;
9use std::path::PathBuf;
10use url::Url;
11
12use std::sync::RwLock;
13
14/// Trait for resolving module imports to file URIs.
15pub trait ModuleResolver: Send + Sync {
16    /// Resolve an import path relative to a base URI.
17    fn resolve(&self, base_uri: &str, import_path: &str) -> Option<String>;
18}
19
20/// A standard resolver that handles relative and absolute file paths.
21pub struct StandardResolver {
22    /// The list of root directories to search for modules.
23    root_dirs: RwLock<Vec<PathBuf>>,
24}
25
26impl StandardResolver {
27    /// Creates a new standard resolver with the given root directories.
28    pub fn new(root_dirs: Vec<PathBuf>) -> Self {
29        Self { root_dirs: RwLock::new(root_dirs) }
30    }
31
32    /// Update the root directories used for resolution.
33    pub fn set_root_dirs(&self, root_dirs: Vec<PathBuf>) {
34        if let Ok(mut dirs) = self.root_dirs.write() {
35            *dirs = root_dirs;
36        }
37    }
38}
39
40impl ModuleResolver for StandardResolver {
41    fn resolve(&self, base_uri: &str, import_path: &str) -> Option<String> {
42        let root_dirs = self.root_dirs.read().ok()?;
43
44        if let Ok(base_url) = Url::parse(base_uri) {
45            if let Ok(base_path) = base_url.to_file_path() {
46                let base_dir = base_path.parent()?;
47                let resolved_path = base_dir.join(import_path);
48
49                if resolved_path.exists() {
50                    return Url::from_file_path(resolved_path).ok().map(|u| u.to_string());
51                }
52            }
53        }
54
55        // Try root directories (like PYTHONPATH or node_modules logic)
56        for root in root_dirs.iter() {
57            let resolved_path = root.join(import_path);
58            if resolved_path.exists() {
59                return Url::from_file_path(resolved_path).ok().map(|u| u.to_string());
60            }
61        }
62
63        None
64    }
65}
66
67/// Global symbol table that stores symbols across the entire workspace.
68pub struct GlobalSymbolTable {
69    /// Map of URI to symbols defined in that file.
70    file_symbols: DashMap<String, Vec<SymbolInformation>>,
71    /// Map of fully qualified name to symbol information.
72    qualified_symbols: DashMap<String, SymbolInformation>,
73}
74
75impl GlobalSymbolTable {
76    /// Creates a new, empty `GlobalSymbolTable`.
77    pub fn new() -> Self {
78        Self { file_symbols: DashMap::new(), qualified_symbols: DashMap::new() }
79    }
80
81    /// Add or update symbols for a file.
82    pub fn update_file_symbols(&self, uri: String, symbols: Vec<SymbolInformation>) {
83        // Remove old qualified symbols for this file
84        if let Some((_, old_symbols)) = self.file_symbols.remove(&uri) {
85            for sym in old_symbols {
86                let fqn = self.make_qualified_name(&sym);
87                self.qualified_symbols.remove(&fqn);
88            }
89        }
90
91        // Add new symbols
92        for sym in &symbols {
93            let fqn = self.make_qualified_name(sym);
94            self.qualified_symbols.insert(fqn, sym.clone());
95        }
96        self.file_symbols.insert(uri, symbols);
97    }
98
99    fn make_qualified_name(&self, sym: &SymbolInformation) -> String {
100        match &sym.container_name {
101            Some(container) => format!("{}::{}", container, sym.name),
102            None => sym.name.clone(),
103        }
104    }
105
106    /// Lookup a symbol by its fully qualified name.
107    pub fn lookup(&self, fqn: &str) -> Option<SymbolInformation> {
108        self.qualified_symbols.get(fqn).map(|r| r.value().clone())
109    }
110
111    /// Get all symbols defined in a specific file.
112    pub fn query_file(&self, uri: &str) -> Vec<SymbolInformation> {
113        self.file_symbols.get(uri).map(|r| r.value().clone()).unwrap_or_default()
114    }
115
116    /// Find all symbols matching a query (for workspace/symbol).
117    pub fn query(&self, query: &str) -> Vec<SymbolInformation> {
118        self.qualified_symbols.iter().filter(|r| r.key().contains(query)).map(|r| r.value().clone()).collect()
119    }
120}
121
122impl Default for GlobalSymbolTable {
123    fn default() -> Self {
124        Self::new()
125    }
126}