1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use i_slint_compiler::diagnostics::{DiagnosticLevel, SourceFile, Spanned};
5use i_slint_compiler::langtype::{ElementType, Type};
6use i_slint_compiler::lookup::LookupCtx;
7use i_slint_compiler::object_tree;
8use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxNode, SyntaxToken};
9use i_slint_compiler::parser::{TextRange, TextSize};
10use i_slint_compiler::typeregister::TypeRegister;
11use smol_str::SmolStr;
12
13use crate::common;
14
15#[cfg(target_arch = "wasm32")]
16use crate::wasm_prelude::UrlWasm;
17
18/// Get the `TextRange` of a `node`, excluding any trailing whitespace tokens.
19pub fn node_range_without_trailing_ws(node: &SyntaxNode) -> TextRange {
20 let range = node.text_range();
21 // shorten range to not include trailing WS:
22 TextRange::new(
23 range.start(),
24 end:last_non_ws_token(node).map(|t| t.text_range().end()).unwrap_or(default:range.end()),
25 )
26}
27
28/// Map a `node` to its `Url` and a `Range` of characters covered by the `node`
29///
30/// This will exclude trailing whitespaces.
31pub fn node_to_url_and_lsp_range(node: &SyntaxNode) -> Option<(lsp_types::Url, lsp_types::Range)> {
32 let path: &Path = node.source_file.path();
33 Some((lsp_types::Url::from_file_path(path).ok()?, node_to_lsp_range(node)))
34}
35
36/// Map a `node` to the `Range` of characters covered by the `node`
37pub fn node_to_lsp_range(node: &SyntaxNode) -> lsp_types::Range {
38 let range: TextRange = node.text_range();
39 text_range_to_lsp_range(&node.source_file, range)
40}
41
42/// Map a `token` to the `Range` of characters covered by the `token`
43pub fn token_to_lsp_range(token: &SyntaxToken) -> lsp_types::Range {
44 let range: TextRange = token.text_range();
45 text_range_to_lsp_range(&token.parent().source_file, range)
46}
47
48/// Convert a `TextSize` to a `Position` for use in the LSP
49pub fn text_size_to_lsp_position(sf: &SourceFile, pos: TextSize) -> lsp_types::Position {
50 let (line: usize, column: usize) = sf.line_column(offset:pos.into());
51 lsp_types::Position::new((line as u32).saturating_sub(1), (column as u32).saturating_sub(1))
52}
53
54/// Convert a `TextRange` to a `Range` for use in the LSP
55pub fn text_range_to_lsp_range(sf: &SourceFile, range: TextRange) -> lsp_types::Range {
56 lsp_types::Range::new(
57 start:text_size_to_lsp_position(sf, range.start()),
58 end:text_size_to_lsp_position(sf, pos:range.end()),
59 )
60}
61
62/// Convert a `Position` from the LSP into a `TextSize`
63pub fn lsp_position_to_text_size(sf: &SourceFile, position: lsp_types::Position) -> TextSize {
64 (sf.offset(
65 line:usize::try_from(position.line).unwrap() + 1,
66 column:usize::try_from(position.character).unwrap() + 1,
67 ) as u32)
68 .into()
69}
70
71/// Convert a `Range` from the LSP into a `TextRange`
72pub fn lsp_range_to_text_range(sf: &SourceFile, range: lsp_types::Range) -> TextRange {
73 TextRange::new(
74 start:lsp_position_to_text_size(sf, range.start),
75 end:lsp_position_to_text_size(sf, position:range.end),
76 )
77}
78
79// Find the last token that is not a Whitespace in a `SyntaxNode`. May return
80// `None` if the node contains no tokens or they are all Whitespace.
81pub fn last_non_ws_token(node: &SyntaxNode) -> Option<SyntaxToken> {
82 let mut last_non_ws: Option = None;
83 let mut token: Option = node.first_token();
84 while let Some(t: SyntaxToken) = token {
85 if t.text_range().end() > node.text_range().end() {
86 break;
87 }
88
89 if t.kind() != SyntaxKind::Whitespace && t.kind() != SyntaxKind::Eof {
90 last_non_ws = Some(t.clone());
91 }
92 token = t.next_token();
93 }
94 last_non_ws
95}
96
97// Find the indentation of the element node itself as well as the indentation of properties inside the
98// element. Returns the element indent.
99pub fn find_element_indent(element: &common::ElementRcNode) -> Option<String> {
100 let mut token: Option<{unknown}> = element.with_element_node(|node: &Element| node.first_token()?.prev_token());
101 while let Some(t) = token {
102 if t.kind() == SyntaxKind::Whitespace && t.text().contains('\n') {
103 return t.text().split('\n').last().map(|s| s.to_owned());
104 }
105 token = t.prev_token();
106 }
107 None
108}
109
110/// Given a node within an element, return the Type for the Element under that node.
111/// (If node is an element, return the Type for that element, otherwise the type of the element under it)
112/// Will return `Foo` in the following example where `|` is the cursor.
113///
114/// ```text
115/// Hello := A {
116/// B {
117/// Foo {
118/// |
119/// }
120/// }
121/// }
122/// ```
123pub fn lookup_current_element_type(mut node: SyntaxNode, tr: &TypeRegister) -> Option<ElementType> {
124 while node.kind() != SyntaxKind::Element {
125 if let Some(parent: SyntaxNode) = node.parent() {
126 node = parent
127 } else {
128 return None;
129 }
130 }
131
132 let parent: SyntaxNode = node.parent()?;
133 if parent.kind() == SyntaxKind::Component
134 && parent.child_text(kind:SyntaxKind::Identifier).is_some_and(|x: SmolStr| x == "global")
135 {
136 return Some(ElementType::Global);
137 }
138 let parent: ElementType = lookup_current_element_type(node:parent, tr).unwrap_or_default();
139 let qualname: QualifiedTypeName = object_tree::QualifiedTypeName::from_node(
140 syntax_nodes::Element::from(node).QualifiedName()?,
141 );
142 parent.lookup_type_for_child_element(&qualname.to_string(), tr).ok()
143}
144
145#[derive(Debug)]
146pub struct ExpressionContextInfo {
147 element: syntax_nodes::Element,
148 property_name: SmolStr,
149 is_animate: bool,
150}
151
152impl ExpressionContextInfo {
153 pub fn new(element: syntax_nodes::Element, property_name: SmolStr, is_animate: bool) -> Self {
154 ExpressionContextInfo { element, property_name, is_animate }
155 }
156}
157
158/// Run the function with the LookupCtx associated with the token
159pub fn with_lookup_ctx<R>(
160 document_cache: &common::DocumentCache,
161 node: SyntaxNode,
162 f: impl FnOnce(&mut LookupCtx) -> R,
163) -> Option<R> {
164 let expr_context_info: ExpressionContextInfo = lookup_expression_context(node)?;
165 with_property_lookup_ctx::<R>(document_cache, &expr_context_info, f)
166}
167
168/// Run the function with the LookupCtx associated with the token
169pub fn with_property_lookup_ctx<R>(
170 document_cache: &common::DocumentCache,
171 expr_context_info: &ExpressionContextInfo,
172 f: impl FnOnce(&mut LookupCtx) -> R,
173) -> Option<R> {
174 let (element, prop_name, is_animate) = (
175 &expr_context_info.element,
176 expr_context_info.property_name.as_str(),
177 expr_context_info.is_animate,
178 );
179 let global_tr = document_cache.global_type_registry();
180 let tr = element
181 .source_file()
182 .and_then(|sf| document_cache.get_document_for_source_file(sf))
183 .map(|doc| &doc.local_registry)
184 .unwrap_or(&global_tr);
185
186 let component = {
187 let mut n = element.parent()?;
188 loop {
189 if let Some(component) = syntax_nodes::Component::new(n.clone()) {
190 break component;
191 }
192 n = n.parent()?;
193 }
194 };
195
196 let mut scope = Vec::new();
197 let component = i_slint_compiler::parser::identifier_text(&component.DeclaredIdentifier())
198 .and_then(|component_name| tr.lookup_element(&component_name).ok())?;
199 if let ElementType::Component(c) = component {
200 let mut it = c.root_element.clone();
201 let offset = element.text_range().start();
202 loop {
203 scope.push(it.clone());
204 if let Some(c) = it.clone().borrow().children.iter().find(|c| {
205 c.borrow().debug.first().is_some_and(|n| n.node.text_range().contains(offset))
206 }) {
207 it = c.clone();
208 } else {
209 break;
210 }
211 }
212 };
213
214 let mut ty = element
215 .PropertyDeclaration()
216 .find_map(|p| {
217 (i_slint_compiler::parser::identifier_text(&p.DeclaredIdentifier())? == prop_name)
218 .then_some(p)
219 })
220 .and_then(|p| p.Type())
221 .map(|n| object_tree::type_from_node(n, &mut Default::default(), tr))
222 .or_else(|| scope.last().map(|e| e.borrow().lookup_property(prop_name).property_type));
223
224 // try to match properties from `PropertyAnimation`
225 if is_animate {
226 ty = global_tr
227 .property_animation_type_for_property(Type::Float32)
228 .property_list()
229 .iter()
230 .find_map(|(p, t)| if p.as_str() == prop_name { Some(t.clone()) } else { None })
231 }
232
233 let mut build_diagnostics = Default::default();
234 let mut lookup_context = LookupCtx::empty_context(tr, &mut build_diagnostics);
235 lookup_context.property_name = Some(prop_name);
236 lookup_context.property_type = ty.unwrap_or_default();
237 lookup_context.component_scope = &scope;
238 lookup_context.current_token = Some((**element).clone().into());
239
240 if let Some(cb) = element
241 .CallbackConnection()
242 .find(|p| i_slint_compiler::parser::identifier_text(p).is_some_and(|x| x == prop_name))
243 {
244 lookup_context.arguments = cb
245 .DeclaredIdentifier()
246 .flat_map(|a| i_slint_compiler::parser::identifier_text(&a))
247 .collect();
248 } else if let Some(f) = element.Function().find(|p| {
249 i_slint_compiler::parser::identifier_text(&p.DeclaredIdentifier())
250 .is_some_and(|x| x == prop_name)
251 }) {
252 lookup_context.arguments = f
253 .ArgumentDeclaration()
254 .flat_map(|a| i_slint_compiler::parser::identifier_text(&a.DeclaredIdentifier()))
255 .collect();
256 }
257 Some(f(&mut lookup_context))
258}
259
260/// Return the element and property name in which we are
261fn lookup_expression_context(mut n: SyntaxNode) -> Option<ExpressionContextInfo> {
262 let (element, prop_name, is_animate) = loop {
263 if let Some(decl) = syntax_nodes::PropertyDeclaration::new(n.clone()) {
264 let prop_name = i_slint_compiler::parser::identifier_text(&decl.DeclaredIdentifier())?;
265 let element = syntax_nodes::Element::new(n.parent()?)?;
266 break (element, prop_name, false);
267 }
268 match n.kind() {
269 SyntaxKind::Binding | SyntaxKind::TwoWayBinding | SyntaxKind::CallbackConnection => {
270 let mut parent = n.parent()?;
271 if parent.kind() == SyntaxKind::PropertyAnimation {
272 let prop_name = i_slint_compiler::parser::identifier_text(&n)?;
273 let element = syntax_nodes::Element::new(parent.parent()?)?;
274 break (element, prop_name, true);
275 } else {
276 let prop_name =
277 i_slint_compiler::parser::identifier_text(&n).unwrap_or_default();
278 loop {
279 if let Some(element) = syntax_nodes::Element::new(parent.clone()) {
280 return Some(ExpressionContextInfo::new(element, prop_name, false));
281 }
282 parent = parent.parent()?;
283 }
284 }
285 }
286 SyntaxKind::Function => {
287 let prop_name = i_slint_compiler::parser::identifier_text(
288 &n.child_node(SyntaxKind::DeclaredIdentifier)?,
289 )?;
290 let element = syntax_nodes::Element::new(n.parent()?)?;
291 break (element, prop_name, false);
292 }
293 SyntaxKind::ConditionalElement | SyntaxKind::RepeatedElement => {
294 let element = syntax_nodes::Element::new(n.parent()?)?;
295 break (element, "$model".into(), false);
296 }
297 SyntaxKind::Element => {
298 // oops: missed it
299 let element = syntax_nodes::Element::new(n)?;
300 break (element, SmolStr::default(), false);
301 }
302 _ => n = n.parent()?,
303 }
304 };
305 Some(ExpressionContextInfo::new(element, prop_name, is_animate))
306}
307
308pub fn to_lsp_diag(d: &i_slint_compiler::diagnostics::Diagnostic) -> lsp_types::Diagnostic {
309 lsp_types::Diagnostic::new(
310 to_range(d.line_column()),
311 severity:Some(to_lsp_diag_level(d.level())),
312 code:None,
313 source:None,
314 message:d.message().to_owned(),
315 related_information:None,
316 tags:None,
317 )
318}
319
320fn to_range(span: (usize, usize)) -> lsp_types::Range {
321 let pos: Position = lsp_types::Position::new(
322 (span.0 as u32).saturating_sub(1),
323 (span.1 as u32).saturating_sub(1),
324 );
325 lsp_types::Range::new(start:pos, end:pos)
326}
327
328fn to_lsp_diag_level(level: DiagnosticLevel) -> lsp_types::DiagnosticSeverity {
329 match level {
330 DiagnosticLevel::Error => lsp_types::DiagnosticSeverity::ERROR,
331 DiagnosticLevel::Warning => lsp_types::DiagnosticSeverity::WARNING,
332 _ => lsp_types::DiagnosticSeverity::INFORMATION,
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 use crate::language::test::loaded_document_cache;
341
342 #[test]
343 fn test_find_element_indent() {
344 let (dc, url, _) = loaded_document_cache(
345 r#"component MainWindow inherits Window {
346 VerticalBox {
347 label := Text { text: "text"; }
348 }
349}"#
350 .to_string(),
351 );
352
353 let window = dc.element_at_position(&url, &lsp_types::Position::new(0, 30));
354 assert_eq!(find_element_indent(&window.unwrap()), None);
355
356 let vbox = dc.element_at_position(&url, &lsp_types::Position::new(1, 4));
357 assert_eq!(find_element_indent(&vbox.unwrap()), Some(" ".to_string()));
358
359 let label = dc.element_at_position(&url, &lsp_types::Position::new(2, 17));
360 assert_eq!(find_element_indent(&label.unwrap()), Some(" ".to_string()));
361 }
362
363 #[test]
364 fn test_map_position() {
365 let text = r#"// 🔥 Test 🎆
366component MainWindow inherits Window {
367 VerticalBox {
368 label := Text { text: "te🦥xt"; }
369 }
370}"#
371 .to_string();
372 let (dc, url, _) = loaded_document_cache(text.clone());
373 let doc = dc.get_document(&url).unwrap();
374 let source = doc.node.as_ref().unwrap().source_file.clone();
375 let mut offset = TextSize::new(0);
376 let mut line = 0_usize;
377 let mut pos = 0_usize;
378 for c in text.chars() {
379 let original_offset = offset;
380 let mapped = text_size_to_lsp_position(&source, u32::from(original_offset).into());
381 eprintln!(
382 "c: {c} <offset: {offset:?}> => {line}:{pos} => mapped {}:{}",
383 mapped.line, mapped.character
384 );
385 assert_eq!(mapped.line, (line as u32));
386 assert_eq!(mapped.character, (pos as u32));
387 let unmapped = lsp_position_to_text_size(&source, mapped);
388 assert_eq!(unmapped, original_offset);
389 offset = offset.checked_add((c.len_utf8() as u32).into()).unwrap();
390 match c {
391 '\n' => {
392 line += 1;
393 pos = 0
394 }
395 c => {
396 pos += c.len_utf8();
397 }
398 }
399 }
400 }
401}
402