Skip to main content

oak_lsp/
service.rs

1use crate::types::{CodeAction, CompletionItem, Diagnostic, DocumentHighlight, FoldingRange, Hover, InitializeParams, InlayHint, LocationRange, SemanticTokens, SignatureHelp, StructureItem, TextEdit, WorkspaceEdit, WorkspaceSymbol};
2use core::range::Range;
3use oak_core::{
4    language::{ElementRole, ElementType, Language},
5    source::Source,
6    tree::RedNode,
7};
8use oak_resolver::ModuleResolver;
9use oak_vfs::{Vfs, WritableVfs};
10use std::future::Future;
11
12/// A trait that defines the capabilities and behavior of a language-specific service.
13///
14/// This trait is the primary interface for implementing Language Server Protocol (LSP)
15/// features. It provides hooks for various IDE features like hover, completion,
16/// diagnostics, and symbol navigation.
17///
18/// # Implementation
19///
20/// Implementors should provide language-specific logic for parsing, resolving symbols,
21/// and generating IDE-specific data structures.
22pub trait LanguageService: Send + Sync {
23    /// The language type this service supports.
24    type Lang: Language;
25    /// The VFS type used for source management.
26    type Vfs: WritableVfs;
27
28    /// Returns a reference to the underlying Virtual File System.
29    fn vfs(&self) -> &Self::Vfs;
30
31    /// Returns a reference to the workspace manager.
32    fn workspace(&self) -> &crate::workspace::WorkspaceManager;
33
34    /// Retrieves the source content for a given URI from the VFS.
35    fn get_source(&self, uri: &str) -> Option<<Self::Vfs as Vfs>::Source> {
36        self.vfs().get_source(uri)
37    }
38
39    /// Retrieves the root red node of a file for the given URI.
40    ///
41    /// This method is responsible for parsing the source and providing a position-aware
42    /// syntax tree. Implementations should typically use a cache to avoid re-parsing
43    /// unchanged files.
44    fn get_root(&self, _uri: &str) -> impl Future<Output = Option<RedNode<'_, Self::Lang>>> + Send + '_ {
45        async { None }
46    }
47
48    /// Executes a closure with the root red node of a file.
49    ///
50    /// This is a convenience helper for running logic that requires the syntax tree.
51    fn with_root<'a, R, F>(&'a self, uri: &'a str, f: F) -> impl Future<Output = Option<R>> + Send + 'a
52    where
53        R: Send,
54        F: FnOnce(RedNode<'a, Self::Lang>) -> R + Send + 'a,
55    {
56        async move {
57            let root = self.get_root(uri).await?;
58            Some(f(root))
59        }
60    }
61
62    /// Executes a closure with multiple root nodes in parallel.
63    ///
64    /// Useful for cross-file operations like workspace symbol search or global rename.
65    fn with_roots<'a, R, F>(&'a self, uris: Vec<String>, f: F) -> impl Future<Output = Vec<R>> + Send + 'a
66    where
67        R: Send + 'static,
68        F: Fn(RedNode<'a, Self::Lang>) -> R + Send + Sync + 'a,
69    {
70        let mut futures = Vec::new();
71        let f = std::sync::Arc::new(f);
72
73        for uri in uris {
74            let f = f.clone();
75            futures.push(async move { if let Some(root) = self.get_root(&uri).await { Some(f(root)) } else { None } })
76        }
77
78        async move { futures::future::join_all(futures).await.into_iter().flatten().collect() }
79    }
80
81    /// Provides hover information for a specific range in a file.
82    ///
83    /// # Arguments
84    /// * `uri` - The URI of the file.
85    /// * `range` - The byte range to provide information for.
86    fn hover(&self, _uri: &str, _range: Range<usize>) -> impl Future<Output = Option<Hover>> + Send + '_ {
87        async { None }
88    }
89
90    /// Provides folding ranges for a file.
91    fn folding_ranges(&self, _uri: &str) -> impl Future<Output = Vec<FoldingRange>> + Send + '_ {
92        async { vec![] }
93    }
94
95    /// Provides document symbols (structure) for a file.
96    ///
97    /// This method extracts structural elements like classes, functions, and variables
98    /// from the source file. It first tries to query the workspace symbol index,
99    /// falling back to parsing the file if necessary.
100    fn document_symbols<'a>(&'a self, uri: &'a str) -> impl Future<Output = Vec<StructureItem>> + Send + 'a {
101        let uri = uri.to_string();
102        async move {
103            let _source = match self.get_source(&uri) {
104                Some(s) => s,
105                None => return vec![],
106            };
107            let _root = match self.get_root(&uri).await {
108                Some(r) => r,
109                None => return vec![],
110            };
111            let symbols = self.workspace().symbols.query_file(&uri);
112            if !symbols.is_empty() {
113                return symbols.into_iter().map(StructureItem::from).collect();
114            }
115            vec![]
116        }
117    }
118
119    /// Provides workspace-wide symbol search based on a query string.
120    ///
121    /// This method searches across all files in the workspace for symbols
122    /// that match the given query string.
123    fn workspace_symbols<'a>(&'a self, query: String) -> impl Future<Output = Vec<WorkspaceSymbol>> + Send + 'a {
124        async move { self.workspace().symbols.query(&query).into_iter().map(|s| WorkspaceSymbol::from(s)).collect() }
125    }
126
127    /// Recursively lists all files in the VFS starting from the given root URI.
128    ///
129    /// This is used to discover all relevant source files in a workspace or directory.
130    fn list_all_files(&self, root_uri: &str) -> impl Future<Output = Vec<String>> + Send + '_ {
131        let root_uri: oak_core::Arc<str> = root_uri.into();
132        async move {
133            let mut files = Vec::new();
134            let mut stack = vec![root_uri];
135
136            while let Some(uri) = stack.pop() {
137                if self.vfs().is_file(&uri) {
138                    files.push(uri.to_string());
139                }
140                else if self.vfs().is_dir(&uri) {
141                    if let Some(entries) = self.vfs().read_dir(&uri) {
142                        stack.extend(entries);
143                    }
144                }
145            }
146            files
147        }
148    }
149
150    /// Finds the definition(s) of a symbol at the specified range.
151    ///
152    /// This method attempts to resolve the symbol under the cursor to its
153    /// original definition. It handles:
154    /// 1. Local symbol resolution (language-specific).
155    /// 2. Global symbol lookup via the workspace index.
156    /// 3. Module/file import resolution.
157    fn definition<'a>(&'a self, uri: &'a str, range: Range<usize>) -> impl Future<Output = Vec<LocationRange>> + Send + 'a {
158        let uri = uri.to_string();
159        async move {
160            let root = match self.get_root(&uri).await {
161                Some(r) => r,
162                None => return vec![],
163            };
164            let source = match self.get_source(&uri) {
165                Some(s) => s,
166                None => return vec![],
167            };
168
169            // 1. Identify token at range
170            use oak_core::tree::RedTree;
171            let node = match root.child_at_offset(range.start) {
172                Some(RedTree::Node(n)) => n,
173                Some(RedTree::Leaf(l)) => return vec![LocationRange { uri: uri.clone().into(), range: l.span }],
174                None => root,
175            };
176
177            // 2. If it's a reference, try to resolve it
178            let role = node.green.kind.role();
179            if role.universal() == oak_core::language::UniversalElementRole::Reference {
180                let name = &source.get_text_in(node.span());
181
182                // Try local symbols first (not implemented here, should be done by lang-specific logic)
183
184                // Try global symbols
185                if let Some(sym) = self.workspace().symbols.lookup(name) {
186                    return vec![LocationRange { uri: sym.uri, range: sym.range }];
187                }
188
189                // Try as a module import
190                if let Some(resolved_uri) = self.workspace().resolver.resolve(&uri, name) {
191                    return vec![LocationRange { uri: resolved_uri.into(), range: (0..0).into() }];
192                }
193
194                // Try local symbols (TODO)
195            }
196
197            vec![]
198        }
199    }
200
201    /// Provides document highlights for a symbol at the specified range.
202    fn document_highlight<'a>(&'a self, _uri: &'a str, _range: Range<usize>) -> impl Future<Output = Vec<DocumentHighlight>> + Send + 'a {
203        async { vec![] }
204    }
205
206    /// Provides code actions for a specific range in a file.
207    fn code_action<'a>(&'a self, _uri: &'a str, _range: Range<usize>) -> impl Future<Output = Vec<CodeAction>> + Send + 'a {
208        async { vec![] }
209    }
210
211    /// Provides formatting edits for a file.
212    fn formatting<'a>(&'a self, _uri: &'a str) -> impl Future<Output = Vec<TextEdit>> + Send + 'a {
213        async { vec![] }
214    }
215
216    /// Provides range formatting edits for a file.
217    fn range_formatting<'a>(&'a self, _uri: &'a str, _range: Range<usize>) -> impl Future<Output = Vec<TextEdit>> + Send + 'a {
218        async { vec![] }
219    }
220
221    /// Provides rename edits for a symbol at the specified range.
222    fn rename<'a>(&'a self, _uri: &'a str, _range: Range<usize>, _new_name: String) -> impl Future<Output = Option<WorkspaceEdit>> + Send + 'a {
223        async { None }
224    }
225
226    /// Provides semantic tokens for a file.
227    fn semantic_tokens<'a>(&'a self, _uri: &'a str) -> impl Future<Output = Option<SemanticTokens>> + Send + 'a {
228        async { None }
229    }
230
231    /// Provides completion items for a file at the specified position.
232    ///
233    /// Implementations should use the syntax tree and symbol index to suggest
234    /// relevant keywords, identifiers, and snippets based on the cursor context.
235    fn completion<'a>(&'a self, _uri: &'a str, _offset: usize) -> impl Future<Output = Vec<CompletionItem>> + Send + 'a {
236        async { vec![] }
237    }
238
239    /// Provides signature help for a symbol at the specified range.
240    ///
241    /// Typically used for function or method calls to show parameter information
242    /// and documentation as the user types.
243    fn signature_help<'a>(&'a self, _uri: &'a str, _range: Range<usize>) -> impl Future<Output = Option<SignatureHelp>> + Send + 'a {
244        async { None }
245    }
246
247    /// Provides inlay hints for a file.
248    ///
249    /// Inlay hints are small pieces of information shown inline with the code,
250    /// such as parameter names or inferred types.
251    fn inlay_hint<'a>(&'a self, _uri: &'a str, _range: Range<usize>) -> impl Future<Output = Vec<InlayHint>> + Send + 'a {
252        async { vec![] }
253    }
254
255    /// Finds all references to a symbol at the specified range.
256    fn references<'a>(&'a self, _uri: &'a str, _range: Range<usize>) -> impl Future<Output = Vec<LocationRange>> + Send + 'a {
257        async { vec![] }
258    }
259
260    /// Finds the type definition of a symbol at the specified range.
261    fn type_definition<'a>(&'a self, _uri: &'a str, _range: Range<usize>) -> impl Future<Output = Vec<LocationRange>> + Send + 'a {
262        async { vec![] }
263    }
264
265    /// Finds the implementation(s) of a symbol at the specified range.
266    fn implementation<'a>(&'a self, _uri: &'a str, _range: Range<usize>) -> impl Future<Output = Vec<LocationRange>> + Send + 'a {
267        async { vec![] }
268    }
269
270    /// Handles an LSP initialize request.
271    fn initialize<'a>(&'a self, _params: InitializeParams) -> impl Future<Output = ()> + Send + 'a {
272        async {}
273    }
274
275    /// Called when the language server is fully initialized.
276    fn initialized<'a>(&'a self) -> impl Future<Output = ()> + Send + 'a {
277        async {}
278    }
279
280    /// Called when the language server is shut down.
281    fn shutdown<'a>(&'a self) -> impl Future<Output = ()> + Send + 'a {
282        async {}
283    }
284
285    /// Called when a file is saved in the editor.
286    fn did_save<'a>(&'a self, _uri: &'a str) -> impl Future<Output = ()> + Send + 'a {
287        async {}
288    }
289
290    /// Called when a file is closed in the editor.
291    fn did_close<'a>(&'a self, _uri: &'a str) -> impl Future<Output = ()> + Send + 'a {
292        async {}
293    }
294
295    /// Provides diagnostics for a file.
296    fn diagnostics<'a>(&'a self, _uri: &'a str) -> impl Future<Output = Vec<Diagnostic>> + Send + 'a {
297        async { vec![] }
298    }
299}