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 | // cSpell: ignore descr rfind unindented |
5 | |
6 | use crate::common::{ComponentInformation, Position, PropertyChange}; |
7 | use crate::language::DocumentCache; |
8 | |
9 | use i_slint_compiler::langtype::{DefaultSizeBinding, ElementType}; |
10 | use lsp_types::Url; |
11 | |
12 | use std::{path::Path, rc::Rc}; |
13 | |
14 | #[cfg (target_arch = "wasm32" )] |
15 | use crate::wasm_prelude::UrlWasm; |
16 | |
17 | fn builtin_component_info(name: &str, fills_parent: bool) -> ComponentInformation { |
18 | let (category, is_layout) = match name { |
19 | "GridLayout" | "HorizontalLayout" | "VerticalLayout" => ("Layout" , true), |
20 | "Dialog" | "Window" | "PopupWindow" => ("Window Management" , false), |
21 | "FocusScope" | "TouchArea" => ("Event Handling" , false), |
22 | "Text" => ("Text Handling" , false), |
23 | _ => ("Primitives" , false), |
24 | }; |
25 | |
26 | let default_properties = match name { |
27 | "Text" | "TextInput" => vec![PropertyChange::new("text" , format!(" \"{name}\"" ))], |
28 | "Image" => vec![PropertyChange::new("source" , "@image-url( \"EDIT_ME.png \")" .to_string())], |
29 | _ => vec![], |
30 | }; |
31 | |
32 | ComponentInformation { |
33 | name: name.to_string(), |
34 | category: category.to_string(), |
35 | is_global: false, |
36 | is_builtin: true, |
37 | is_std_widget: false, |
38 | is_layout, |
39 | fills_parent: is_layout || fills_parent, |
40 | is_exported: true, |
41 | defined_at: None, |
42 | default_properties, |
43 | } |
44 | } |
45 | |
46 | fn std_widgets_info(name: &str, is_global: bool) -> ComponentInformation { |
47 | let (category, is_layout) = match name { |
48 | "GridBox" | "HorizontalBox" | "VerticalBox" => ("Layout" , true), |
49 | "LineEdit" | "TextEdit" => ("Text Handling" , false), |
50 | "Button" | "CheckBox" | "ComboBox" | "Slider" | "SpinBox" | "Switch" => ("Input" , false), |
51 | "ProgressIndicator" | "Spinner" => ("Status" , false), |
52 | "ListView" | "StandardListView" | "StandardTableView" => ("Views" , false), |
53 | _ => ("Widgets" , false), |
54 | }; |
55 | |
56 | let default_properties = match name { |
57 | "Button" | "CheckBox" | "LineEdit" | "Switch" | "TextEdit" => { |
58 | vec![PropertyChange::new("text" , format!(" \"{name}\"" ))] |
59 | } |
60 | "ComboBox" => { |
61 | vec![PropertyChange::new("model" , "[ \"first \", \"second \", \"third \"]" .to_string())] |
62 | } |
63 | "Slider" | "SpinBox" => vec![ |
64 | PropertyChange::new("minimum" , "0" .to_string()), |
65 | PropertyChange::new("value" , "42" .to_string()), |
66 | PropertyChange::new("maximum" , "100" .to_string()), |
67 | ], |
68 | "StandardButton" => vec![PropertyChange::new("kind" , "ok" .to_string())], |
69 | _ => vec![], |
70 | }; |
71 | |
72 | ComponentInformation { |
73 | name: name.to_string(), |
74 | category: category.to_string(), |
75 | is_global, |
76 | is_builtin: false, |
77 | is_std_widget: true, |
78 | is_layout, |
79 | fills_parent: is_layout, |
80 | is_exported: true, |
81 | defined_at: None, |
82 | default_properties, |
83 | } |
84 | } |
85 | |
86 | fn exported_project_component_info( |
87 | name: &str, |
88 | is_global: bool, |
89 | position: Position, |
90 | ) -> ComponentInformation { |
91 | ComponentInformation { |
92 | name: name.to_string(), |
93 | category: "User Defined" .to_string(), |
94 | is_global, |
95 | is_builtin: false, |
96 | is_std_widget: false, |
97 | is_layout: false, |
98 | fills_parent: false, |
99 | is_exported: true, |
100 | defined_at: Some(position), |
101 | default_properties: vec![], |
102 | } |
103 | } |
104 | |
105 | fn file_local_component_info(name: &str, position: Position) -> ComponentInformation { |
106 | ComponentInformation { |
107 | name: name.to_string(), |
108 | category: "User Defined" .to_string(), |
109 | is_global: false, |
110 | is_builtin: false, |
111 | is_std_widget: false, |
112 | is_layout: false, |
113 | fills_parent: false, |
114 | is_exported: false, |
115 | defined_at: Some(position), |
116 | default_properties: vec![], |
117 | } |
118 | } |
119 | |
120 | pub fn builtin_components(document_cache: &DocumentCache, result: &mut Vec<ComponentInformation>) { |
121 | let registry: Ref<'_, TypeRegister> = document_cache.documents.global_type_registry.borrow(); |
122 | result.extend(iter:registry.all_elements().iter().filter_map(|(name: &String, ty: &ElementType)| match ty { |
123 | ElementType::Builtin(b: &Rc) if !b.is_internal => { |
124 | let fills_parent: bool = |
125 | matches!(b.default_size_binding, DefaultSizeBinding::ExpandsToParentGeometry); |
126 | Some(builtin_component_info(name, fills_parent)) |
127 | } |
128 | _ => None, |
129 | })); |
130 | } |
131 | |
132 | pub fn all_exported_components( |
133 | document_cache: &DocumentCache, |
134 | filter: &mut dyn FnMut(&ComponentInformation) -> bool, |
135 | result: &mut Vec<ComponentInformation>, |
136 | ) { |
137 | for file in document_cache.documents.all_files() { |
138 | let Some(doc) = document_cache.documents.get_document(file) else { continue }; |
139 | let is_builtin = file.starts_with("builtin:/" ); |
140 | let is_std_widget = is_builtin |
141 | && file.file_name().map(|f| f.to_str() == Some("std-widgets.slint" )).unwrap_or(false); |
142 | |
143 | for (exported_name, ty) in &*doc.exports { |
144 | let Some(c) = ty.as_ref().left() else { |
145 | continue; |
146 | }; |
147 | |
148 | let to_push = if is_std_widget && !exported_name.as_str().ends_with("Impl" ) { |
149 | Some(std_widgets_info(exported_name.as_str(), c.is_global())) |
150 | } else if !is_builtin { |
151 | let Ok(url) = Url::from_file_path(file) else { |
152 | continue; |
153 | }; |
154 | let offset = |
155 | c.node.as_ref().map(|n| n.text_range().start().into()).unwrap_or_default(); |
156 | Some(exported_project_component_info( |
157 | exported_name.as_str(), |
158 | c.is_global(), |
159 | Position { url, offset }, |
160 | )) |
161 | } else { |
162 | continue; |
163 | }; |
164 | |
165 | let Some(to_push) = to_push else { |
166 | continue; |
167 | }; |
168 | |
169 | if filter(&to_push) { |
170 | continue; |
171 | } |
172 | |
173 | result.push(to_push); |
174 | } |
175 | } |
176 | } |
177 | |
178 | pub fn file_local_components( |
179 | document_cache: &DocumentCache, |
180 | file: &Path, |
181 | result: &mut Vec<ComponentInformation>, |
182 | ) { |
183 | let Ok(url: Url) = Url::from_file_path(file) else { |
184 | return; |
185 | }; |
186 | |
187 | let Some(doc: &Document) = document_cache.documents.get_document(path:file) else { return }; |
188 | let exported_components = |
189 | doc.exports.iter().filter_map(|(_, e)| e.as_ref().left()).cloned().collect::<Vec<_>>(); |
190 | for component: &Rc in &*doc.inner_components { |
191 | // component.exported_global_names is always empty since the pass populating it has not |
192 | // run. |
193 | if !exported_components.iter().any(|rc: &Rc| Rc::ptr_eq(this:rc, other:component)) { |
194 | let offset: u32 = |
195 | component.node.as_ref().map(|n: &SyntaxNode| n.text_range().start().into()).unwrap_or_default(); |
196 | result.push(file_local_component_info( |
197 | &component.id, |
198 | Position { url: url.clone(), offset }, |
199 | )); |
200 | } |
201 | } |
202 | } |
203 | |
204 | #[cfg (test)] |
205 | mod tests { |
206 | use super::*; |
207 | |
208 | #[test ] |
209 | fn builtin_component_catalog() { |
210 | let (dc, _, _) = crate::language::test::loaded_document_cache(r#""# .to_string()); |
211 | |
212 | let mut result = Default::default(); |
213 | builtin_components(&dc, &mut result); |
214 | |
215 | assert!(result.iter().all(|ci| !ci.is_std_widget)); |
216 | assert!(result.iter().all(|ci| ci.is_exported)); |
217 | assert!(result.iter().all(|ci| ci.is_builtin)); |
218 | assert!(result.iter().all(|ci| !ci.is_global)); |
219 | assert!(result.iter().any(|ci| &ci.name == "TouchArea" )); |
220 | assert!(!result.iter().any(|ci| &ci.name == "AboutSlint" )); |
221 | assert!(!result.iter().any(|ci| &ci.name == "ProgressIndicator" )); |
222 | } |
223 | |
224 | #[test ] |
225 | fn exported_component_catalog_std_widgets_only() { |
226 | let (dc, _, _) = crate::language::test::loaded_document_cache(r#""# .to_string()); |
227 | |
228 | let mut result = Default::default(); |
229 | all_exported_components(&dc, &mut |_| false, &mut result); |
230 | |
231 | assert!(result.iter().all(|ci| ci.is_std_widget)); |
232 | assert!(result.iter().all(|ci| ci.is_exported)); |
233 | assert!(result.iter().all(|ci| !ci.is_builtin)); |
234 | // assert!(result.iter().all(|ci| ci.is_global)); // mixed! |
235 | assert!(!result.iter().any(|ci| &ci.name == "TouchArea" )); |
236 | assert!(result.iter().any(|ci| &ci.name == "AboutSlint" )); |
237 | assert!(result.iter().any(|ci| &ci.name == "ProgressIndicator" )); |
238 | } |
239 | |
240 | #[test ] |
241 | fn exported_component_catalog_filtered() { |
242 | let (dc, _, _) = crate::language::test::loaded_document_cache(r#""# .to_string()); |
243 | |
244 | let mut result = Default::default(); |
245 | all_exported_components(&dc, &mut |_| true, &mut result); |
246 | |
247 | assert!(result.is_empty()); |
248 | } |
249 | |
250 | #[test ] |
251 | fn exported_component_catalog_exported_component() { |
252 | let baseline = { |
253 | let (dc, _, _) = crate::language::test::loaded_document_cache(r#""# .to_string()); |
254 | |
255 | let mut result = Default::default(); |
256 | all_exported_components(&dc, &mut |_| false, &mut result); |
257 | result.len() |
258 | }; |
259 | |
260 | let (dc, _, _) = crate::language::test::loaded_document_cache( |
261 | r#"export component Test1 {}"# .to_string(), |
262 | ); |
263 | |
264 | let mut result = Default::default(); |
265 | all_exported_components(&dc, &mut |_| false, &mut result); |
266 | |
267 | assert!(result.iter().any(|ci| &ci.name == "Test1" )); |
268 | assert!(!result.iter().any(|ci| &ci.name == "TouchArea" )); |
269 | assert!(result.iter().any(|ci| &ci.name == "AboutSlint" )); |
270 | assert!(result.iter().any(|ci| &ci.name == "ProgressIndicator" )); |
271 | assert_eq!(result.len(), baseline + 1); |
272 | } |
273 | |
274 | #[test ] |
275 | fn local_component_catalog_one_unexported_component() { |
276 | let (dc, url, _) = |
277 | crate::language::test::loaded_document_cache(r#"component Test1 {}"# .to_string()); |
278 | |
279 | let mut result = Default::default(); |
280 | file_local_components(&dc, &url.to_file_path().unwrap(), &mut result); |
281 | assert!(result.is_empty()); // Test1 is implicitly exported! |
282 | } |
283 | |
284 | #[test ] |
285 | fn local_component_catalog_two_unexported_components_without_export() { |
286 | let (dc, url, _) = crate::language::test::loaded_document_cache( |
287 | r#" |
288 | component Test1 {} |
289 | component Test2 {}"# |
290 | .to_string(), |
291 | ); |
292 | |
293 | let mut result = Default::default(); |
294 | file_local_components(&dc, &url.to_file_path().unwrap(), &mut result); |
295 | assert_eq!(result.len(), 1); |
296 | |
297 | let test1 = result.iter().find(|ci| &ci.name == "Test1" ).unwrap(); |
298 | assert!(!test1.is_std_widget); |
299 | assert!(!test1.is_builtin); |
300 | assert!(!test1.is_exported); |
301 | assert!(!test1.is_global); |
302 | assert!(!result.iter().any(|ci| &ci.name == "Test2" )); // Test2 is implicitly exported |
303 | } |
304 | #[test ] |
305 | fn local_component_catalog_two_unexported_components_with_export() { |
306 | let (dc, url, _) = crate::language::test::loaded_document_cache( |
307 | r#" |
308 | component Test1 {} |
309 | export component Export1 {} |
310 | component Test2 {}"# |
311 | .to_string(), |
312 | ); |
313 | |
314 | let mut result = Default::default(); |
315 | file_local_components(&dc, &url.to_file_path().unwrap(), &mut result); |
316 | assert_eq!(result.len(), 2); |
317 | |
318 | let test1 = result.iter().find(|ci| &ci.name == "Test1" ).unwrap(); |
319 | assert!(!test1.is_std_widget); |
320 | assert!(!test1.is_builtin); |
321 | assert!(!test1.is_exported); |
322 | assert!(!test1.is_global); |
323 | let test2 = result.iter().find(|ci| &ci.name == "Test2" ).unwrap(); |
324 | assert!(!test2.is_std_widget); |
325 | assert!(!test2.is_builtin); |
326 | assert!(!test2.is_exported); |
327 | assert!(!test2.is_global); |
328 | assert!(!result.iter().any(|ci| &ci.name == "Export1" )); |
329 | } |
330 | } |
331 | |