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 crate::common::{self, Result}; |
5 | use crate::language; |
6 | use crate::util; |
7 | |
8 | use i_slint_compiler::diagnostics::{BuildDiagnostics, SourceFileVersion, Spanned}; |
9 | use i_slint_compiler::langtype::{ElementType, Type}; |
10 | use i_slint_compiler::object_tree::{Element, PropertyDeclaration, PropertyVisibility}; |
11 | use i_slint_compiler::parser::{syntax_nodes, Language, SyntaxKind}; |
12 | |
13 | use std::collections::HashSet; |
14 | |
15 | #[cfg (target_arch = "wasm32" )] |
16 | use crate::wasm_prelude::*; |
17 | |
18 | #[derive (serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq)] |
19 | pub(crate) struct DefinitionInformation { |
20 | property_definition_range: lsp_types::Range, |
21 | selection_range: lsp_types::Range, |
22 | expression_range: lsp_types::Range, |
23 | expression_value: String, |
24 | } |
25 | |
26 | #[derive (serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq)] |
27 | pub(crate) struct DeclarationInformation { |
28 | uri: lsp_types::Url, |
29 | start_position: lsp_types::Position, |
30 | } |
31 | |
32 | #[derive (serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq)] |
33 | pub(crate) struct PropertyInformation { |
34 | name: String, |
35 | type_name: String, |
36 | declared_at: Option<DeclarationInformation>, |
37 | defined_at: Option<DefinitionInformation>, // Range in the elements source file! |
38 | group: String, |
39 | } |
40 | |
41 | #[derive (serde::Deserialize, serde::Serialize, Debug)] |
42 | pub(crate) struct ElementInformation { |
43 | id: String, |
44 | type_name: String, |
45 | range: Option<lsp_types::Range>, |
46 | } |
47 | |
48 | #[derive (serde::Deserialize, serde::Serialize)] |
49 | pub(crate) struct QueryPropertyResponse { |
50 | properties: Vec<PropertyInformation>, |
51 | element: Option<ElementInformation>, |
52 | source_uri: String, |
53 | source_version: i32, |
54 | } |
55 | |
56 | impl QueryPropertyResponse { |
57 | pub fn no_element_response(source_uri: String, source_version: i32) -> Self { |
58 | QueryPropertyResponse { properties: vec![], element: None, source_uri, source_version } |
59 | } |
60 | } |
61 | |
62 | #[derive (Debug, serde::Deserialize, serde::Serialize)] |
63 | pub struct SetBindingResponse { |
64 | diagnostics: Vec<lsp_types::Diagnostic>, |
65 | } |
66 | |
67 | // This gets defined accessibility properties... |
68 | fn get_reserved_properties<'a>( |
69 | group: &'a str, |
70 | properties: &'a [(&'a str, Type)], |
71 | ) -> impl Iterator<Item = PropertyInformation> + 'a { |
72 | properties.iter().map(|p: &(&str, Type)| PropertyInformation { |
73 | name: p.0.to_string(), |
74 | type_name: format!(" {}" , p.1), |
75 | declared_at: None, |
76 | defined_at: None, |
77 | group: group.to_string(), |
78 | }) |
79 | } |
80 | |
81 | fn property_is_editable(property: &PropertyDeclaration, is_local_element: bool) -> bool { |
82 | if !property.property_type.is_property_type() { |
83 | // Filter away the callbacks |
84 | return false; |
85 | } |
86 | if matches!(property.visibility, PropertyVisibility::Output | PropertyVisibility::Private) |
87 | && !is_local_element |
88 | { |
89 | // Skip properties that cannot be set because of visibility rules |
90 | return false; |
91 | } |
92 | if property.type_node().is_none() { |
93 | return false; |
94 | } |
95 | |
96 | true |
97 | } |
98 | |
99 | fn add_element_properties( |
100 | element: &Element, |
101 | group: &str, |
102 | is_local_element: bool, |
103 | result: &mut Vec<PropertyInformation>, |
104 | ) { |
105 | result.extend(iter:element.property_declarations.iter().filter_map(move |(name: &String, value: &PropertyDeclaration)| { |
106 | if !property_is_editable(property:value, is_local_element) { |
107 | return None; |
108 | } |
109 | |
110 | let declared_at: Option = valueOption<(Url, Range)> |
111 | .type_node() |
112 | .as_ref() |
113 | .and_then(util::map_node_and_url) |
114 | .map(|(uri: Url, range: Range)| DeclarationInformation { uri, start_position: range.start }); |
115 | Some(PropertyInformation { |
116 | name: name.clone(), |
117 | type_name: value.property_type.to_string(), |
118 | declared_at, |
119 | defined_at: None, |
120 | group: group.to_string(), |
121 | }) |
122 | })) |
123 | } |
124 | |
125 | /// Move left from the start of a `token` to include white-space and comments that go with it. |
126 | fn left_extend(token: rowan::SyntaxToken<Language>) -> rowan::SyntaxToken<Language> { |
127 | let mut current_token = token.prev_token(); |
128 | let mut start_token = token.clone(); |
129 | let mut last_comment = token; |
130 | |
131 | // Walk backwards: |
132 | while let Some(t) = current_token { |
133 | if t.kind() == SyntaxKind::Whitespace { |
134 | let lbs = t.text().matches(' \n' ).count(); |
135 | if lbs >= 1 { |
136 | start_token = last_comment.clone(); |
137 | } |
138 | if lbs >= 2 { |
139 | break; |
140 | } |
141 | current_token = t.prev_token(); |
142 | continue; |
143 | } |
144 | if t.kind() == SyntaxKind::Comment { |
145 | last_comment = t.clone(); |
146 | current_token = t.prev_token(); |
147 | continue; |
148 | } |
149 | break; |
150 | } |
151 | |
152 | start_token |
153 | } |
154 | |
155 | /// Move right from the end of the `token` to include white-space and comments that go with it. |
156 | fn right_extend(token: rowan::SyntaxToken<Language>) -> rowan::SyntaxToken<Language> { |
157 | let mut current_token = token.next_token(); |
158 | let mut end_token = token.clone(); |
159 | let mut last_comment = token; |
160 | |
161 | // Walk forwards: |
162 | while let Some(t) = current_token { |
163 | if t.kind() == SyntaxKind::RBrace { |
164 | // All comments between us and a `}` belong to us! |
165 | end_token = last_comment; |
166 | break; |
167 | } |
168 | if t.kind() == SyntaxKind::Whitespace { |
169 | let lbs = t.text().matches(' \n' ).count(); |
170 | if lbs > 0 { |
171 | // comments in the current line belong to us, *if* there is a linebreak |
172 | end_token = last_comment; |
173 | break; |
174 | } |
175 | current_token = t.next_token(); |
176 | continue; |
177 | } |
178 | if t.kind() == SyntaxKind::Comment { |
179 | last_comment = t.clone(); |
180 | current_token = t.next_token(); |
181 | continue; |
182 | } |
183 | |
184 | // in all other cases: Leave the comment to the following token! |
185 | break; |
186 | } |
187 | |
188 | end_token |
189 | } |
190 | |
191 | fn find_expression_range( |
192 | element: &syntax_nodes::Element, |
193 | offset: u32, |
194 | ) -> Option<DefinitionInformation> { |
195 | let mut selection_range = None; |
196 | let mut expression_range = None; |
197 | let mut expression_value = None; |
198 | let mut property_definition_range = None; |
199 | |
200 | let source_file = element.source_file()?; |
201 | |
202 | if let Some(token) = element.token_at_offset(offset.into()).right_biased() { |
203 | for ancestor in token.parent_ancestors() { |
204 | if ancestor.kind() == SyntaxKind::BindingExpression { |
205 | // The BindingExpression contains leading and trailing whitespace + `;` |
206 | let expr = &ancestor.first_child(); |
207 | expression_range = expr.as_ref().map(|e| e.text_range()); |
208 | expression_value = expr.as_ref().map(|e| e.text().to_string()); |
209 | continue; |
210 | } |
211 | if (ancestor.kind() == SyntaxKind::Binding) |
212 | || (ancestor.kind() == SyntaxKind::PropertyDeclaration) |
213 | { |
214 | property_definition_range = Some(ancestor.text_range()); |
215 | selection_range = Some(rowan::TextRange::new( |
216 | left_extend(ancestor.first_token()?).text_range().start(), |
217 | right_extend(ancestor.last_token()?).text_range().end(), |
218 | )) |
219 | .or(property_definition_range); |
220 | break; |
221 | } |
222 | if ancestor.kind() == SyntaxKind::Element { |
223 | // There should have been a binding before the element! |
224 | break; |
225 | } |
226 | } |
227 | } |
228 | Some(DefinitionInformation { |
229 | property_definition_range: util::map_range(source_file, property_definition_range?), |
230 | selection_range: util::map_range(source_file, selection_range?), |
231 | expression_range: util::map_range(source_file, expression_range?), |
232 | expression_value: expression_value?, |
233 | }) |
234 | } |
235 | |
236 | fn find_property_binding_offset( |
237 | element: &common::ElementRcNode, |
238 | property_name: &str, |
239 | ) -> Option<u32> { |
240 | let element_range = element.with_element_node(|node: &Element| node.text_range()); |
241 | |
242 | let element: Ref<'_, Element> = element.element.borrow(); |
243 | |
244 | if let Some(v: &RefCell) = element.bindings.get(key:property_name) { |
245 | if let Some(span: &SourceLocation) = &v.borrow().span { |
246 | let offset: u32 = span.span().offset as u32; |
247 | if element.source_file().map(|sf: &Rc| sf.path()) |
248 | == span.source_file.as_ref().map(|sf: &Rc| sf.path()) |
249 | && element_range.contains(offset.into()) |
250 | { |
251 | return Some(offset); |
252 | } |
253 | } |
254 | } |
255 | |
256 | None |
257 | } |
258 | |
259 | fn insert_property_definitions( |
260 | element: &common::ElementRcNode, |
261 | mut properties: Vec<PropertyInformation>, |
262 | ) -> Vec<PropertyInformation> { |
263 | for prop_info: &mut PropertyInformation in properties.iter_mut() { |
264 | if let Some(offset: u32) = find_property_binding_offset(element, property_name:prop_info.name.as_str()) { |
265 | prop_info.defined_at = |
266 | element.with_element_node(|node: &Element| find_expression_range(element:node, offset)); |
267 | } |
268 | } |
269 | properties |
270 | } |
271 | |
272 | fn get_properties(element: &common::ElementRcNode) -> Vec<PropertyInformation> { |
273 | let mut result = Vec::new(); |
274 | add_element_properties(&element.element.borrow(), "" , true, &mut result); |
275 | |
276 | let mut current_element = element.element.clone(); |
277 | |
278 | let geometry_prop = HashSet::from(["x" , "y" , "width" , "height" ]); |
279 | |
280 | loop { |
281 | let base_type = current_element.borrow().base_type.clone(); |
282 | match base_type { |
283 | ElementType::Component(c) => { |
284 | current_element = c.root_element.clone(); |
285 | add_element_properties(¤t_element.borrow(), &c.id, false, &mut result); |
286 | continue; |
287 | } |
288 | ElementType::Builtin(b) => { |
289 | result.extend(b.properties.iter().filter_map(|(k, t)| { |
290 | if geometry_prop.contains(k.as_str()) { |
291 | // skip geometry property because they are part of the reserved ones |
292 | return None; |
293 | } |
294 | if !t.ty.is_property_type() { |
295 | // skip callbacks and other functions |
296 | return None; |
297 | } |
298 | if t.property_visibility == PropertyVisibility::Output { |
299 | // Skip output-only properties |
300 | return None; |
301 | } |
302 | |
303 | Some(PropertyInformation { |
304 | name: k.clone(), |
305 | type_name: t.ty.to_string(), |
306 | declared_at: None, |
307 | defined_at: None, |
308 | group: b.name.clone(), |
309 | }) |
310 | })); |
311 | |
312 | if b.name == "Rectangle" { |
313 | result.push(PropertyInformation { |
314 | name: "clip" .into(), |
315 | type_name: Type::Bool.to_string(), |
316 | declared_at: None, |
317 | defined_at: None, |
318 | group: String::new(), |
319 | }); |
320 | } |
321 | |
322 | result.push(PropertyInformation { |
323 | name: "opacity" .into(), |
324 | type_name: Type::Float32.to_string(), |
325 | declared_at: None, |
326 | defined_at: None, |
327 | group: String::new(), |
328 | }); |
329 | result.push(PropertyInformation { |
330 | name: "visible" .into(), |
331 | type_name: Type::Bool.to_string(), |
332 | declared_at: None, |
333 | defined_at: None, |
334 | group: String::new(), |
335 | }); |
336 | |
337 | if b.name == "Image" { |
338 | result.extend(get_reserved_properties( |
339 | "rotation" , |
340 | i_slint_compiler::typeregister::RESERVED_ROTATION_PROPERTIES, |
341 | )); |
342 | } |
343 | |
344 | if b.name == "Rectangle" { |
345 | result.extend(get_reserved_properties( |
346 | "drop-shadow" , |
347 | i_slint_compiler::typeregister::RESERVED_DROP_SHADOW_PROPERTIES, |
348 | )); |
349 | } |
350 | } |
351 | ElementType::Global => { |
352 | break; |
353 | } |
354 | |
355 | _ => {} |
356 | } |
357 | |
358 | result.extend(get_reserved_properties( |
359 | "geometry" , |
360 | i_slint_compiler::typeregister::RESERVED_GEOMETRY_PROPERTIES, |
361 | )); |
362 | result.extend( |
363 | get_reserved_properties( |
364 | "layout" , |
365 | i_slint_compiler::typeregister::RESERVED_LAYOUT_PROPERTIES, |
366 | ) |
367 | // padding arbitrary items is not yet implemented |
368 | .filter(|x| !x.name.starts_with("padding" )), |
369 | ); |
370 | // FIXME: ideally only if parent is a grid layout |
371 | result.extend(get_reserved_properties( |
372 | "layout" , |
373 | i_slint_compiler::typeregister::RESERVED_GRIDLAYOUT_PROPERTIES, |
374 | )); |
375 | result.push(PropertyInformation { |
376 | name: "accessible-role" .into(), |
377 | type_name: Type::Enumeration( |
378 | i_slint_compiler::typeregister::BUILTIN_ENUMS.with(|e| e.AccessibleRole.clone()), |
379 | ) |
380 | .to_string(), |
381 | declared_at: None, |
382 | defined_at: None, |
383 | group: "accessibility" .into(), |
384 | }); |
385 | if current_element.borrow().is_binding_set("accessible-role" , true) { |
386 | result.extend(get_reserved_properties( |
387 | "accessibility" , |
388 | i_slint_compiler::typeregister::RESERVED_ACCESSIBILITY_PROPERTIES, |
389 | )); |
390 | } |
391 | break; |
392 | } |
393 | |
394 | insert_property_definitions(element, result) |
395 | } |
396 | |
397 | fn find_block_range(element: &common::ElementRcNode) -> Option<lsp_types::Range> { |
398 | element.with_element_node(|node: &Element| { |
399 | let open_brace = node.child_token(SyntaxKind::LBrace)?; |
400 | let close_brace = node.child_token(SyntaxKind::RBrace)?; |
401 | |
402 | Some(lsp_types::Range::new( |
403 | start:util::map_position(node.source_file()?, open_brace.text_range().start()), |
404 | end:util::map_position(sf:node.source_file()?, pos:close_brace.text_range().end()), |
405 | )) |
406 | }) |
407 | } |
408 | |
409 | fn get_element_information(element: &common::ElementRcNode) -> ElementInformation { |
410 | let range: Option = element.with_element_node(|node: &Element| util::map_node(node)); |
411 | let e: Ref<'_, Element> = element.element.borrow(); |
412 | |
413 | ElementInformation { id: e.id.clone(), type_name: e.base_type.to_string(), range } |
414 | } |
415 | |
416 | pub(crate) fn query_properties( |
417 | uri: &lsp_types::Url, |
418 | source_version: SourceFileVersion, |
419 | element: &common::ElementRcNode, |
420 | ) -> Result<QueryPropertyResponse> { |
421 | Ok(QueryPropertyResponse { |
422 | properties: get_properties(element), |
423 | element: Some(get_element_information(element)), |
424 | source_uri: uri.to_string(), |
425 | source_version: source_version.unwrap_or(default:i32::MIN), |
426 | }) |
427 | } |
428 | |
429 | fn get_property_information( |
430 | properties: &[PropertyInformation], |
431 | property_name: &str, |
432 | ) -> Result<PropertyInformation> { |
433 | if let Some(property: &PropertyInformation) = properties.iter().find(|pi: &&PropertyInformation| pi.name == property_name) { |
434 | Ok(property.clone()) |
435 | } else { |
436 | Err(format!("Element has no property with name {property_name}" ).into()) |
437 | } |
438 | } |
439 | |
440 | fn validate_property_expression_type( |
441 | property: &PropertyInformation, |
442 | new_expression_type: Type, |
443 | diag: &mut BuildDiagnostics, |
444 | ) { |
445 | // Check return type match: |
446 | if new_expression_type != i_slint_compiler::langtype::Type::Invalid |
447 | && new_expression_type.to_string() != property.type_name |
448 | { |
449 | diag.push_error_with_span( |
450 | message:format!( |
451 | "return type mismatch in \"{}\" (was: {new_expression_type}, expected: {})" , |
452 | property.name, property.type_name |
453 | ), |
454 | span:i_slint_compiler::diagnostics::SourceLocation { |
455 | source_file: None, |
456 | span: i_slint_compiler::diagnostics::Span::new(offset:0), |
457 | }, |
458 | ); |
459 | } |
460 | } |
461 | |
462 | fn create_workspace_edit_for_set_binding_on_existing_property( |
463 | uri: lsp_types::Url, |
464 | version: SourceFileVersion, |
465 | property: &PropertyInformation, |
466 | new_expression: String, |
467 | ) -> Option<lsp_types::WorkspaceEdit> { |
468 | property.defined_at.as_ref().map(|defined_at: &DefinitionInformation| { |
469 | let edit: TextEdit = |
470 | lsp_types::TextEdit { range: defined_at.expression_range, new_text: new_expression }; |
471 | common::create_workspace_edit(uri, version, edits:vec![edit]) |
472 | }) |
473 | } |
474 | |
475 | fn set_binding_on_existing_property( |
476 | uri: lsp_types::Url, |
477 | version: SourceFileVersion, |
478 | property: &PropertyInformation, |
479 | new_expression: String, |
480 | diag: &mut BuildDiagnostics, |
481 | ) -> Result<(SetBindingResponse, Option<lsp_types::WorkspaceEdit>)> { |
482 | let workspace_edit: Option = (!diag.has_error()) |
483 | .then(|| { |
484 | create_workspace_edit_for_set_binding_on_existing_property( |
485 | uri, |
486 | version, |
487 | property, |
488 | new_expression, |
489 | ) |
490 | }) |
491 | .flatten(); |
492 | |
493 | Ok(( |
494 | SetBindingResponse { diagnostics: diag.iter().map(util::to_lsp_diag).collect::<Vec<_>>() }, |
495 | workspace_edit, |
496 | )) |
497 | } |
498 | |
499 | enum InsertPosition { |
500 | Before, |
501 | After, |
502 | } |
503 | |
504 | fn find_insert_position_relative_to_defined_properties( |
505 | properties: &[PropertyInformation], |
506 | property_name: &str, |
507 | ) -> Option<(lsp_types::Range, InsertPosition)> { |
508 | let mut previous_property: Option<(usize, Position)> = None; |
509 | let mut property_index: usize = usize::MAX; |
510 | |
511 | for (i: usize, p: &PropertyInformation) in properties.iter().enumerate() { |
512 | if p.name == property_name { |
513 | property_index = i; |
514 | } else if let Some(defined_at: &DefinitionInformation) = &p.defined_at { |
515 | if property_index == usize::MAX { |
516 | previous_property = Some((i, defined_at.selection_range.end)); |
517 | } else { |
518 | if let Some((pi: usize, pp: Position)) = previous_property { |
519 | if (i - property_index) >= (property_index - pi) { |
520 | return Some((lsp_types::Range::new(start:pp, end:pp), InsertPosition::After)); |
521 | } |
522 | } |
523 | let p: Position = defined_at.selection_range.start; |
524 | return Some((lsp_types::Range::new(start:p, end:p), InsertPosition::Before)); |
525 | } |
526 | } |
527 | } |
528 | |
529 | None |
530 | } |
531 | |
532 | fn find_insert_range_for_property( |
533 | block_range: &Option<lsp_types::Range>, |
534 | properties: &[PropertyInformation], |
535 | property_name: &str, |
536 | ) -> Option<(lsp_types::Range, InsertPosition)> { |
537 | find_insert_position_relative_to_defined_properties(properties, property_name).or_else(|| { |
538 | // No properties defined yet: |
539 | block_range.map(|r: Range| { |
540 | // Right after the leading `{`... |
541 | let r: Range = lsp_types::Range::new( |
542 | start:lsp_types::Position::new(r.start.line, r.start.character.saturating_add(1)), |
543 | end:lsp_types::Position::new(r.start.line, character:r.start.character.saturating_add(1)), |
544 | ); |
545 | (r, InsertPosition::After) |
546 | }) |
547 | }) |
548 | } |
549 | |
550 | fn create_workspace_edit_for_set_binding_on_known_property( |
551 | uri: lsp_types::Url, |
552 | version: SourceFileVersion, |
553 | element: &common::ElementRcNode, |
554 | properties: &[PropertyInformation], |
555 | property_name: &str, |
556 | new_expression: &str, |
557 | ) -> Option<lsp_types::WorkspaceEdit> { |
558 | let block_range: Option = find_block_range(element); |
559 | |
560 | find_insert_range_for_property(&block_range, properties, property_name).map( |
561 | |(range: Range, insert_type: InsertPosition)| { |
562 | let indent: String = util::find_element_indent(element).unwrap_or_default(); |
563 | let edit: TextEdit = lsp_types::TextEdit { |
564 | range, |
565 | new_text: match insert_type { |
566 | InsertPosition::Before => { |
567 | format!(" {property_name}: {new_expression}; \n{indent} " ) |
568 | } |
569 | InsertPosition::After => { |
570 | format!(" \n{indent} {property_name}: {new_expression};" ) |
571 | } |
572 | }, |
573 | }; |
574 | common::create_workspace_edit(uri, version, edits:vec![edit]) |
575 | }, |
576 | ) |
577 | } |
578 | |
579 | fn set_binding_on_known_property( |
580 | uri: lsp_types::Url, |
581 | version: SourceFileVersion, |
582 | element: &common::ElementRcNode, |
583 | properties: &[PropertyInformation], |
584 | property_name: &str, |
585 | new_expression: &str, |
586 | diag: &mut BuildDiagnostics, |
587 | ) -> Result<(SetBindingResponse, Option<lsp_types::WorkspaceEdit>)> { |
588 | let workspace_edit: Option = if diag.has_error() { |
589 | None |
590 | } else { |
591 | create_workspace_edit_for_set_binding_on_known_property( |
592 | uri, |
593 | version, |
594 | element, |
595 | properties, |
596 | property_name, |
597 | new_expression, |
598 | ) |
599 | }; |
600 | |
601 | Ok(( |
602 | SetBindingResponse { diagnostics: diag.iter().map(util::to_lsp_diag).collect::<Vec<_>>() }, |
603 | workspace_edit, |
604 | )) |
605 | } |
606 | |
607 | pub fn set_binding( |
608 | document_cache: &language::DocumentCache, |
609 | uri: &lsp_types::Url, |
610 | version: SourceFileVersion, |
611 | element: &common::ElementRcNode, |
612 | property_name: &str, |
613 | new_expression: String, |
614 | ) -> Result<(SetBindingResponse, Option<lsp_types::WorkspaceEdit>)> { |
615 | let (mut diag, expression_node) = { |
616 | let mut diagnostics = BuildDiagnostics::default(); |
617 | |
618 | let syntax_node = i_slint_compiler::parser::parse_expression_as_bindingexpression( |
619 | &new_expression, |
620 | &mut diagnostics, |
621 | ); |
622 | |
623 | (diagnostics, syntax_node) |
624 | }; |
625 | |
626 | let new_expression_type = { |
627 | let expr_context_info = element.with_element_node(|node| { |
628 | util::ExpressionContextInfo::new(node.clone(), property_name.to_string(), false) |
629 | }); |
630 | util::with_property_lookup_ctx(&document_cache.documents, &expr_context_info, |ctx| { |
631 | let expression = |
632 | i_slint_compiler::expression_tree::Expression::from_binding_expression_node( |
633 | expression_node, |
634 | ctx, |
635 | ); |
636 | expression.ty() |
637 | }) |
638 | .unwrap_or(Type::Invalid) |
639 | }; |
640 | |
641 | let properties = get_properties(element); |
642 | let property = match get_property_information(&properties, property_name) { |
643 | Ok(p) => p, |
644 | Err(e) => { |
645 | diag.push_error_with_span( |
646 | e.to_string(), |
647 | i_slint_compiler::diagnostics::SourceLocation { |
648 | source_file: None, |
649 | span: i_slint_compiler::diagnostics::Span::new(0), |
650 | }, |
651 | ); |
652 | return Ok(( |
653 | SetBindingResponse { |
654 | diagnostics: diag.iter().map(util::to_lsp_diag).collect::<Vec<_>>(), |
655 | }, |
656 | None, |
657 | )); |
658 | } |
659 | }; |
660 | |
661 | validate_property_expression_type(&property, new_expression_type, &mut diag); |
662 | if property.defined_at.is_some() { |
663 | // Change an already defined property: |
664 | set_binding_on_existing_property(uri.clone(), version, &property, new_expression, &mut diag) |
665 | } else { |
666 | // Add a new definition to a known property: |
667 | set_binding_on_known_property( |
668 | uri.clone(), |
669 | version, |
670 | element, |
671 | &properties, |
672 | &property.name, |
673 | &new_expression, |
674 | &mut diag, |
675 | ) |
676 | } |
677 | } |
678 | |
679 | #[cfg (any(feature = "preview-external" , feature = "preview-engine" ))] |
680 | pub fn set_bindings( |
681 | document_cache: &language::DocumentCache, |
682 | uri: lsp_types::Url, |
683 | version: SourceFileVersion, |
684 | element: &common::ElementRcNode, |
685 | properties: &[crate::common::PropertyChange], |
686 | ) -> Result<(SetBindingResponse, Option<lsp_types::WorkspaceEdit>)> { |
687 | let (responses, edits) = properties |
688 | .iter() |
689 | .map(|p| set_binding(document_cache, &uri, version, element, &p.name, p.value.clone())) |
690 | .fold( |
691 | Ok((SetBindingResponse { diagnostics: Default::default() }, Vec::new())), |
692 | |prev_result: Result<(SetBindingResponse, Vec<lsp_types::TextEdit>)>, next_result| { |
693 | let (mut responses, mut edits) = prev_result?; |
694 | let (nr, ne) = next_result?; |
695 | |
696 | responses.diagnostics.extend_from_slice(&nr.diagnostics); |
697 | |
698 | match ne { |
699 | Some(lsp_types::WorkspaceEdit { |
700 | document_changes: Some(lsp_types::DocumentChanges::Edits(e)), |
701 | .. |
702 | }) => { |
703 | edits.extend(e.first().unwrap().edits.iter().filter_map(|e| match e { |
704 | lsp_types::OneOf::Left(edit) => Some(edit.clone()), |
705 | _ => None, |
706 | })); |
707 | } |
708 | _ => { /* do nothing */ } |
709 | }; |
710 | |
711 | Ok((responses, edits)) |
712 | }, |
713 | )?; |
714 | if edits.is_empty() { |
715 | Ok((responses, None)) |
716 | } else { |
717 | Ok((responses, Some(common::create_workspace_edit(uri, version, edits)))) |
718 | } |
719 | } |
720 | |
721 | #[cfg (any(feature = "preview-external" , feature = "preview-engine" ))] |
722 | fn element_at_source_code_position( |
723 | dc: &mut language::DocumentCache, |
724 | position: &common::VersionedPosition, |
725 | ) -> Result<common::ElementRcNode> { |
726 | let file: PathBuf = lsp_types::Url::to_file_path(position.url()) |
727 | .map_err(|_| "Failed to convert URL to file path" .to_string())?; |
728 | |
729 | if &dc.document_version(target_uri:position.url()) != position.version() { |
730 | return Err("Document version mismatch." .into()); |
731 | } |
732 | |
733 | let doc: &Document = dc.documents.get_document(&file).ok_or_else(|| "Document not found" .to_string())?; |
734 | |
735 | let source_file: Option<&Rc> = doc |
736 | .node |
737 | .as_ref() |
738 | .map(|n| n.source_file.clone()) |
739 | .ok_or_else(|| "Document had no node" .to_string())?; |
740 | let element_position: Position = util::map_position(&source_file, pos:position.offset().into()); |
741 | |
742 | Ok(language::element_at_position(&dc.documents, position.url(), &element_position).ok_or_else( |
743 | || format!("No element found at the given start position {:?}" , &element_position), |
744 | )?) |
745 | } |
746 | |
747 | #[cfg (any(feature = "preview-external" , feature = "preview-engine" ))] |
748 | pub fn update_element_properties( |
749 | ctx: &language::Context, |
750 | position: common::VersionedPosition, |
751 | properties: Vec<common::PropertyChange>, |
752 | ) -> Result<lsp_types::WorkspaceEdit> { |
753 | let element: ElementRcNode = element_at_source_code_position(&mut ctx.document_cache.borrow_mut(), &position)?; |
754 | |
755 | let (_, e: Option) = set_bindings( |
756 | &ctx.document_cache.borrow_mut(), |
757 | uri:position.url().clone(), |
758 | *position.version(), |
759 | &element, |
760 | &properties, |
761 | )?; |
762 | Ok(e.ok_or_else(|| "Failed to create workspace edit" .to_string())?) |
763 | } |
764 | |
765 | fn create_workspace_edit_for_remove_binding( |
766 | uri: lsp_types::Url, |
767 | version: SourceFileVersion, |
768 | range: lsp_types::Range, |
769 | ) -> lsp_types::WorkspaceEdit { |
770 | let edit: TextEdit = lsp_types::TextEdit { range, new_text: String::new() }; |
771 | common::create_workspace_edit(uri.clone(), version, edits:vec![edit]) |
772 | } |
773 | |
774 | pub fn remove_binding( |
775 | uri: lsp_types::Url, |
776 | version: common::UrlVersion, |
777 | element: &common::ElementRcNode, |
778 | property_name: &str, |
779 | ) -> Result<lsp_types::WorkspaceEdit> { |
780 | let source_file = element.with_element_node(|node| node.source_file.clone()); |
781 | |
782 | let range = find_property_binding_offset(element, property_name) |
783 | .and_then(|offset| { |
784 | element.with_element_node(|node| node.token_at_offset(offset.into()).right_biased()) |
785 | }) |
786 | .and_then(|token| { |
787 | for ancestor in token.parent_ancestors() { |
788 | if (ancestor.kind() == SyntaxKind::Binding) |
789 | || (ancestor.kind() == SyntaxKind::PropertyDeclaration) |
790 | { |
791 | let start = { |
792 | let token = left_extend(ancestor.first_token()?); |
793 | let start = token.text_range().start(); |
794 | token |
795 | .prev_token() |
796 | .and_then(|t| { |
797 | if t.kind() == SyntaxKind::Whitespace && t.text().contains(' \n' ) { |
798 | let to_sub = |
799 | t.text().split(' \n' ).last().unwrap_or_default().len() |
800 | as u32; |
801 | start.checked_sub(to_sub.into()) |
802 | } else { |
803 | None |
804 | } |
805 | }) |
806 | .unwrap_or(start) |
807 | }; |
808 | let end = { |
809 | let token = right_extend(ancestor.last_token()?); |
810 | let end = token.text_range().end(); |
811 | token |
812 | .next_token() |
813 | .and_then(|t| { |
814 | if t.kind() == SyntaxKind::Whitespace && t.text().contains(' \n' ) { |
815 | let to_add = |
816 | t.text().split(' \n' ).next().unwrap_or_default().len() |
817 | as u32; |
818 | end.checked_add((to_add + 1/* <cr> */).into()) |
819 | } else { |
820 | None |
821 | } |
822 | }) |
823 | .unwrap_or(end) |
824 | }; |
825 | |
826 | return Some(util::map_range(&source_file, rowan::TextRange::new(start, end))); |
827 | } |
828 | if ancestor.kind() == SyntaxKind::Element { |
829 | // There should have been a binding before the element! |
830 | break; |
831 | } |
832 | } |
833 | None |
834 | }) |
835 | .ok_or_else(|| Into::<common::Error>::into("Could not find range to delete." ))?; |
836 | |
837 | Ok(create_workspace_edit_for_remove_binding(uri, version, range)) |
838 | } |
839 | |
840 | #[cfg (test)] |
841 | mod tests { |
842 | use i_slint_compiler::typeloader::TypeLoader; |
843 | |
844 | use super::*; |
845 | |
846 | use crate::language::test::{complex_document_cache, loaded_document_cache}; |
847 | |
848 | fn find_property<'a>( |
849 | properties: &'a [PropertyInformation], |
850 | name: &'_ str, |
851 | ) -> Option<&'a PropertyInformation> { |
852 | properties.iter().find(|p| p.name == name) |
853 | } |
854 | |
855 | fn properties_at_position_in_cache( |
856 | line: u32, |
857 | character: u32, |
858 | tl: &TypeLoader, |
859 | url: &lsp_types::Url, |
860 | ) -> Option<(common::ElementRcNode, Vec<PropertyInformation>)> { |
861 | let element = |
862 | language::element_at_position(tl, url, &lsp_types::Position { line, character })?; |
863 | Some((element.clone(), get_properties(&element))) |
864 | } |
865 | |
866 | fn properties_at_position( |
867 | line: u32, |
868 | character: u32, |
869 | ) -> Option<( |
870 | common::ElementRcNode, |
871 | Vec<PropertyInformation>, |
872 | language::DocumentCache, |
873 | lsp_types::Url, |
874 | )> { |
875 | let (dc, url, _) = complex_document_cache(); |
876 | if let Some((e, p)) = properties_at_position_in_cache(line, character, &dc.documents, &url) |
877 | { |
878 | Some((e, p, dc, url)) |
879 | } else { |
880 | None |
881 | } |
882 | } |
883 | |
884 | #[test ] |
885 | fn test_get_properties() { |
886 | let (_, result, _, _) = properties_at_position(6, 4).unwrap(); |
887 | |
888 | // Property of element: |
889 | assert_eq!(&find_property(&result, "elapsed-time" ).unwrap().type_name, "duration" ); |
890 | // Property of base type: |
891 | assert_eq!(&find_property(&result, "no-frame" ).unwrap().type_name, "bool" ); |
892 | // reserved properties: |
893 | assert_eq!( |
894 | &find_property(&result, "accessible-role" ).unwrap().type_name, |
895 | "enum AccessibleRole" |
896 | ); |
897 | |
898 | // Poke deeper: |
899 | let (_, result, _, _) = properties_at_position(21, 30).unwrap(); |
900 | let property = find_property(&result, "background" ).unwrap(); |
901 | |
902 | let def_at = property.defined_at.as_ref().unwrap(); |
903 | assert_eq!(def_at.expression_range.end.line, def_at.expression_range.start.line); |
904 | // -1 because the lsp range end location is exclusive. |
905 | assert_eq!( |
906 | (def_at.expression_range.end.character - def_at.expression_range.start.character) |
907 | as usize, |
908 | "lightblue" .len() |
909 | ); |
910 | } |
911 | |
912 | #[test ] |
913 | fn test_element_information() { |
914 | let (dc, url, _) = complex_document_cache(); |
915 | let element = |
916 | language::element_at_position(&dc.documents, &url, &lsp_types::Position::new(33, 4)) |
917 | .unwrap(); |
918 | |
919 | let result = get_element_information(&element); |
920 | |
921 | let r = result.range.unwrap(); |
922 | assert_eq!(r.start.line, 32); |
923 | assert_eq!(r.start.character, 12); |
924 | assert_eq!(r.end.line, 35); |
925 | assert_eq!(r.end.character, 13); |
926 | } |
927 | |
928 | fn delete_range_test( |
929 | content: String, |
930 | pos_l: u32, |
931 | pos_c: u32, |
932 | sl: u32, |
933 | sc: u32, |
934 | el: u32, |
935 | ec: u32, |
936 | ) { |
937 | for (i, l) in content.split(' \n' ).enumerate() { |
938 | println!(" {i:2}: {l}" ); |
939 | } |
940 | println!("-------------------------------------------------------------------" ); |
941 | println!(" : 1 2 3 4 5" ); |
942 | println!(" : 012345678901234567890123456789012345678901234567890123456789" ); |
943 | |
944 | let (dc, url, _) = loaded_document_cache(content); |
945 | |
946 | let (_, result) = |
947 | properties_at_position_in_cache(pos_l, pos_c, &dc.documents, &url).unwrap(); |
948 | |
949 | let p = find_property(&result, "text" ).unwrap(); |
950 | let definition = p.defined_at.as_ref().unwrap(); |
951 | |
952 | assert_eq!(&definition.expression_value, " \"text \"" ); |
953 | |
954 | println!("Actual: (l: {}, c: {}) - (l: {}, c: {}) --- Expected: (l: {sl}, c: {sc}) - (l: {el}, c: {ec})" , |
955 | definition.selection_range.start.line, |
956 | definition.selection_range.start.character, |
957 | definition.selection_range.end.line, |
958 | definition.selection_range.end.character, |
959 | ); |
960 | |
961 | assert_eq!(definition.selection_range.start.line, sl); |
962 | assert_eq!(definition.selection_range.start.character, sc); |
963 | assert_eq!(definition.selection_range.end.line, el); |
964 | assert_eq!(definition.selection_range.end.character, ec); |
965 | } |
966 | |
967 | #[test ] |
968 | fn test_get_property_delete_range_no_extend() { |
969 | delete_range_test( |
970 | r#"import { VerticalBox } from "std-widgets.slint"; |
971 | |
972 | component MainWindow inherits Window { |
973 | VerticalBox { |
974 | Text { text: "text"; } |
975 | } |
976 | } |
977 | "# |
978 | .to_string(), |
979 | 4, |
980 | 12, |
981 | 4, |
982 | 15, |
983 | 4, |
984 | 28, |
985 | ); |
986 | } |
987 | |
988 | #[test ] |
989 | fn test_get_property_delete_range_line_extend_left_extra_indent() { |
990 | delete_range_test( |
991 | r#"import { VerticalBox } from "std-widgets.slint"; |
992 | |
993 | component MainWindow inherits Window { |
994 | VerticalBox { |
995 | Text { |
996 | // Cut |
997 | text: "text"; |
998 | } |
999 | } |
1000 | } |
1001 | "# |
1002 | .to_string(), |
1003 | 4, |
1004 | 12, |
1005 | 5, |
1006 | 14, |
1007 | 6, |
1008 | 25, |
1009 | ); |
1010 | } |
1011 | |
1012 | #[test ] |
1013 | fn test_get_property_delete_range_line_extend_left_no_ws() { |
1014 | delete_range_test( |
1015 | r#"import { VerticalBox } from "std-widgets.slint"; |
1016 | |
1017 | component MainWindow inherits Window { |
1018 | VerticalBox { |
1019 | Text { |
1020 | /* Cut */text: "text"; |
1021 | } |
1022 | } |
1023 | } |
1024 | "# |
1025 | .to_string(), |
1026 | 4, |
1027 | 12, |
1028 | 5, |
1029 | 12, |
1030 | 5, |
1031 | 34, |
1032 | ); |
1033 | } |
1034 | |
1035 | #[test ] |
1036 | fn test_get_property_delete_range_extend_left_to_empty_line() { |
1037 | delete_range_test( |
1038 | r#"import { VerticalBox } from "std-widgets.slint"; |
1039 | |
1040 | component MainWindow inherits Window { |
1041 | VerticalBox { |
1042 | Text { |
1043 | font-size: 12px; |
1044 | // Keep |
1045 | |
1046 | // Cut |
1047 | text: "text"; |
1048 | } |
1049 | } |
1050 | } |
1051 | "# |
1052 | .to_string(), |
1053 | 4, |
1054 | 12, |
1055 | 8, |
1056 | 12, |
1057 | 9, |
1058 | 25, |
1059 | ); |
1060 | } |
1061 | |
1062 | #[test ] |
1063 | fn test_get_property_delete_range_extend_left_many_lines() { |
1064 | delete_range_test( |
1065 | r#"import { VerticalBox } from "std-widgets.slint"; |
1066 | |
1067 | component MainWindow inherits Window { |
1068 | VerticalBox { |
1069 | Text { |
1070 | font-size: 12px; |
1071 | // Keep |
1072 | |
1073 | // Cut |
1074 | // Cut |
1075 | // Cut |
1076 | // Cut |
1077 | // Cut |
1078 | // Cut |
1079 | // Cut |
1080 | // Cut |
1081 | // Cut |
1082 | // Cut |
1083 | // Cut |
1084 | text: "text"; |
1085 | } |
1086 | } |
1087 | } |
1088 | "# |
1089 | .to_string(), |
1090 | 4, |
1091 | 12, |
1092 | 8, |
1093 | 12, |
1094 | 19, |
1095 | 25, |
1096 | ); |
1097 | } |
1098 | |
1099 | #[test ] |
1100 | fn test_get_property_delete_range_extend_left_multiline_comment() { |
1101 | delete_range_test( |
1102 | r#"import { VerticalBox } from "std-widgets.slint"; |
1103 | |
1104 | component MainWindow inherits Window { |
1105 | VerticalBox { |
1106 | Text { |
1107 | font-size: 12px; |
1108 | // Keep |
1109 | |
1110 | /* Cut |
1111 | Cut |
1112 | /* Cut |
1113 | --- Cut */ |
1114 | |
1115 | // Cut |
1116 | // Cut */ |
1117 | text: "text"; |
1118 | } |
1119 | } |
1120 | } |
1121 | "# |
1122 | .to_string(), |
1123 | 4, |
1124 | 12, |
1125 | 8, |
1126 | 12, |
1127 | 15, |
1128 | 25, |
1129 | ); |
1130 | } |
1131 | |
1132 | #[test ] |
1133 | fn test_get_property_delete_range_extend_left_un_indented_property() { |
1134 | delete_range_test( |
1135 | r#"import { VerticalBox } from "std-widgets.slint"; |
1136 | |
1137 | component MainWindow inherits Window { |
1138 | VerticalBox { |
1139 | Text { |
1140 | font-size: 12px; |
1141 | |
1142 | /* Cut |
1143 | Cut |
1144 | |
1145 | /* Cut |
1146 | --- Cut */ |
1147 | Cut */ |
1148 | // Cut |
1149 | // Cut |
1150 | text: "text"; |
1151 | } |
1152 | } |
1153 | } |
1154 | "# |
1155 | .to_string(), |
1156 | 4, |
1157 | 12, |
1158 | 7, |
1159 | 8, |
1160 | 15, |
1161 | 13, |
1162 | ); |
1163 | } |
1164 | |
1165 | #[test ] |
1166 | fn test_get_property_delete_range_extend_left_leading_line_comment() { |
1167 | delete_range_test( |
1168 | r#"import { VerticalBox } from "std-widgets.slint"; |
1169 | |
1170 | component MainWindow inherits Window { |
1171 | VerticalBox { |
1172 | Text { |
1173 | font-size: 12px; |
1174 | // Cut |
1175 | /* Cut |
1176 | Cut |
1177 | |
1178 | /* Cut |
1179 | --- Cut */ |
1180 | Cut */ |
1181 | // Cut |
1182 | // Cut |
1183 | /* cut */ text: "text"; |
1184 | } |
1185 | } |
1186 | } |
1187 | "# |
1188 | .to_string(), |
1189 | 4, |
1190 | 12, |
1191 | 6, |
1192 | 10, |
1193 | 15, |
1194 | 35, |
1195 | ); |
1196 | } |
1197 | |
1198 | #[test ] |
1199 | fn test_get_property_delete_range_right_extend() { |
1200 | delete_range_test( |
1201 | r#"import { VerticalBox } from "std-widgets.slint"; |
1202 | |
1203 | component MainWindow inherits Window { |
1204 | VerticalBox { |
1205 | Text { |
1206 | text: "text"; // Cut |
1207 | // Keep |
1208 | } |
1209 | } |
1210 | } |
1211 | "# |
1212 | .to_string(), |
1213 | 4, |
1214 | 12, |
1215 | 5, |
1216 | 12, |
1217 | 5, |
1218 | 32, |
1219 | ); |
1220 | } |
1221 | |
1222 | #[test ] |
1223 | fn test_get_property_delete_range_right_extend_to_line_break() { |
1224 | delete_range_test( |
1225 | r#"import { VerticalBox } from "std-widgets.slint"; |
1226 | |
1227 | component MainWindow inherits Window { |
1228 | VerticalBox { |
1229 | Text { |
1230 | text: "text"; /* Cut |
1231 | // Cut |
1232 | Cut |
1233 | * Cut */ |
1234 | |
1235 | // Keep |
1236 | font-size: 12px; |
1237 | } |
1238 | } |
1239 | } |
1240 | "# |
1241 | .to_string(), |
1242 | 4, |
1243 | 12, |
1244 | 5, |
1245 | 12, |
1246 | 8, |
1247 | 27, |
1248 | ); |
1249 | } |
1250 | |
1251 | #[test ] |
1252 | fn test_get_property_delete_range_no_right_extend() { |
1253 | delete_range_test( |
1254 | r#"import { VerticalBox } from "std-widgets.slint"; |
1255 | |
1256 | component MainWindow { |
1257 | VerticalBox { |
1258 | Text { |
1259 | text: "text";/*Keep*/ font_size: 12px; |
1260 | } |
1261 | } |
1262 | } |
1263 | "# |
1264 | .to_string(), |
1265 | 4, |
1266 | 12, |
1267 | 5, |
1268 | 12, |
1269 | 5, |
1270 | 25, |
1271 | ); |
1272 | } |
1273 | |
1274 | #[test ] |
1275 | fn test_get_property_delete_range_no_right_extend_with_ws() { |
1276 | delete_range_test( |
1277 | r#"import { VerticalBox } from "std-widgets.slint"; |
1278 | |
1279 | component MainWindow { |
1280 | VerticalBox { |
1281 | Text { |
1282 | text: "text"; /*Keep*/ font_size: 12px; |
1283 | } |
1284 | } |
1285 | } |
1286 | "# |
1287 | .to_string(), |
1288 | 4, |
1289 | 12, |
1290 | 5, |
1291 | 12, |
1292 | 5, |
1293 | 25, |
1294 | ); |
1295 | } |
1296 | |
1297 | #[test ] |
1298 | fn test_get_property_delete_range_right_extend_to_rbrace() { |
1299 | delete_range_test( |
1300 | r#"import { VerticalBox } from "std-widgets.slint"; |
1301 | |
1302 | component MainWindow { |
1303 | VerticalBox { |
1304 | Text { text: "text";/* Cut */} |
1305 | } |
1306 | } |
1307 | } |
1308 | "# |
1309 | .to_string(), |
1310 | 4, |
1311 | 12, |
1312 | 4, |
1313 | 15, |
1314 | 4, |
1315 | 37, |
1316 | ); |
1317 | } |
1318 | |
1319 | #[test ] |
1320 | fn test_get_property_delete_range_right_extend_to_rbrace_ws() { |
1321 | delete_range_test( |
1322 | r#"import { VerticalBox } from "std-widgets.slint"; |
1323 | |
1324 | component MainWindow inherits Window { |
1325 | VerticalBox { |
1326 | Text { text: "text"; /* Cut */ /* Cut */ } |
1327 | } |
1328 | } |
1329 | } |
1330 | "# |
1331 | .to_string(), |
1332 | 4, |
1333 | 12, |
1334 | 4, |
1335 | 15, |
1336 | 4, |
1337 | 53, |
1338 | ); |
1339 | } |
1340 | |
1341 | #[test ] |
1342 | fn test_get_property_definition() { |
1343 | let (dc, url, _) = loaded_document_cache( |
1344 | r#"import { LineEdit, Button, Slider, HorizontalBox, VerticalBox } from "std-widgets.slint"; |
1345 | |
1346 | component Base1 { |
1347 | in-out property<int> foo = 42; |
1348 | } |
1349 | |
1350 | component Base2 inherits Base1 { |
1351 | foo: 23; |
1352 | } |
1353 | |
1354 | component MainWindow inherits Window { |
1355 | property <duration> total-time: slider.value * 1s; |
1356 | property <duration> elapsed-time; |
1357 | |
1358 | callback tick(duration); |
1359 | tick(passed-time) => { |
1360 | elapsed-time += passed-time; |
1361 | elapsed-time = min(elapsed-time, total-time); |
1362 | } |
1363 | |
1364 | VerticalBox { |
1365 | HorizontalBox { |
1366 | padding-left: 0; |
1367 | Text { text: "Elapsed Time:"; } |
1368 | base2 := Base2 { |
1369 | foo: 15; |
1370 | min-width: 200px; |
1371 | max-height: 30px; |
1372 | background: gray; |
1373 | Rectangle { |
1374 | height: 100%; |
1375 | width: parent.width * (elapsed-time/total-time); |
1376 | background: lightblue; |
1377 | } |
1378 | } |
1379 | } |
1380 | Text{ |
1381 | text: (total-time / 1s) + "s"; |
1382 | } |
1383 | HorizontalBox { |
1384 | padding-left: 0; |
1385 | Text { |
1386 | text: "Duration:"; |
1387 | vertical-alignment: center; |
1388 | } |
1389 | slider := Slider { |
1390 | maximum: 30s / 1s; |
1391 | value: 10s / 1s; |
1392 | changed(new-duration) => { |
1393 | root.total-time = new-duration * 1s; |
1394 | root.elapsed-time = min(root.elapsed-time, root.total-time); |
1395 | } |
1396 | } |
1397 | } |
1398 | Button { |
1399 | text: "Reset"; |
1400 | clicked => { |
1401 | elapsed-time = 0 |
1402 | } |
1403 | } |
1404 | } |
1405 | } |
1406 | "# .to_string()); |
1407 | let file_url = url.clone(); |
1408 | |
1409 | let doc = dc.documents.get_document(&crate::language::uri_to_file(&url).unwrap()).unwrap(); |
1410 | let source = &doc.node.as_ref().unwrap().source_file; |
1411 | let (l, c) = source.line_column(source.source().unwrap().find("base2 :=" ).unwrap()); |
1412 | let (_, result) = |
1413 | properties_at_position_in_cache(l as u32, c as u32, &dc.documents, &url).unwrap(); |
1414 | |
1415 | let foo_property = find_property(&result, "foo" ).unwrap(); |
1416 | |
1417 | assert_eq!(foo_property.type_name, "int" ); |
1418 | |
1419 | let declaration = foo_property.declared_at.as_ref().unwrap(); |
1420 | assert_eq!(declaration.uri, file_url); |
1421 | assert_eq!(declaration.start_position.line, 3); |
1422 | assert_eq!(declaration.start_position.character, 20); // This should probably point to the start of |
1423 | // `property<int> foo = 42`, not to the `<` |
1424 | assert_eq!(foo_property.group, "Base1" ); |
1425 | } |
1426 | |
1427 | #[test ] |
1428 | fn test_invalid_properties() { |
1429 | let (dc, url, _) = loaded_document_cache( |
1430 | r#" |
1431 | global SomeGlobal := { |
1432 | property <int> glob: 77; |
1433 | } |
1434 | |
1435 | component SomeRect inherits Rectangle { |
1436 | component foo inherits InvalidType { |
1437 | property <int> abcd: 41; |
1438 | width: 45px; |
1439 | } |
1440 | } |
1441 | "# |
1442 | .to_string(), |
1443 | ); |
1444 | |
1445 | let (_, result) = properties_at_position_in_cache(1, 25, &dc.documents, &url).unwrap(); |
1446 | |
1447 | let glob_property = find_property(&result, "glob" ).unwrap(); |
1448 | assert_eq!(glob_property.type_name, "int" ); |
1449 | let declaration = glob_property.declared_at.as_ref().unwrap(); |
1450 | assert_eq!(declaration.uri, url); |
1451 | assert_eq!(declaration.start_position.line, 2); |
1452 | assert_eq!(glob_property.group, "" ); |
1453 | assert_eq!(find_property(&result, "width" ), None); |
1454 | |
1455 | let (_, result) = properties_at_position_in_cache(8, 4, &dc.documents, &url).unwrap(); |
1456 | let abcd_property = find_property(&result, "abcd" ).unwrap(); |
1457 | assert_eq!(abcd_property.type_name, "int" ); |
1458 | let declaration = abcd_property.declared_at.as_ref().unwrap(); |
1459 | assert_eq!(declaration.uri, url); |
1460 | assert_eq!(declaration.start_position.line, 7); |
1461 | assert_eq!(abcd_property.group, "" ); |
1462 | |
1463 | let x_property = find_property(&result, "x" ).unwrap(); |
1464 | assert_eq!(x_property.type_name, "length" ); |
1465 | assert_eq!(x_property.defined_at, None); |
1466 | assert_eq!(x_property.group, "geometry" ); |
1467 | |
1468 | let width_property = find_property(&result, "width" ).unwrap(); |
1469 | assert_eq!(width_property.type_name, "length" ); |
1470 | let definition = width_property.defined_at.as_ref().unwrap(); |
1471 | assert_eq!(definition.expression_range.start.line, 8); |
1472 | assert_eq!(width_property.group, "geometry" ); |
1473 | } |
1474 | |
1475 | #[test ] |
1476 | fn test_invalid_property_panic() { |
1477 | let (dc, url, _) = |
1478 | loaded_document_cache(r#"export component Demo { Text { text: } }"# .to_string()); |
1479 | |
1480 | let (_, result) = properties_at_position_in_cache(0, 35, &dc.documents, &url).unwrap(); |
1481 | |
1482 | let prop = find_property(&result, "text" ).unwrap(); |
1483 | assert_eq!(prop.defined_at, None); // The property has no valid definition at this time |
1484 | } |
1485 | |
1486 | #[test ] |
1487 | fn test_codeblock_property_declaration() { |
1488 | let (dc, url, _) = loaded_document_cache( |
1489 | r#" |
1490 | component Base { |
1491 | property <int> a1: { 1 + 1 } |
1492 | property <int> a2: { 1 + 2; } |
1493 | property <int> a3: { 1 + 3 }; |
1494 | property <int> a4: { 1 + 4; }; |
1495 | in property <int> b: { |
1496 | if (something) { return 42; } |
1497 | return 1 + 2; |
1498 | } |
1499 | } |
1500 | "# |
1501 | .to_string(), |
1502 | ); |
1503 | |
1504 | let (_, result) = properties_at_position_in_cache(3, 0, &dc.documents, &url).unwrap(); |
1505 | assert_eq!(find_property(&result, "a1" ).unwrap().type_name, "int" ); |
1506 | assert_eq!( |
1507 | find_property(&result, "a1" ).unwrap().defined_at.as_ref().unwrap().expression_value, |
1508 | "{ 1 + 1 }" |
1509 | ); |
1510 | assert_eq!(find_property(&result, "a2" ).unwrap().type_name, "int" ); |
1511 | assert_eq!( |
1512 | find_property(&result, "a2" ).unwrap().defined_at.as_ref().unwrap().expression_value, |
1513 | "{ 1 + 2; }" |
1514 | ); |
1515 | assert_eq!(find_property(&result, "a3" ).unwrap().type_name, "int" ); |
1516 | assert_eq!( |
1517 | find_property(&result, "a3" ).unwrap().defined_at.as_ref().unwrap().expression_value, |
1518 | "{ 1 + 3 }" |
1519 | ); |
1520 | assert_eq!(find_property(&result, "a4" ).unwrap().type_name, "int" ); |
1521 | assert_eq!( |
1522 | find_property(&result, "a4" ).unwrap().defined_at.as_ref().unwrap().expression_value, |
1523 | "{ 1 + 4; }" |
1524 | ); |
1525 | assert_eq!(find_property(&result, "b" ).unwrap().type_name, "int" ); |
1526 | assert_eq!( |
1527 | find_property(&result, "b" ).unwrap().defined_at.as_ref().unwrap().expression_value, |
1528 | "{ \n if (something) { return 42; } \n return 1 + 2; \n }" |
1529 | ); |
1530 | } |
1531 | |
1532 | #[test ] |
1533 | fn test_codeblock_property_definitions() { |
1534 | let (dc, url, _) = loaded_document_cache( |
1535 | r#" |
1536 | component Base { |
1537 | in property <int> a1; |
1538 | in property <int> a2; |
1539 | in property <int> a3; |
1540 | in property <int> a4; |
1541 | in property <int> b; |
1542 | } |
1543 | component MyComp { |
1544 | Base { |
1545 | a1: { 1 + 1 } |
1546 | a2: { 1 + 2; } |
1547 | a3: { 1 + 3 }; |
1548 | a4: { 1 + 4; }; |
1549 | b: { |
1550 | if (something) { return 42; } |
1551 | return 1 + 2; |
1552 | } |
1553 | } |
1554 | } |
1555 | "# |
1556 | .to_string(), |
1557 | ); |
1558 | |
1559 | let (_, result) = properties_at_position_in_cache(11, 1, &dc.documents, &url).unwrap(); |
1560 | assert_eq!(find_property(&result, "a1" ).unwrap().type_name, "int" ); |
1561 | assert_eq!( |
1562 | find_property(&result, "a1" ).unwrap().defined_at.as_ref().unwrap().expression_value, |
1563 | "{ 1 + 1 }" |
1564 | ); |
1565 | assert_eq!(find_property(&result, "a2" ).unwrap().type_name, "int" ); |
1566 | assert_eq!( |
1567 | find_property(&result, "a2" ).unwrap().defined_at.as_ref().unwrap().expression_value, |
1568 | "{ 1 + 2; }" |
1569 | ); |
1570 | assert_eq!(find_property(&result, "a3" ).unwrap().type_name, "int" ); |
1571 | assert_eq!( |
1572 | find_property(&result, "a3" ).unwrap().defined_at.as_ref().unwrap().expression_value, |
1573 | "{ 1 + 3 }" |
1574 | ); |
1575 | assert_eq!(find_property(&result, "a4" ).unwrap().type_name, "int" ); |
1576 | assert_eq!( |
1577 | find_property(&result, "a4" ).unwrap().defined_at.as_ref().unwrap().expression_value, |
1578 | "{ 1 + 4; }" |
1579 | ); |
1580 | assert_eq!(find_property(&result, "b" ).unwrap().type_name, "int" ); |
1581 | assert_eq!( |
1582 | find_property(&result, "b" ).unwrap().defined_at.as_ref().unwrap().expression_value, |
1583 | "{ \n if (something) { return 42; } \n return 1 + 2; \n }" , |
1584 | ); |
1585 | } |
1586 | |
1587 | #[test ] |
1588 | fn test_output_properties() { |
1589 | let (dc, url, _) = loaded_document_cache( |
1590 | r#" |
1591 | component Base { |
1592 | property <int> a: 1; |
1593 | in property <int> b: 2; |
1594 | out property <int> c: 3; |
1595 | in-out property <int> d: 4; |
1596 | } |
1597 | |
1598 | component MyComp { |
1599 | Base { |
1600 | |
1601 | } |
1602 | TouchArea { |
1603 | |
1604 | } |
1605 | } |
1606 | "# |
1607 | .to_string(), |
1608 | ); |
1609 | |
1610 | let (_, result) = properties_at_position_in_cache(3, 0, &dc.documents, &url).unwrap(); |
1611 | assert_eq!(find_property(&result, "a" ).unwrap().type_name, "int" ); |
1612 | assert_eq!(find_property(&result, "b" ).unwrap().type_name, "int" ); |
1613 | assert_eq!(find_property(&result, "c" ).unwrap().type_name, "int" ); |
1614 | assert_eq!(find_property(&result, "d" ).unwrap().type_name, "int" ); |
1615 | |
1616 | let (_, result) = properties_at_position_in_cache(10, 0, &dc.documents, &url).unwrap(); |
1617 | assert_eq!(find_property(&result, "a" ), None); |
1618 | assert_eq!(find_property(&result, "b" ).unwrap().type_name, "int" ); |
1619 | assert_eq!(find_property(&result, "c" ), None); |
1620 | assert_eq!(find_property(&result, "d" ).unwrap().type_name, "int" ); |
1621 | |
1622 | let (_, result) = properties_at_position_in_cache(13, 0, &dc.documents, &url).unwrap(); |
1623 | assert_eq!(find_property(&result, "enabled" ).unwrap().type_name, "bool" ); |
1624 | assert_eq!(find_property(&result, "pressed" ), None); |
1625 | } |
1626 | |
1627 | fn set_binding_helper( |
1628 | property_name: &str, |
1629 | new_value: &str, |
1630 | ) -> (SetBindingResponse, Option<lsp_types::WorkspaceEdit>) { |
1631 | let (element, _, dc, url) = properties_at_position(18, 15).unwrap(); |
1632 | set_binding(&dc, &url, None, &element, property_name, new_value.to_string()).unwrap() |
1633 | } |
1634 | |
1635 | #[test ] |
1636 | fn test_set_binding_valid_expression_unknown_property() { |
1637 | let (result, edit) = set_binding_helper("foobar" , "1 + 2" ); |
1638 | |
1639 | assert_eq!(edit, None); |
1640 | assert_eq!(result.diagnostics.len(), 1_usize); |
1641 | assert_eq!(result.diagnostics[0].severity, Some(lsp_types::DiagnosticSeverity::ERROR)); |
1642 | assert!(result.diagnostics[0].message.contains("no property" )); |
1643 | } |
1644 | |
1645 | #[test ] |
1646 | fn test_set_binding_valid_expression_undefined_property() { |
1647 | let (result, edit) = set_binding_helper("x" , "30px" ); |
1648 | |
1649 | let edit = edit.unwrap(); |
1650 | let dcs = if let Some(lsp_types::DocumentChanges::Edits(e)) = &edit.document_changes { |
1651 | e |
1652 | } else { |
1653 | unreachable!(); |
1654 | }; |
1655 | assert_eq!(dcs.len(), 1_usize); |
1656 | |
1657 | let tcs = &dcs[0].edits; |
1658 | assert_eq!(tcs.len(), 1_usize); |
1659 | |
1660 | let tc = if let lsp_types::OneOf::Left(tc) = &tcs[0] { |
1661 | tc |
1662 | } else { |
1663 | unreachable!(); |
1664 | }; |
1665 | assert_eq!(&tc.new_text, "x: 30px; \n " ); |
1666 | assert_eq!(tc.range.start, lsp_types::Position { line: 17, character: 16 }); |
1667 | assert_eq!(tc.range.end, lsp_types::Position { line: 17, character: 16 }); |
1668 | |
1669 | assert_eq!(result.diagnostics.len(), 0_usize); |
1670 | } |
1671 | |
1672 | #[test ] |
1673 | fn test_set_binding_valid_expression_wrong_return_type() { |
1674 | let (result, edit) = set_binding_helper("min-width" , " \"test \"" ); |
1675 | |
1676 | assert_eq!(edit, None); |
1677 | assert_eq!(result.diagnostics.len(), 1_usize); |
1678 | assert_eq!(result.diagnostics[0].severity, Some(lsp_types::DiagnosticSeverity::ERROR)); |
1679 | assert!(result.diagnostics[0].message.contains("return type mismatch" )); |
1680 | } |
1681 | |
1682 | #[test ] |
1683 | fn test_set_binding_invalid_expression() { |
1684 | let (result, edit) = set_binding_helper("min-width" , "?=///1 + 2" ); |
1685 | |
1686 | assert_eq!(edit, None); |
1687 | assert_eq!(result.diagnostics.len(), 1_usize); |
1688 | assert_eq!(result.diagnostics[0].severity, Some(lsp_types::DiagnosticSeverity::ERROR)); |
1689 | assert!(result.diagnostics[0].message.contains("invalid expression" )); |
1690 | } |
1691 | |
1692 | #[test ] |
1693 | fn test_set_binding_trailing_garbage() { |
1694 | let (result, edit) = set_binding_helper("min-width" , "1px;" ); |
1695 | |
1696 | assert_eq!(edit, None); |
1697 | assert_eq!(result.diagnostics.len(), 1_usize); |
1698 | assert_eq!(result.diagnostics[0].severity, Some(lsp_types::DiagnosticSeverity::ERROR)); |
1699 | assert!(result.diagnostics[0].message.contains("end of string" )); |
1700 | } |
1701 | |
1702 | #[test ] |
1703 | fn test_set_binding_valid() { |
1704 | let (result, edit) = set_binding_helper("min-width" , "5px" ); |
1705 | |
1706 | let edit = edit.unwrap(); |
1707 | let dcs = if let Some(lsp_types::DocumentChanges::Edits(e)) = &edit.document_changes { |
1708 | e |
1709 | } else { |
1710 | unreachable!(); |
1711 | }; |
1712 | assert_eq!(dcs.len(), 1_usize); |
1713 | |
1714 | let tcs = &dcs[0].edits; |
1715 | assert_eq!(tcs.len(), 1_usize); |
1716 | |
1717 | let tc = if let lsp_types::OneOf::Left(tc) = &tcs[0] { |
1718 | tc |
1719 | } else { |
1720 | unreachable!(); |
1721 | }; |
1722 | assert_eq!(&tc.new_text, "5px" ); |
1723 | assert_eq!(tc.range.start, lsp_types::Position { line: 17, character: 27 }); |
1724 | assert_eq!(tc.range.end, lsp_types::Position { line: 17, character: 32 }); |
1725 | |
1726 | assert_eq!(result.diagnostics.len(), 0_usize); |
1727 | } |
1728 | } |
1729 | |