diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index b1b9e7247..a3d6e1b68 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -1172,10 +1172,10 @@ fn prepare_scene( } dirty_region = (renderer.dirty_region.to_rect().cast() * factor) - .round_out() - .intersection(&euclid::rect(0., 0., i16::MAX as f32, i16::MAX as f32)) - .unwrap_or_default() - .cast(); + .round_out() + .intersection(&euclid::rect(0., 0., i16::MAX as f32, i16::MAX as f32)) + .unwrap_or_default() + .cast(); dirty_region = software_renderer.apply_dirty_region(dirty_region, size); diff --git a/tools/lsp/common.rs b/tools/lsp/common.rs index f4f50ef9f..05a21ea0a 100644 --- a/tools/lsp/common.rs +++ b/tools/lsp/common.rs @@ -5,6 +5,7 @@ use i_slint_compiler::diagnostics::{SourceFile, SourceFileVersion}; use i_slint_compiler::object_tree::ElementRc; +use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxNode}; use lsp_types::{TextEdit, Url, WorkspaceEdit}; use std::{collections::HashMap, path::PathBuf}; @@ -16,6 +17,17 @@ pub type UrlVersion = Option; #[cfg(target_arch = "wasm32")] use crate::wasm_prelude::*; +pub fn extract_element(node: SyntaxNode) -> Option { + match node.kind() { + SyntaxKind::Element => Some(node.into()), + SyntaxKind::SubElement => extract_element(node.child_node(SyntaxKind::Element)?), + SyntaxKind::ConditionalElement | SyntaxKind::RepeatedElement => { + extract_element(node.child_node(SyntaxKind::SubElement)?) + } + _ => None, + } +} + #[derive(Clone)] pub struct ElementRcNode { pub element: ElementRc, @@ -100,6 +112,46 @@ impl ElementRcNode { pub fn as_element(&self) -> &ElementRc { &self.element } + + pub fn parent(&self, root_element: ElementRc) -> Option { + let parent = self.with_element_node(|node| { + let mut ancestor = node.parent()?; + loop { + if ancestor.kind() == SyntaxKind::Element { + return Some(ancestor); + } + ancestor = ancestor.parent()?; + } + })?; + + let (parent_path, parent_offset) = + (parent.source_file.path().to_owned(), u32::from(parent.text_range().start())); + Self::find_in_or_below(root_element, &parent_path, parent_offset) + } + + pub fn children(&self) -> Vec { + self.with_element_node(|node| { + let mut children = Vec::new(); + for c in node.children() { + if let Some(element) = extract_element(c.clone()) { + let e_path = element.source_file.path().to_path_buf(); + let e_offset = u32::from(element.text_range().start()); + + let Some(child_node) = ElementRcNode::find_in_or_below( + self.as_element().clone(), + &e_path, + e_offset, + ) else { + continue; + }; + + children.push(child_node); + } + } + + children + }) + } } pub fn create_workspace_edit( diff --git a/tools/lsp/preview.rs b/tools/lsp/preview.rs index 9ca076bd2..e3b12c948 100644 --- a/tools/lsp/preview.rs +++ b/tools/lsp/preview.rs @@ -6,7 +6,7 @@ use crate::lsp_ext::Health; use crate::preview::element_selection::ElementSelection; use crate::util; use i_slint_compiler::object_tree::ElementRc; -use i_slint_compiler::parser::{syntax_nodes::Element, SyntaxKind}; +use i_slint_compiler::parser::{syntax_nodes, SyntaxKind}; use i_slint_core::component_factory::FactoryContext; use i_slint_core::lengths::{LogicalLength, LogicalPoint}; use i_slint_core::model::VecModel; @@ -168,6 +168,22 @@ fn drop_component(component_type: slint::SharedString, x: f32, y: f32) { }; } +fn placeholder_node_text(selected: &common::ElementRcNode) -> String { + let Some(component_instance) = component_instance() else { + return Default::default(); + }; + let root_element = element_selection::root_element(&component_instance); + let Some(parent) = selected.parent(root_element) else { + return Default::default(); + }; + + if parent.layout_kind() != ui::LayoutKind::None && parent.children().len() == 1 { + return format!("Rectangle {{ /* {} */ }}", NODE_IGNORE_COMMENT); + } + + Default::default() +} + // triggered from the UI, running in UI thread fn delete_selected_element() { let Some(selected) = selected_element() else { @@ -183,24 +199,26 @@ fn delete_selected_element() { return; }; - let Some(range) = selected.as_element_node().and_then(|en| { - en.with_element_node(|n| { - if let Some(parent) = &n.parent() { - if parent.kind() == SyntaxKind::SubElement { - return util::map_node(parent); - } + let Some(selected_node) = selected.as_element_node() else { + return; + }; + + let Some(range) = selected_node.with_element_node(|n| { + if let Some(parent) = &n.parent() { + if parent.kind() == SyntaxKind::SubElement { + return util::map_node(parent); } - util::map_node(n) - }) + } + util::map_node(n) }) else { return; }; - let edit = common::create_workspace_edit( - url, - version, - vec![lsp_types::TextEdit { range, new_text: "".into() }], - ); + // Insert a placeholder node into layouts if those end up empty: + let new_text = placeholder_node_text(&selected_node); + + let edit = + common::create_workspace_edit(url, version, vec![lsp_types::TextEdit { range, new_text }]); send_message_to_lsp(crate::common::PreviewToLspMessage::SendWorkspaceEdit { label: Some("Delete element".to_string()), @@ -501,7 +519,8 @@ async fn reload_preview_impl( let (_, from_cache) = get_url_from_cache(&component.url).unwrap_or_default(); if let Some(component_name) = &component.component { format!( - "{from_cache}\nexport component _SLINT_LivePreview inherits {component_name} {{ /* {NODE_IGNORE_COMMENT} */ }}\n", + "{from_cache}\nexport component _SLINT_LivePreview inherits {component_name} {{ /* {} */ }}\n", + NODE_IGNORE_COMMENT, ) } else { from_cache @@ -831,11 +850,11 @@ pub fn lsp_to_preview_message( /// Use this in nodes you want the language server and preview to /// ignore a node for code analysis purposes. -const NODE_IGNORE_COMMENT: &str = "@lsp:ignore-node"; +pub const NODE_IGNORE_COMMENT: &str = "@lsp:ignore-node"; /// Check whether a node is marked to be ignored in the LSP/live preview /// using a comment containing `@lsp:ignore-node` -fn is_element_node_ignored(node: &Element) -> bool { +pub fn is_element_node_ignored(node: &syntax_nodes::Element) -> bool { node.children_with_tokens().any(|nt| { nt.as_token() .map(|t| t.kind() == SyntaxKind::Comment && t.text().contains(NODE_IGNORE_COMMENT)) diff --git a/tools/lsp/preview/drop_location.rs b/tools/lsp/preview/drop_location.rs index eab36f992..9cfa7d32c 100644 --- a/tools/lsp/preview/drop_location.rs +++ b/tools/lsp/preview/drop_location.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.2 OR LicenseRef-Slint-commercial use i_slint_compiler::diagnostics::SourceFile; -use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxNode}; +use i_slint_compiler::parser::SyntaxKind; use i_slint_core::lengths::{LogicalPoint, LogicalRect, LogicalSize}; use slint_interpreter::ComponentInstance; @@ -163,11 +163,13 @@ fn calculate_drop_information_for_layout( LogicalSize::new(new_midpoint - last_midpoint, geometry.size.height), ); if hit_rect.contains(position) { - let start_pos = if pos == 0 { - geometry.origin.x - } else { - last_endpoint + (c.origin.x - last_endpoint) / 2.0 - }; + let start = (c.origin.x - last_endpoint) / 2.0; + let start_pos = last_endpoint + + if start.floor() < geometry.origin.x { + geometry.origin.x + } else { + start + }; let end_pos = start_pos + 1.0; return ( @@ -224,11 +226,13 @@ fn calculate_drop_information_for_layout( LogicalSize::new(geometry.size.width, new_midpoint - last_midpoint), ); if hit_rect.contains(position) { - let start_pos = if pos == 0 { - geometry.origin.y - } else { - last_endpoint + (c.origin.y - last_endpoint) / 2.0 - }; + let start = (c.origin.y - last_endpoint) / 2.0; + let start_pos = last_endpoint + + if start.floor() < geometry.origin.y { + geometry.origin.y + } else { + start + }; let end_pos = start_pos + 1.0; return ( @@ -427,17 +431,6 @@ fn drop_target_element_nodes( result } -fn extract_element(node: SyntaxNode) -> Option { - match node.kind() { - SyntaxKind::Element => Some(node.into()), - SyntaxKind::SubElement => extract_element(node.child_node(SyntaxKind::Element)?), - SyntaxKind::ConditionalElement | SyntaxKind::RepeatedElement => { - extract_element(node.child_node(SyntaxKind::SubElement)?) - } - _ => None, - } -} - fn find_element_to_drop_into( component_instance: &ComponentInstance, position: LogicalPoint, @@ -484,40 +477,18 @@ fn find_drop_location( let layout_kind = drop_target_node.layout_kind(); if layout_kind != ui::LayoutKind::None { let geometry = drop_target_node.geometry_at(component_instance, position)?; - let children_information: Vec<_> = drop_target_node.with_element_node(|node| { - let mut children_info = Vec::new(); - for c in node.children() { - if let Some(element) = extract_element(c.clone()) { - if preview::is_element_node_ignored(&element) { - continue; - } - - let e_path = element.source_file.path().to_path_buf(); - let e_offset = u32::from(element.text_range().start()); - - let Some(child_node) = common::ElementRcNode::find_in_or_below( - drop_target_node.as_element().clone(), - &e_path, - e_offset, - ) else { - continue; - }; - let Some(c_geometry) = child_node.geometry_in(component_instance, &geometry) - else { - continue; - }; - children_info.push(c_geometry); - } - } - - children_info - }); + let children_geometries: Vec<_> = drop_target_node + .children() + .iter() + .filter(|c| !c.with_element_node(preview::is_element_node_ignored)) + .filter_map(|c| c.geometry_in(component_instance, &geometry)) + .collect(); let (drop_mark, child_index) = calculate_drop_information_for_layout( &geometry, position, &layout_kind, - &children_information, + &children_geometries, ); let insert_info = { @@ -566,14 +537,22 @@ fn drop_ignored_elements_from_node( node.with_element_node(|node| { node.children() .filter_map(|c| { - let e = extract_element(c.clone())?; + let e = common::extract_element(c.clone())?; if preview::is_element_node_ignored(&e) { let first_et = e.first_token()?; let before_et = first_et.prev_token()?; let start_pos = if before_et.kind() == SyntaxKind::Whitespace && before_et.text().contains('\n') { - e.text_range().start() // Leave WS in place, so that the next token after us can go into our place + before_et.text_range().end() + - rowan::TextSize::from( + before_et + .text() + .split('\n') + .last() + .map(|s| s.len()) + .unwrap_or_default() as u32, + ) } else if before_et.kind() == SyntaxKind::Whitespace { before_et.text_range().start() // Cut away all WS! } else { @@ -585,7 +564,15 @@ fn drop_ignored_elements_from_node( let end_pos = if after_et.kind() == SyntaxKind::Whitespace && after_et.text().contains('\n') { - after_et.text_range().end() // Eat all the WS so that the next non-WS is at our start + after_et.text_range().start() + + rowan::TextSize::from( + after_et + .text() + .split('\n') + .next() + .map(|s| s.len() + 1) + .unwrap_or_default() as u32, + ) } else { last_et.text_range().end() // Use existing WS or not WS as appropriate }; diff --git a/tools/lsp/preview/element_selection.rs b/tools/lsp/preview/element_selection.rs index 203711ae6..c5313c5ff 100644 --- a/tools/lsp/preview/element_selection.rs +++ b/tools/lsp/preview/element_selection.rs @@ -7,7 +7,7 @@ use i_slint_compiler::object_tree::{Component, ElementRc}; use i_slint_core::lengths::LogicalPoint; use slint_interpreter::ComponentInstance; -use crate::common::ElementRcNode; +use crate::{common, preview}; #[derive(Clone, Debug)] pub struct ElementSelection { @@ -25,7 +25,7 @@ impl ElementSelection { elements.get(self.instance_index).or_else(|| elements.first()).map(|(e, _)| e.clone()) } - pub fn as_element_node(&self) -> Option { + pub fn as_element_node(&self) -> Option { let element = self.as_element()?; let debug_index = { @@ -36,7 +36,7 @@ impl ElementSelection { }) }; - debug_index.map(|i| ElementRcNode { element, debug_index: i }) + debug_index.map(|i| common::ElementRcNode { element, debug_index: i }) } } @@ -52,7 +52,9 @@ fn self_or_embedded_component_root(element: &ElementRc) -> ElementRc { element.clone() } -fn lsp_element_node_position(element: &ElementRcNode) -> Option<(String, lsp_types::Range)> { +fn lsp_element_node_position( + element: &common::ElementRcNode, +) -> Option<(String, lsp_types::Range)> { let location = element.with_element_node(|n| { n.parent() .filter(|p| p.kind() == i_slint_compiler::parser::SyntaxKind::SubElement) @@ -122,7 +124,7 @@ fn select_element_at_source_code_position_impl( fn select_element_node( component_instance: &ComponentInstance, - selected_element: &ElementRcNode, + selected_element: &common::ElementRcNode, position: Option, ) { let (path, offset) = selected_element.path_and_offset(); @@ -158,12 +160,12 @@ pub struct SelectionCandidate { } impl SelectionCandidate { - pub fn is_selected_element_node(&self, selection: &ElementRcNode) -> bool { + pub fn is_selected_element_node(&self, selection: &common::ElementRcNode) -> bool { self.as_element_node().map(|en| en.path_and_offset()) == Some(selection.path_and_offset()) } - pub fn as_element_node(&self) -> Option { - ElementRcNode::new(self.element.clone(), self.debug_index) + pub fn as_element_node(&self) -> Option { + common::ElementRcNode::new(self.element.clone(), self.debug_index) } } @@ -241,14 +243,14 @@ pub fn collect_all_element_nodes_covering( pub fn is_root_element_node( component_instance: &ComponentInstance, - element_node: &ElementRcNode, + element_node: &common::ElementRcNode, ) -> bool { let root_element = root_element(component_instance); let Some((root_path, root_offset)) = root_element .borrow() .debug .iter() - .find(|(n, _)| !super::is_element_node_ignored(n)) + .find(|(n, _)| !preview::is_element_node_ignored(n)) .map(|(n, _)| (n.source_file.path().to_owned(), u32::from(n.text_range().start()))) else { return false; @@ -260,7 +262,7 @@ pub fn is_root_element_node( pub fn is_same_file_as_root_node( component_instance: &ComponentInstance, - element_node: &ElementRcNode, + element_node: &common::ElementRcNode, ) -> bool { let root_element = root_element(component_instance); let Some(root_path) = @@ -277,7 +279,7 @@ fn select_element_at_impl( component_instance: &ComponentInstance, position: LogicalPoint, enter_component: bool, -) -> Option { +) -> Option { for sc in &collect_all_element_nodes_covering(position, component_instance) { if let Some(en) = filter_nodes_for_selection(component_instance, sc, enter_component) { return Some(en); @@ -309,7 +311,7 @@ pub fn select_element_at(x: f32, y: f32, enter_component: bool) { select_element_node(&component_instance, &en, Some(position)); } -pub fn is_element_node_in_layout(element: &ElementRcNode) -> bool { +pub fn is_element_node_in_layout(element: &common::ElementRcNode) -> bool { if element.debug_index > 0 { // If we are not the first node, then we might have been inlined right // after a layout managing us @@ -336,10 +338,10 @@ fn filter_nodes_for_selection( component_instance: &ComponentInstance, selection_candidate: &SelectionCandidate, enter_component: bool, -) -> Option { +) -> Option { let en = selection_candidate.as_element_node()?; - if en.with_element_node(super::is_element_node_ignored) { + if en.with_element_node(preview::is_element_node_ignored) { return None; } @@ -356,11 +358,11 @@ fn filter_nodes_for_selection( pub fn select_element_behind_impl( component_instance: &ComponentInstance, - selected_element_node: &ElementRcNode, + selected_element_node: &common::ElementRcNode, position: LogicalPoint, enter_component: bool, reverse: bool, -) -> Option { +) -> Option { let elements = collect_all_element_nodes_covering(position, component_instance); let current_selection_position = elements.iter().position(|sc| sc.is_selected_element_node(selected_element_node))?;