1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | use super::DocumentCache; |
5 | use crate::util::{lookup_current_element_type, map_node_and_url, with_lookup_ctx}; |
6 | |
7 | use i_slint_compiler::diagnostics::Spanned; |
8 | use i_slint_compiler::expression_tree::Expression; |
9 | use i_slint_compiler::langtype::{ElementType, Type}; |
10 | use i_slint_compiler::lookup::{LookupObject, LookupResult}; |
11 | use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxNode, SyntaxToken}; |
12 | use i_slint_compiler::pathutils::clean_path; |
13 | |
14 | use lsp_types::{GotoDefinitionResponse, LocationLink, Range}; |
15 | use std::path::Path; |
16 | |
17 | pub fn goto_definition( |
18 | document_cache: &mut DocumentCache, |
19 | token: SyntaxToken, |
20 | ) -> Option<GotoDefinitionResponse> { |
21 | let mut node = token.parent(); |
22 | loop { |
23 | if let Some(n) = syntax_nodes::QualifiedName::new(node.clone()) { |
24 | let parent = n.parent()?; |
25 | return match parent.kind() { |
26 | SyntaxKind::Type => { |
27 | let qual = i_slint_compiler::object_tree::QualifiedTypeName::from_node(n); |
28 | let doc = document_cache.documents.get_document(node.source_file.path())?; |
29 | match doc.local_registry.lookup_qualified(&qual.members) { |
30 | Type::Struct { node: Some(node), .. } => goto_node(node.parent().as_ref()?), |
31 | Type::Enumeration(e) => goto_node(e.node.as_ref()?), |
32 | _ => None, |
33 | } |
34 | } |
35 | SyntaxKind::Element => { |
36 | let qual = i_slint_compiler::object_tree::QualifiedTypeName::from_node(n); |
37 | let doc = document_cache.documents.get_document(node.source_file.path())?; |
38 | match doc.local_registry.lookup_element(&qual.to_string()) { |
39 | Ok(ElementType::Component(c)) => { |
40 | goto_node(&c.root_element.borrow().debug.first()?.0) |
41 | } |
42 | _ => None, |
43 | } |
44 | } |
45 | SyntaxKind::Expression => { |
46 | if token.kind() != SyntaxKind::Identifier { |
47 | return None; |
48 | } |
49 | let lr = with_lookup_ctx(&document_cache.documents, node, |ctx| { |
50 | let mut it = n |
51 | .children_with_tokens() |
52 | .filter_map(|t| t.into_token()) |
53 | .filter(|t| t.kind() == SyntaxKind::Identifier); |
54 | let mut cur_tok = it.next()?; |
55 | let first_str = |
56 | i_slint_compiler::parser::normalize_identifier(cur_tok.text()); |
57 | let global = i_slint_compiler::lookup::global_lookup(); |
58 | let mut expr_it = global.lookup(ctx, &first_str)?; |
59 | while cur_tok.token != token.token { |
60 | cur_tok = it.next()?; |
61 | let str = |
62 | i_slint_compiler::parser::normalize_identifier(cur_tok.text()); |
63 | expr_it = expr_it.lookup(ctx, &str)?; |
64 | } |
65 | Some(expr_it) |
66 | })?; |
67 | let gn = match lr? { |
68 | LookupResult::Expression { |
69 | expression: Expression::ElementReference(e), |
70 | .. |
71 | } => e.upgrade()?.borrow().debug.first()?.0.clone().into(), |
72 | LookupResult::Expression { |
73 | expression: |
74 | Expression::CallbackReference(nr, _) |
75 | | Expression::PropertyReference(nr) |
76 | | Expression::FunctionReference(nr, _), |
77 | .. |
78 | } => { |
79 | let mut el = nr.element(); |
80 | loop { |
81 | if let Some(x) = el.borrow().property_declarations.get(nr.name()) { |
82 | break x.node.clone()?; |
83 | } |
84 | let base = el.borrow().base_type.clone(); |
85 | if let ElementType::Component(c) = base { |
86 | el = c.root_element.clone(); |
87 | } else { |
88 | return None; |
89 | } |
90 | } |
91 | } |
92 | LookupResult::Expression { |
93 | expression: Expression::EnumerationValue(v), |
94 | .. |
95 | } => { |
96 | // FIXME: this goes to the enum definition instead of the value definition. |
97 | v.enumeration.node.clone()?.into() |
98 | } |
99 | LookupResult::Enumeration(e) => e.node.clone()?.into(), |
100 | _ => return None, |
101 | }; |
102 | goto_node(&gn) |
103 | } |
104 | _ => None, |
105 | }; |
106 | } else if let Some(n) = syntax_nodes::ImportIdentifier::new(node.clone()) { |
107 | let doc = document_cache.documents.get_document(node.source_file.path())?; |
108 | let imp_name = i_slint_compiler::typeloader::ImportedName::from_node(n); |
109 | return match doc.local_registry.lookup_element(&imp_name.internal_name) { |
110 | Ok(ElementType::Component(c)) => { |
111 | goto_node(&c.root_element.borrow().debug.first()?.0) |
112 | } |
113 | _ => None, |
114 | }; |
115 | } else if let Some(n) = syntax_nodes::ImportSpecifier::new(node.clone()) { |
116 | let import_file = node |
117 | .source_file |
118 | .path() |
119 | .parent() |
120 | .unwrap_or_else(|| Path::new("/" )) |
121 | .join(n.child_text(SyntaxKind::StringLiteral)?.trim_matches(' \"' )); |
122 | let import_file = clean_path(&import_file); |
123 | let doc = document_cache.documents.get_document(&import_file)?; |
124 | let doc_node = doc.node.clone()?; |
125 | return goto_node(&doc_node); |
126 | } else if syntax_nodes::BindingExpression::new(node.clone()).is_some() { |
127 | // don't fallback to the Binding |
128 | return None; |
129 | } else if let Some(n) = syntax_nodes::Binding::new(node.clone()) { |
130 | if token.kind() != SyntaxKind::Identifier { |
131 | return None; |
132 | } |
133 | let prop_name = token.text(); |
134 | let element = syntax_nodes::Element::new(n.parent()?)?; |
135 | if let Some(p) = element.PropertyDeclaration().find_map(|p| { |
136 | (i_slint_compiler::parser::identifier_text(&p.DeclaredIdentifier())? == prop_name) |
137 | .then_some(p) |
138 | }) { |
139 | return goto_node(&p); |
140 | } |
141 | let n = find_property_declaration_in_base(document_cache, element, prop_name)?; |
142 | return goto_node(&n); |
143 | } else if let Some(n) = syntax_nodes::TwoWayBinding::new(node.clone()) { |
144 | if token.kind() != SyntaxKind::Identifier { |
145 | return None; |
146 | } |
147 | let prop_name = token.text(); |
148 | if prop_name != n.child_text(SyntaxKind::Identifier)? { |
149 | return None; |
150 | } |
151 | let element = syntax_nodes::Element::new(n.parent()?)?; |
152 | if let Some(p) = element.PropertyDeclaration().find_map(|p| { |
153 | (i_slint_compiler::parser::identifier_text(&p.DeclaredIdentifier())? == prop_name) |
154 | .then_some(p) |
155 | }) { |
156 | return goto_node(&p); |
157 | } |
158 | let n = find_property_declaration_in_base(document_cache, element, prop_name)?; |
159 | return goto_node(&n); |
160 | } else if let Some(n) = syntax_nodes::CallbackConnection::new(node.clone()) { |
161 | if token.kind() != SyntaxKind::Identifier { |
162 | return None; |
163 | } |
164 | let prop_name = token.text(); |
165 | if prop_name != n.child_text(SyntaxKind::Identifier)? { |
166 | return None; |
167 | } |
168 | let element = syntax_nodes::Element::new(n.parent()?)?; |
169 | if let Some(p) = element.CallbackDeclaration().find_map(|p| { |
170 | (i_slint_compiler::parser::identifier_text(&p.DeclaredIdentifier())? == prop_name) |
171 | .then_some(p) |
172 | }) { |
173 | return goto_node(&p); |
174 | } |
175 | let n = find_property_declaration_in_base(document_cache, element, prop_name)?; |
176 | return goto_node(&n); |
177 | } |
178 | node = node.parent()?; |
179 | } |
180 | } |
181 | |
182 | /// Try to lookup the property `prop_name` in the base of the given Element |
183 | fn find_property_declaration_in_base( |
184 | document_cache: &DocumentCache, |
185 | element: syntax_nodes::Element, |
186 | prop_name: &str, |
187 | ) -> Option<SyntaxNode> { |
188 | let global_tr: Ref<'_, TypeRegister> = document_cache.documents.global_type_registry.borrow(); |
189 | let tr: &TypeRegister = element |
190 | .source_file() |
191 | .and_then(|sf| document_cache.documents.get_document(sf.path())) |
192 | .map(|doc| &doc.local_registry) |
193 | .unwrap_or(&global_tr); |
194 | |
195 | let mut element_type: ElementType = lookup_current_element_type((*element).clone(), tr)?; |
196 | while let ElementType::Component(com: Rc) = element_type { |
197 | if let Some(p: &PropertyDeclaration) = com.root_element.borrow().property_declarations.get(key:prop_name) { |
198 | return p.node.clone(); |
199 | } |
200 | element_type = com.root_element.borrow().base_type.clone(); |
201 | } |
202 | None |
203 | } |
204 | |
205 | fn goto_node(node: &SyntaxNode) -> Option<GotoDefinitionResponse> { |
206 | let (target_uri: Url, range: Range) = map_node_and_url(node)?; |
207 | let range: Range = Range::new(range.start, end:range.start); // Shrink range to a position:-) |
208 | Some(GotoDefinitionResponse::Link(vec![LocationLink { |
209 | origin_selection_range: None, |
210 | target_uri, |
211 | target_range: range, |
212 | target_selection_range: range, |
213 | }])) |
214 | } |
215 | |
216 | #[test ] |
217 | fn test_goto_definition() { |
218 | fn first_link(def: &GotoDefinitionResponse) -> &LocationLink { |
219 | let GotoDefinitionResponse::Link(link) = def else { panic!("not a single link {def:?}" ) }; |
220 | link.first().unwrap() |
221 | } |
222 | |
223 | let source = r#" |
224 | import { Button } from "std-widgets.slint"; |
225 | component Abc { |
226 | in property <string> hello; |
227 | } |
228 | export component Test { |
229 | abc := Abc { |
230 | hello: "foo"; |
231 | } |
232 | btn := Button { |
233 | text: abc.hello; |
234 | } |
235 | rec := Rectangle { } |
236 | }"# ; |
237 | |
238 | let (mut dc, uri, _) = crate::language::test::loaded_document_cache(source.into()); |
239 | let doc = dc |
240 | .documents |
241 | .get_document(&crate::language::uri_to_file(&uri).unwrap()) |
242 | .unwrap() |
243 | .node |
244 | .clone() |
245 | .unwrap(); |
246 | |
247 | // Jump to the definition of Abc |
248 | let offset = source.find("abc := Abc" ).unwrap() as u32; |
249 | let token = crate::language::token_at_offset(&doc, offset + 8).unwrap(); |
250 | assert_eq!(token.text(), "Abc" ); |
251 | let def = goto_definition(&mut dc, token).unwrap(); |
252 | let link = first_link(&def); |
253 | assert_eq!(link.target_uri, uri); |
254 | assert_eq!(link.target_range.start.line, 2); |
255 | |
256 | // Jump to the definition of abc |
257 | let offset = source.find("text: abc.hello" ).unwrap() as u32; |
258 | let token = crate::language::token_at_offset(&doc, offset + 7).unwrap(); |
259 | assert_eq!(token.text(), "abc" ); |
260 | let def = goto_definition(&mut dc, token).unwrap(); |
261 | let link = first_link(&def); |
262 | assert_eq!(link.target_uri, uri); |
263 | assert_eq!(link.target_range.start.line, 6); |
264 | |
265 | // Jump to the definition of hello |
266 | let offset = source.find("text: abc.hello" ).unwrap() as u32; |
267 | let token = crate::language::token_at_offset(&doc, offset + 12).unwrap(); |
268 | assert_eq!(token.text(), "hello" ); |
269 | let def = goto_definition(&mut dc, token).unwrap(); |
270 | let link = first_link(&def); |
271 | assert_eq!(link.target_uri, uri); |
272 | assert_eq!(link.target_range.start.line, 3); |
273 | |
274 | // Also jump to the definition of hello |
275 | let offset = source.find("hello: \"foo \"" ).unwrap() as u32; |
276 | let token = crate::language::token_at_offset(&doc, offset).unwrap(); |
277 | assert_eq!(token.text(), "hello" ); |
278 | let def = goto_definition(&mut dc, token).unwrap(); |
279 | let link = first_link(&def); |
280 | assert_eq!(link.target_uri, uri); |
281 | assert_eq!(link.target_range.start.line, 3); |
282 | |
283 | // Rectangle is builtin and not accessible |
284 | let offset = source.find("rec := " ).unwrap() as u32; |
285 | let token = crate::language::token_at_offset(&doc, offset + 8).unwrap(); |
286 | assert_eq!(token.text(), "Rectangle" ); |
287 | assert!(goto_definition(&mut dc, token).is_none()); |
288 | |
289 | // Button is builtin and not accessible |
290 | let offset = source.find("btn := " ).unwrap() as u32; |
291 | let token = crate::language::token_at_offset(&doc, offset + 9).unwrap(); |
292 | assert_eq!(token.text(), "Button" ); |
293 | assert!(goto_definition(&mut dc, token).is_none()); |
294 | let offset = source.find("text: abc.hello" ).unwrap() as u32; |
295 | let token = crate::language::token_at_offset(&doc, offset).unwrap(); |
296 | assert_eq!(token.text(), "text" ); |
297 | assert!(goto_definition(&mut dc, token).is_none()); |
298 | } |
299 | |