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