1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
3
4use crate::common::{self, Result};
5use crate::language;
6use crate::util;
7
8use i_slint_compiler::diagnostics::{BuildDiagnostics, SourceFileVersion, Spanned};
9use i_slint_compiler::langtype::{ElementType, Type};
10use i_slint_compiler::object_tree::{Element, PropertyDeclaration, PropertyVisibility};
11use i_slint_compiler::parser::{syntax_nodes, Language, SyntaxKind};
12
13use std::collections::HashSet;
14
15#[cfg(target_arch = "wasm32")]
16use crate::wasm_prelude::*;
17
18#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq)]
19pub(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)]
27pub(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)]
33pub(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)]
42pub(crate) struct ElementInformation {
43 id: String,
44 type_name: String,
45 range: Option<lsp_types::Range>,
46}
47
48#[derive(serde::Deserialize, serde::Serialize)]
49pub(crate) struct QueryPropertyResponse {
50 properties: Vec<PropertyInformation>,
51 element: Option<ElementInformation>,
52 source_uri: String,
53 source_version: i32,
54}
55
56impl 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)]
63pub struct SetBindingResponse {
64 diagnostics: Vec<lsp_types::Diagnostic>,
65}
66
67// This gets defined accessibility properties...
68fn 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
81fn 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
99fn 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.
126fn 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.
156fn 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
191fn 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
236fn 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
259fn 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
272fn 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(&current_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
397fn 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
409fn 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
416pub(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
429fn 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
440fn 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
462fn 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
475fn 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
499enum InsertPosition {
500 Before,
501 After,
502}
503
504fn 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
532fn 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
550fn 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
579fn 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
607pub 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"))]
680pub 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"))]
722fn 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"))]
748pub 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
765fn 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
774pub 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)]
841mod 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
972component 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
993component 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
1017component 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
1040component 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
1067component 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
1104component 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
1137component 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
1150text: "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
1170component 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
1203component 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
1227component 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
1256component 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
1279component 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
1302component 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
1324component 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
1346component Base1 {
1347 in-out property<int> foo = 42;
1348}
1349
1350component Base2 inherits Base1 {
1351 foo: 23;
1352}
1353
1354component 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#"
1431global SomeGlobal := {
1432 property <int> glob: 77;
1433}
1434
1435component 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#"
1490component 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#"
1536component 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}
1543component 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#"
1591component 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
1598component 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