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
4use super::DocumentCache;
5use crate::util::{lookup_current_element_type, map_node_and_url, with_lookup_ctx};
6
7use i_slint_compiler::diagnostics::Spanned;
8use i_slint_compiler::expression_tree::Expression;
9use i_slint_compiler::langtype::{ElementType, Type};
10use i_slint_compiler::lookup::{LookupObject, LookupResult};
11use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxNode, SyntaxToken};
12use i_slint_compiler::pathutils::clean_path;
13
14use lsp_types::{GotoDefinitionResponse, LocationLink, Range};
15use std::path::Path;
16
17pub 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
183fn 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
205fn 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]
217fn 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#"
224import { Button } from "std-widgets.slint";
225component Abc {
226 in property <string> hello;
227}
228export 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