Skip to main content

oak_json/lsp/
mod.rs

1#![doc = include_str!("readme.md")]
2#[cfg(feature = "oak-highlight")]
3pub mod highlighter;
4
5use crate::{JsonLanguage, lexer::token_type::JsonTokenType};
6use core::range::Range;
7use dashmap::DashMap;
8use oak_core::{ParseCache, TokenType, parser::session::ParseSession, source::Source, tree::RedNode};
9#[cfg(feature = "lsp")]
10use {
11    futures::Future,
12    oak_hover::{Hover, HoverProvider},
13    oak_lsp::service::LanguageService,
14    oak_vfs::Vfs,
15};
16
17use crate::parser::element_type::JsonElementType;
18
19/// Hover provider implementation for JSON.
20#[cfg(feature = "lsp")]
21pub struct JsonHoverProvider;
22#[cfg(feature = "lsp")]
23impl HoverProvider<JsonLanguage> for JsonHoverProvider {
24    fn hover(&self, node: &RedNode<JsonLanguage>, _range: Range<usize>) -> Option<Hover> {
25        let kind = node.green.kind;
26        // Provide context-aware hover information
27        let contents = match kind {
28            JsonElementType::Object => "### JSON Object\nA collection of key-value pairs.",
29            JsonElementType::Array => "### JSON Array\nAn ordered list of values.",
30            JsonElementType::ObjectEntry => "### JSON Property\nA key-value pair in an object.",
31            JsonElementType::StringLiteral => "### JSON String\nA sequence of Unicode characters.",
32            JsonElementType::NumberLiteral => "### JSON Number\nA numeric value.",
33            JsonElementType::BooleanLiteral => "### JSON Boolean\nA true or false value.",
34            JsonElementType::NullLiteral => "### JSON Null\nRepresents the intentional absence of any value.",
35            _ => return None,
36        };
37        Some(Hover { contents: contents.to_string(), range: Some(node.span()) })
38    }
39}
40/// Language service implementation for JSON.
41#[cfg(feature = "lsp")]
42pub struct JsonLanguageService<V: Vfs> {
43    vfs: V,
44    workspace: oak_lsp::workspace::WorkspaceManager,
45    hover_provider: JsonHoverProvider,
46    sessions: DashMap<String, Box<ParseSession<JsonLanguage>>>,
47}
48impl<V: Vfs> JsonLanguageService<V> {
49    /// Creates a new `JsonLanguageService`.
50    pub fn new(vfs: V) -> Self {
51        Self { vfs, workspace: oak_lsp::workspace::WorkspaceManager::default(), hover_provider: JsonHoverProvider, sessions: DashMap::new() }
52    }
53    fn collect_definitions(&self, node: &RedNode<JsonLanguage>, name: &str, source: &V::Source, uri: &str, definitions: &mut Vec<oak_lsp::LocationRange>) {
54        use oak_core::{
55            language::{ElementType, UniversalElementRole, UniversalTokenRole},
56            tree::RedTree,
57        };
58        // In JSON, every ObjectEntry can be considered a definition if its key matches
59        if ElementType::is_universal(&node.green.kind, UniversalElementRole::Statement) && node.green.kind == JsonElementType::ObjectEntry {
60            for child in node.children() {
61                if let RedTree::Leaf(leaf) = child {
62                    // Keys are Name (if BareKey) or Literal (if StringLiteral)
63                    // But for navigation, we treat the key as the identifier
64                    if TokenType::is_universal(&leaf.kind, UniversalTokenRole::Name) || leaf.kind == JsonTokenType::StringLiteral {
65                        let text = source.get_text_in(leaf.span.clone());
66                        // Strip quotes for string literals
67                        let key_name = if leaf.kind == JsonTokenType::StringLiteral { text.trim_matches('"') } else { &text };
68                        if key_name == name {
69                            definitions.push(oak_lsp::LocationRange { uri: uri.to_string().into(), range: leaf.span });
70                            return;
71                        }
72                    }
73                }
74            }
75        }
76        for child in node.children() {
77            if let RedTree::Node(child_node) = child {
78                self.collect_definitions(&child_node, name, source, uri, definitions);
79            }
80        }
81    }
82}
83impl<V: Vfs + Send + Sync + 'static + oak_vfs::WritableVfs> LanguageService for JsonLanguageService<V> {
84    type Lang = JsonLanguage;
85    type Vfs = V;
86    fn vfs(&self) -> &Self::Vfs {
87        &self.vfs
88    }
89    fn workspace(&self) -> &oak_lsp::workspace::WorkspaceManager {
90        &self.workspace
91    }
92    fn get_root(&self, uri: &str) -> impl Future<Output = Option<RedNode<'_, JsonLanguage>>> + Send + '_ {
93        let uri = uri.to_string();
94        async move {
95            let source = self.vfs().get_source(&uri)?;
96            let mut session_entry = self.sessions.entry(uri.clone()).or_insert_with(|| Box::new(ParseSession::<JsonLanguage>::default()));
97            let session = session_entry.as_mut();
98            let language = JsonLanguage::default();
99            let parser = crate::parser::JsonParser::new(&language);
100            let lexer = crate::lexer::JsonLexer::new(&language);
101            let tree = {
102                let output = oak_core::parser::parse(&parser, &lexer, &source, &[], session);
103                let tree_ref = output.result.as_ref().ok()?;
104                // Safety: The tree is allocated in session's arena.
105                // We cast away local lifetimes to return a reference that can be returned from this block.
106                // The tree actually lives as long as the session in self.sessions.
107                unsafe { &*(*tree_ref as *const oak_core::GreenNode<JsonLanguage> as *const oak_core::GreenNode<'static, JsonLanguage>) }
108            };
109            session.commit_generation(tree);
110            Some(RedNode::new(tree, 0))
111        }
112    }
113    fn definition<'a>(&'a self, uri: &'a str, range: Range<usize>) -> impl Future<Output = Vec<oak_lsp::LocationRange>> + Send + 'a {
114        let uri = uri.to_string();
115        async move {
116            let Some(root) = self.get_root(&uri).await
117            else {
118                return vec![];
119            };
120            let Some(source) = self.vfs().get_source(&uri)
121            else {
122                return vec![];
123            };
124            let Some(leaf) = root.leaf_at_offset(range.start)
125            else {
126                return vec![];
127            };
128            let text = source.get_text_in(leaf.span.clone());
129            let name = text.trim_matches('"');
130            // Search for definitions in all files in the workspace
131            let mut all_definitions = Vec::new();
132            let files = self.list_all_files(&uri).await;
133            for file_uri in files {
134                if let Some(file_root) = self.get_root(&file_uri).await {
135                    if let Some(file_source) = self.vfs().get_source(&file_uri) {
136                        self.collect_definitions(&file_root, name, &file_source, &file_uri, &mut all_definitions);
137                    }
138                }
139            }
140            all_definitions
141        }
142    }
143    fn references<'a>(&'a self, uri: &'a str, range: Range<usize>) -> impl Future<Output = Vec<oak_lsp::LocationRange>> + Send + 'a {
144        let uri = uri.to_string();
145        async move {
146            let Some(root) = self.get_root(&uri).await
147            else {
148                return vec![];
149            };
150            let Some(source) = self.vfs().get_source(&uri)
151            else {
152                return vec![];
153            };
154            let Some(leaf) = root.leaf_at_offset(range.start)
155            else {
156                return vec![];
157            };
158            let text = source.get_text_in(leaf.span.clone());
159            let name = text.trim_matches('"');
160            // Search for references in all files in the workspace
161            let mut all_refs = Vec::new();
162            let files = self.list_all_files(&uri).await;
163            for file_uri in files {
164                if let Some(file_root) = self.get_root(&file_uri).await {
165                    if let Some(file_source) = self.vfs().get_source(&file_uri) {
166                        // In JSON, we use collect_definitions as a proxy for finding key references
167                        self.collect_definitions(&file_root, name, &file_source, &file_uri, &mut all_refs);
168                    }
169                }
170            }
171            all_refs
172        }
173    }
174    fn rename<'a>(&'a self, uri: &'a str, range: Range<usize>, new_name: String) -> impl Future<Output = Option<oak_lsp::WorkspaceEdit>> + Send + 'a {
175        let uri = uri.to_string();
176        async move {
177            let refs = self.references(&uri, range).await;
178            if refs.is_empty() {
179                return None;
180            }
181            let mut changes = std::collections::HashMap::new();
182            for r in refs {
183                // Ensure new name is quoted if the original was quoted
184                let Some(source) = self.vfs().get_source(&r.uri)
185                else {
186                    continue;
187                };
188                let old_text = source.get_text_in(r.range.clone());
189                let formatted_new_name = if old_text.starts_with('"') { format!("\"{}\"", new_name) } else { new_name.clone() };
190                changes.entry(r.uri.to_string()).or_insert_with(Vec::new).push(oak_lsp::TextEdit { range: r.range, new_text: formatted_new_name });
191            }
192            Some(oak_lsp::WorkspaceEdit { changes })
193        }
194    }
195    fn hover(&self, uri: &str, range: Range<usize>) -> impl Future<Output = Option<oak_lsp::Hover>> + Send + '_ {
196        let uri = uri.to_string();
197        async move { self.with_root(&uri, |root| self.hover_provider.hover(&root, range).map(|h| oak_lsp::Hover { contents: h.contents, range: h.range })).await.flatten() }
198    }
199}