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 i_slint_core::lengths::{LogicalLength, LogicalPoint};
5use slint_interpreter::ComponentInstance;
6
7use crate::common;
8use crate::language::completion;
9use crate::preview::{self, element_selection};
10use crate::util;
11
12#[cfg(target_arch = "wasm32")]
13use crate::wasm_prelude::*;
14
15pub struct TextOffsetAdjustment {
16 pub start_offset: u32,
17 pub end_offset: u32,
18 pub new_text_length: u32,
19}
20
21impl TextOffsetAdjustment {
22 pub fn new(
23 edit: &lsp_types::TextEdit,
24 source_file: &i_slint_compiler::diagnostics::SourceFile,
25 ) -> Self {
26 let new_text_length = edit.new_text.len() as u32;
27 let (start_offset, end_offset) = {
28 let so = source_file
29 .offset(edit.range.start.line as usize, edit.range.start.character as usize);
30 let eo =
31 source_file.offset(edit.range.end.line as usize, edit.range.end.character as usize);
32 (std::cmp::min(so, eo) as u32, std::cmp::max(so, eo) as u32)
33 };
34
35 Self { start_offset, end_offset, new_text_length }
36 }
37
38 pub fn adjust(&self, offset: u32) -> u32 {
39 // This is a bit simplistic: We ignore special cases like the offset
40 // being in the area that gets removed.
41 // Worst case: Some unexpected element gets selected. We can live with that.
42 if offset >= self.start_offset {
43 let old_length = self.end_offset - self.start_offset;
44 offset + self.new_text_length - old_length
45 } else {
46 offset
47 }
48 }
49}
50
51pub struct DropInformation {
52 pub target_element_node: common::ElementRcNode,
53 pub insertion_position: common::VersionedPosition,
54}
55
56fn find_drop_location(
57 component_instance: &ComponentInstance,
58 x: f32,
59 y: f32,
60 component_type: &str,
61) -> Option<DropInformation> {
62 let target_element_node = {
63 let mut result = None;
64 let tl = component_instance.definition().type_loader();
65 for sc in &element_selection::collect_all_element_nodes_covering(x, y, component_instance) {
66 let Some(en) = sc.as_element_node() else {
67 continue;
68 };
69
70 if en.with_element_node(preview::is_element_node_ignored) {
71 continue;
72 }
73
74 let (path, _) = en.path_and_offset();
75 let Some(doc) = tl.get_document(&path) else {
76 continue;
77 };
78 if let Some(element_type) = en.with_element_node(|node| {
79 util::lookup_current_element_type((node.clone()).into(), &doc.local_registry)
80 }) {
81 if !en.is_layout()
82 && element_type
83 .accepts_child_element(component_type, &doc.local_registry)
84 .is_err()
85 {
86 break;
87 }
88 }
89
90 if !element_selection::is_same_file_as_root_node(component_instance, &en) {
91 continue;
92 }
93
94 result = Some(en);
95 break;
96 }
97 result
98 }?;
99
100 let insertion_position = target_element_node.with_element_node(|node| {
101 let last_token = crate::util::last_non_ws_token(node)?;
102
103 let url = lsp_types::Url::from_file_path(node.source_file.path()).ok()?;
104 let (version, _) = preview::get_url_from_cache(&url)?;
105
106 Some(common::VersionedPosition::new(
107 crate::common::VersionedUrl::new(url, version),
108 Into::<u32>::into(last_token.text_range().end()).saturating_sub(1),
109 ))
110 })?;
111
112 Some(DropInformation { target_element_node, insertion_position })
113}
114
115/// Find the Element to insert into. None means we can not insert at this point.
116pub fn can_drop_at(x: f32, y: f32, component: &common::ComponentInformation) -> bool {
117 let component_type: String = component.name.to_string();
118 superOption::component_instance()
119 .and_then(|ci: ComponentInstance| find_drop_location(&ci, x, y, &component_type))
120 .is_some()
121}
122
123/// Extra data on an added Element, relevant to the Preview side only.
124#[derive(Clone, Debug)]
125pub struct DropData {
126 /// The offset to select next. This is different from the insert position
127 /// due to indentation, etc.
128 pub selection_offset: u32,
129 pub path: std::path::PathBuf,
130 pub is_layout: bool,
131}
132
133/// Find a location in a file that would be a good place to insert the new component at
134///
135/// Return a WorkspaceEdit to send to the editor and extra info for the live preview in
136/// the DropData struct.
137pub fn drop_at(
138 x: f32,
139 y: f32,
140 component: &common::ComponentInformation,
141) -> Option<(lsp_types::WorkspaceEdit, DropData)> {
142 let component_type = &component.name;
143 let component_instance = preview::component_instance()?;
144 let tl = component_instance.definition().type_loader();
145 let drop_info = find_drop_location(&component_instance, x, y, component_type)?;
146
147 let properties = {
148 let mut props = component.default_properties.clone();
149
150 let click_position =
151 LogicalPoint::from_lengths(LogicalLength::new(x), LogicalLength::new(y));
152
153 if !drop_info.target_element_node.is_layout() && !component.fills_parent {
154 if let Some(area) = component_instance
155 .element_position(&drop_info.target_element_node.element)
156 .iter()
157 .find(|p| p.contains(click_position))
158 {
159 props.push(common::PropertyChange::new("x", format!("{}px", x - area.origin.x)));
160 props.push(common::PropertyChange::new("y", format!("{}px", y - area.origin.y)));
161 }
162 }
163
164 props
165 };
166
167 let indentation = format!(
168 "{} ",
169 crate::util::find_element_indent(&drop_info.target_element_node).unwrap_or_default()
170 );
171
172 let new_text = if properties.is_empty() {
173 format!("{}{} {{ }}\n", indentation, component_type)
174 } else {
175 let mut to_insert = format!("{}{} {{\n", indentation, component_type);
176 for p in &properties {
177 to_insert += &format!("{} {}: {};\n", indentation, p.name, p.value);
178 }
179 to_insert += &format!("{}}}\n", indentation);
180 to_insert
181 };
182
183 let mut selection_offset = drop_info.insertion_position.offset()
184 + new_text.chars().take_while(|c| c.is_whitespace()).map(|c| c.len_utf8()).sum::<usize>()
185 as u32;
186
187 let (path, _) = drop_info.target_element_node.path_and_offset();
188
189 let doc = tl.get_document(&path)?;
190 let mut edits = Vec::with_capacity(2);
191 let import_file = component.import_file_name(&lsp_types::Url::from_file_path(&path).ok());
192 if let Some(edit) = completion::create_import_edit(doc, component_type, &import_file) {
193 if let Some(sf) = doc.node.as_ref().map(|n| &n.source_file) {
194 selection_offset = TextOffsetAdjustment::new(&edit, sf).adjust(selection_offset);
195 }
196 edits.push(edit);
197 }
198
199 let source_file = doc.node.as_ref().unwrap().source_file.clone();
200
201 let ip = util::map_position(&source_file, drop_info.insertion_position.offset().into());
202 edits.push(lsp_types::TextEdit { range: lsp_types::Range::new(ip, ip), new_text });
203
204 Some((
205 common::create_workspace_edit_from_source_file(&source_file, edits)?,
206 DropData { selection_offset, path, is_layout: component.is_layout },
207 ))
208}
209