logicaffeine_lsp/
workspace.rs1use std::path::Path;
10
11use dashmap::DashMap;
12use tower_lsp::lsp_types::{Location, Range, SymbolKind, Url};
13
14use crate::index::DefinitionKind;
15use crate::line_index::LineIndex;
16use crate::pipeline;
17
18const MAX_FILES: usize = 2_000;
20const MAX_FILE_BYTES: u64 = 512 * 1024;
21const SKIP_DIRS: &[&str] = &["target", "node_modules", "dist", "out", "logs"];
22
23pub struct WorkspaceSymbol {
24 pub name: String,
25 pub kind: SymbolKind,
26 pub container: Option<String>,
27 pub location: Location,
28}
29
30pub struct FileReference {
32 pub name: String,
33 pub range: Range,
34 pub resolved_locally: bool,
37}
38
39#[derive(Default)]
40pub struct WorkspaceIndex {
41 files: DashMap<Url, Vec<WorkspaceSymbol>>,
42 refs: DashMap<Url, Vec<FileReference>>,
43}
44
45impl WorkspaceIndex {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn scan_folder(&self, root: &Path) {
53 let mut pending = vec![root.to_path_buf()];
54 let mut seen_files = 0usize;
55 while let Some(dir) = pending.pop() {
56 let Ok(entries) = std::fs::read_dir(&dir) else { continue };
57 for entry in entries.flatten() {
58 let path = entry.path();
59 let name = entry.file_name();
60 let name = name.to_string_lossy();
61 if path.is_dir() {
62 if !name.starts_with('.') && !SKIP_DIRS.contains(&name.as_ref()) {
63 pending.push(path);
64 }
65 continue;
66 }
67 let is_logos = matches!(
68 path.extension().and_then(|e| e.to_str()),
69 Some("lg") | Some("md")
70 );
71 if !is_logos {
72 continue;
73 }
74 if entry.metadata().map(|m| m.len() > MAX_FILE_BYTES).unwrap_or(true) {
75 continue;
76 }
77 if seen_files >= MAX_FILES {
78 log::warn!(
79 "workspace scan hit the {MAX_FILES}-file bound at {}; remaining files are unindexed",
80 path.display()
81 );
82 return;
83 }
84 seen_files += 1;
85 self.index_file(&path);
86 }
87 }
88 }
89
90 pub fn index_file(&self, path: &Path) {
92 let Ok(uri) = Url::from_file_path(path) else { return };
93 let Ok(source) = std::fs::read_to_string(path) else {
94 self.files.remove(&uri);
95 return;
96 };
97 let analysis = pipeline::analyze(&source);
98 let line_index = LineIndex::new(&source);
99
100 let symbols = analysis
101 .symbol_index
102 .definitions
103 .iter()
104 .filter(|def| def.span != logicaffeine_language::token::Span::default())
105 .filter_map(|def| {
106 let kind = match def.kind {
108 DefinitionKind::Function => SymbolKind::FUNCTION,
109 DefinitionKind::Struct => SymbolKind::STRUCT,
110 DefinitionKind::Enum => SymbolKind::ENUM,
111 DefinitionKind::Field => SymbolKind::FIELD,
112 DefinitionKind::Variant => SymbolKind::ENUM_MEMBER,
113 DefinitionKind::Theorem => SymbolKind::CLASS,
114 DefinitionKind::Block => SymbolKind::NAMESPACE,
115 DefinitionKind::Variable | DefinitionKind::Parameter => return None,
116 };
117 Some(WorkspaceSymbol {
118 name: def.name.clone(),
119 kind,
120 container: def.detail.clone(),
121 location: Location {
122 uri: uri.clone(),
123 range: Range {
124 start: line_index.position(def.span.start),
125 end: line_index.position(def.span.end),
126 },
127 },
128 })
129 })
130 .collect();
131
132 let references = analysis
133 .symbol_index
134 .references
135 .iter()
136 .map(|reference| FileReference {
137 name: reference.name.clone(),
138 range: Range {
139 start: line_index.position(reference.span.start),
140 end: line_index.position(reference.span.end),
141 },
142 resolved_locally: reference.definition_idx.is_some(),
143 })
144 .collect();
145
146 self.files.insert(uri.clone(), symbols);
147 self.refs.insert(uri, references);
148 }
149
150 pub fn remove(&self, uri: &Url) {
151 self.files.remove(uri);
152 self.refs.remove(uri);
153 }
154
155 fn defines(&self, uri: &Url, name: &str) -> bool {
157 self.files
158 .get(uri)
159 .map(|symbols| symbols.iter().any(|s| s.name == name))
160 .unwrap_or(false)
161 }
162
163 pub fn references_of(&self, name: &str, skip: &[&Url]) -> Vec<Location> {
168 let mut locations = Vec::new();
169 for entry in self.refs.iter() {
170 let uri = entry.key();
171 if skip.contains(&uri) {
172 continue;
173 }
174 let file_defines = self.defines(uri, name);
175 for reference in entry.value() {
176 if reference.name == name && (!reference.resolved_locally || file_defines) {
177 locations.push(Location { uri: uri.clone(), range: reference.range });
178 }
179 }
180 }
181 locations
182 }
183
184 pub fn query(&self, needle: &str, limit: usize) -> Vec<WorkspaceSymbol> {
186 let needle = needle.to_lowercase();
187 let mut hits = Vec::new();
188 for entry in self.files.iter() {
189 for symbol in entry.value() {
190 if needle.is_empty() || symbol.name.to_lowercase().contains(&needle) {
191 hits.push(WorkspaceSymbol {
192 name: symbol.name.clone(),
193 kind: symbol.kind,
194 container: symbol.container.clone(),
195 location: symbol.location.clone(),
196 });
197 if hits.len() >= limit {
198 return hits;
199 }
200 }
201 }
202 }
203 hits
204 }
205
206 pub fn definition_of(&self, name: &str) -> Option<Location> {
210 let mut fallback = None;
211 for entry in self.files.iter() {
212 for symbol in entry.value() {
213 if symbol.name != name {
214 continue;
215 }
216 match symbol.kind {
217 SymbolKind::FUNCTION | SymbolKind::STRUCT | SymbolKind::ENUM => {
218 return Some(symbol.location.clone());
219 }
220 _ => fallback = Some(symbol.location.clone()),
221 }
222 }
223 }
224 fallback
225 }
226}