1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | use i_slint_core::lengths::{LogicalLength, LogicalPoint}; |
5 | use slint_interpreter::ComponentInstance; |
6 | |
7 | use crate::common; |
8 | use crate::language::completion; |
9 | use crate::preview::{self, element_selection}; |
10 | use crate::util; |
11 | |
12 | #[cfg (target_arch = "wasm32" )] |
13 | use crate::wasm_prelude::*; |
14 | |
15 | pub struct TextOffsetAdjustment { |
16 | pub start_offset: u32, |
17 | pub end_offset: u32, |
18 | pub new_text_length: u32, |
19 | } |
20 | |
21 | impl 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 | |
51 | pub struct DropInformation { |
52 | pub target_element_node: common::ElementRcNode, |
53 | pub insertion_position: common::VersionedPosition, |
54 | } |
55 | |
56 | fn 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. |
116 | pub 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)] |
125 | pub 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. |
137 | pub 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 | |