1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Data structures common between LSP and previewer
5
6use i_slint_compiler::diagnostics::{BuildDiagnostics, SourceFile};
7use i_slint_compiler::object_tree::Document;
8use i_slint_compiler::parser::{syntax_nodes, TextSize};
9use i_slint_compiler::typeloader::TypeLoader;
10use i_slint_compiler::typeregister::TypeRegister;
11use lsp_types::Url;
12
13use std::{
14 cell::RefCell,
15 collections::HashMap,
16 future::Future,
17 path::{Path, PathBuf},
18 pin::Pin,
19 rc::Rc,
20};
21
22use crate::common::{file_to_uri, uri_to_file, ElementRcNode, Result};
23use std::collections::HashSet;
24
25pub type SourceFileVersion = Option<i32>;
26
27pub type SourceFileVersionMap = HashMap<PathBuf, SourceFileVersion>;
28
29fn default_cc() -> i_slint_compiler::CompilerConfiguration {
30 i_slint_compiler::CompilerConfiguration::new(
31 i_slint_compiler::generator::OutputFormat::Interpreter,
32 )
33}
34
35pub type OpenImportFallback = Option<
36 Rc<
37 dyn Fn(
38 String,
39 ) -> Pin<
40 Box<dyn Future<Output = Option<std::io::Result<(SourceFileVersion, String)>>>>,
41 >,
42 >,
43>;
44
45pub struct CompilerConfiguration {
46 pub include_paths: Vec<std::path::PathBuf>,
47 pub library_paths: HashMap<String, std::path::PathBuf>,
48 pub style: Option<String>,
49 pub open_import_fallback: OpenImportFallback,
50 pub resource_url_mapper:
51 Option<Rc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = Option<String>>>>>>,
52}
53
54impl Default for CompilerConfiguration {
55 fn default() -> Self {
56 let mut cc: CompilerConfiguration = default_cc();
57
58 Self {
59 include_paths: std::mem::take(&mut cc.include_paths),
60 library_paths: std::mem::take(&mut cc.library_paths),
61 style: std::mem::take(&mut cc.style),
62 open_import_fallback: None,
63 resource_url_mapper: std::mem::take(&mut cc.resource_url_mapper),
64 }
65 }
66}
67
68impl CompilerConfiguration {
69 fn build(mut self) -> (i_slint_compiler::CompilerConfiguration, OpenImportFallback) {
70 let mut result: CompilerConfiguration = default_cc();
71 result.include_paths = std::mem::take(&mut self.include_paths);
72 result.library_paths = std::mem::take(&mut self.library_paths);
73 result.style = std::mem::take(&mut self.style);
74 result.resource_url_mapper = std::mem::take(&mut self.resource_url_mapper);
75
76 (result, self.open_import_fallback)
77 }
78}
79
80/// A cache of loaded documents
81pub struct DocumentCache {
82 type_loader: TypeLoader,
83 open_import_fallback: OpenImportFallback,
84 source_file_versions: Rc<RefCell<SourceFileVersionMap>>,
85}
86
87#[cfg(feature = "preview-engine")]
88pub fn document_cache_parts_setup(
89 compiler_config: &mut i_slint_compiler::CompilerConfiguration,
90 open_import_fallback: OpenImportFallback,
91 initial_file_versions: SourceFileVersionMap,
92) -> (OpenImportFallback, Rc<RefCell<SourceFileVersionMap>>) {
93 let source_file_versions: Rc>> = Rc::new(RefCell::new(initial_file_versions));
94 DocumentCache::wire_up_import_fallback(
95 compiler_config,
96 open_import_fallback,
97 source_file_versions,
98 )
99}
100
101impl DocumentCache {
102 fn wire_up_import_fallback(
103 compiler_config: &mut i_slint_compiler::CompilerConfiguration,
104 open_import_fallback: OpenImportFallback,
105 source_file_versions: Rc<RefCell<SourceFileVersionMap>>,
106 ) -> (OpenImportFallback, Rc<RefCell<SourceFileVersionMap>>) {
107 let sfv = source_file_versions.clone();
108 if let Some(open_import_fallback) = open_import_fallback.clone() {
109 compiler_config.open_import_fallback = Some(Rc::new(move |file_name: String| {
110 let flfb = open_import_fallback(file_name.clone());
111 let sfv = sfv.clone();
112 Box::pin(async move {
113 flfb.await.map(|r| {
114 let path = PathBuf::from(file_name);
115 match r {
116 Ok((v, c)) => {
117 sfv.borrow_mut().insert(path, v);
118 Ok(c)
119 }
120 Err(e) => {
121 sfv.borrow_mut().remove(&path);
122 Err(e)
123 }
124 }
125 })
126 })
127 }))
128 }
129
130 (open_import_fallback, source_file_versions)
131 }
132
133 pub fn new(config: CompilerConfiguration) -> Self {
134 let (mut compiler_config, open_import_fallback) = config.build();
135
136 let (open_import_fallback, source_file_versions) = Self::wire_up_import_fallback(
137 &mut compiler_config,
138 open_import_fallback,
139 Rc::new(RefCell::new(SourceFileVersionMap::default())),
140 );
141
142 Self {
143 type_loader: TypeLoader::new(
144 i_slint_compiler::typeregister::TypeRegister::builtin(),
145 compiler_config,
146 &mut BuildDiagnostics::default(),
147 ),
148 open_import_fallback,
149 source_file_versions,
150 }
151 }
152
153 pub fn new_from_raw_parts(
154 mut type_loader: TypeLoader,
155 open_import_fallback: OpenImportFallback,
156 source_file_versions: Rc<RefCell<SourceFileVersionMap>>,
157 ) -> Self {
158 let (open_import_fallback, source_file_versions) = Self::wire_up_import_fallback(
159 &mut type_loader.compiler_config,
160 open_import_fallback,
161 source_file_versions,
162 );
163
164 Self { type_loader, open_import_fallback, source_file_versions }
165 }
166
167 pub fn snapshot(&self) -> Option<Self> {
168 let open_import_fallback = self.open_import_fallback.clone();
169 let source_file_versions =
170 Rc::new(RefCell::new(self.source_file_versions.borrow().clone()));
171 i_slint_compiler::typeloader::snapshot(&self.type_loader)
172 .map(|tl| Self::new_from_raw_parts(tl, open_import_fallback, source_file_versions))
173 }
174
175 pub fn resolve_import_path(
176 &self,
177 import_token: Option<&i_slint_compiler::parser::NodeOrToken>,
178 maybe_relative_path_or_url: &str,
179 ) -> Option<(PathBuf, Option<&'static [u8]>)> {
180 self.type_loader.resolve_import_path(import_token, maybe_relative_path_or_url)
181 }
182
183 pub fn document_version(&self, target_uri: &Url) -> SourceFileVersion {
184 self.document_version_by_path(&uri_to_file(target_uri).unwrap_or_default())
185 }
186
187 pub fn document_version_by_path(&self, path: &Path) -> SourceFileVersion {
188 self.source_file_versions.borrow().get(path).and_then(|v| *v)
189 }
190
191 pub fn get_document<'a>(&'a self, url: &'_ Url) -> Option<&'a Document> {
192 let path = uri_to_file(url)?;
193 self.type_loader.get_document(&path)
194 }
195
196 fn uses_widgets_impl(&self, doc_path: PathBuf, dedup: &mut HashSet<PathBuf>) -> bool {
197 if dedup.contains(&doc_path) {
198 return false;
199 }
200
201 if doc_path.starts_with("builtin:/") && doc_path.ends_with("std-widgets.slint") {
202 return true;
203 }
204
205 let Some(doc) = self.get_document_by_path(&doc_path) else {
206 return false;
207 };
208
209 dedup.insert(doc_path.to_path_buf());
210
211 for import in doc.imports.iter().map(|i| PathBuf::from(&i.file)) {
212 if self.uses_widgets_impl(import, dedup) {
213 return true;
214 }
215 }
216
217 false
218 }
219
220 /// Returns true if doc_url uses (possibly indirectly) widgets from "std-widgets.slint"
221 pub fn uses_widgets(&self, doc_url: &Url) -> bool {
222 let Some(doc_path) = uri_to_file(doc_url) else {
223 return false;
224 };
225
226 let mut dedup = HashSet::new();
227
228 self.uses_widgets_impl(doc_path, &mut dedup)
229 }
230
231 pub fn get_document_by_path<'a>(&'a self, path: &'_ Path) -> Option<&'a Document> {
232 self.type_loader.get_document(path)
233 }
234
235 pub fn get_document_for_source_file<'a>(
236 &'a self,
237 source_file: &'_ SourceFile,
238 ) -> Option<&'a Document> {
239 self.type_loader.get_document(source_file.path())
240 }
241
242 pub fn get_document_and_offset<'a>(
243 &'a self,
244 text_document_uri: &'_ Url,
245 pos: &'_ lsp_types::Position,
246 ) -> Option<(&'a i_slint_compiler::object_tree::Document, TextSize)> {
247 let doc = self.get_document(text_document_uri)?;
248 let o = (doc
249 .node
250 .as_ref()?
251 .source_file
252 .offset(pos.line as usize + 1, pos.character as usize + 1) as u32)
253 .into();
254 doc.node.as_ref()?.text_range().contains_inclusive(o).then_some((doc, o))
255 }
256
257 pub fn all_url_documents(&self) -> impl Iterator<Item = (Url, &syntax_nodes::Document)> + '_ {
258 self.type_loader.all_file_documents().filter_map(|(p, d)| Some((file_to_uri(p)?, d)))
259 }
260
261 pub fn all_urls(&self) -> impl Iterator<Item = Url> + '_ {
262 self.type_loader.all_files().filter_map(|p| file_to_uri(p))
263 }
264
265 pub fn global_type_registry(&self) -> std::cell::Ref<TypeRegister> {
266 self.type_loader.global_type_registry.borrow()
267 }
268
269 fn invalidate_everything(&mut self) {
270 let all_files = self.type_loader.all_files().cloned().collect::<Vec<_>>();
271
272 for path in all_files {
273 self.type_loader.invalidate_document(&path);
274 }
275 }
276
277 pub async fn reconfigure(
278 &mut self,
279 style: Option<String>,
280 include_paths: Option<Vec<PathBuf>>,
281 library_paths: Option<HashMap<String, PathBuf>>,
282 ) -> Result<CompilerConfiguration> {
283 if style.is_none() && include_paths.is_none() && library_paths.is_none() {
284 return Ok(self.compiler_configuration());
285 }
286
287 if let Some(s) = style {
288 if s.is_empty() {
289 self.type_loader.compiler_config.style = None;
290 } else {
291 self.type_loader.compiler_config.style = Some(s);
292 }
293 }
294
295 if let Some(ip) = include_paths {
296 self.type_loader.compiler_config.include_paths = ip;
297 }
298
299 if let Some(lp) = library_paths {
300 self.type_loader.compiler_config.library_paths = lp;
301 }
302
303 self.invalidate_everything();
304
305 self.preload_builtins().await;
306
307 Ok(self.compiler_configuration())
308 }
309
310 pub async fn preload_builtins(&mut self) {
311 // Always load the widgets so we can auto-complete them
312 let mut diag = BuildDiagnostics::default();
313 self.type_loader.import_component("std-widgets.slint", "StyleMetrics", &mut diag).await;
314 assert!(!diag.has_errors());
315 }
316
317 pub async fn load_url(
318 &mut self,
319 url: &Url,
320 version: SourceFileVersion,
321 content: String,
322 diag: &mut BuildDiagnostics,
323 ) -> Result<()> {
324 let path =
325 uri_to_file(url).ok_or_else(|| format!("Failed to convert path for loading: {url}"))?;
326 self.type_loader.load_file(&path, &path, content, false, diag).await;
327 self.source_file_versions.borrow_mut().insert(path, version);
328 Ok(())
329 }
330
331 pub async fn reload_cached_file(&mut self, url: &Url, diag: &mut BuildDiagnostics) {
332 let Some(path) = uri_to_file(url) else { return };
333 self.type_loader.reload_cached_file(&path, diag).await;
334 }
335
336 pub fn drop_document(&mut self, url: &Url) -> Result<()> {
337 let Some(path) = uri_to_file(url) else {
338 // This isn't fatal, but we might want to learn about paths/schemes to support in the future.
339 eprintln!("Failed to convert path for dropping document: {url}");
340 return Ok(());
341 };
342 Ok(self.type_loader.drop_document(&path)?)
343 }
344
345 /// Invalidate a document and all its dependencies.
346 /// return the list of dependencies that were invalidated.
347 pub fn invalidate_url(&mut self, url: &Url) -> HashSet<Url> {
348 let Some(path) = uri_to_file(url) else { return HashSet::new() };
349 self.type_loader
350 .invalidate_document(&path)
351 .into_iter()
352 .filter_map(|x| file_to_uri(&x))
353 .collect()
354 }
355
356 pub fn compiler_configuration(&self) -> CompilerConfiguration {
357 CompilerConfiguration {
358 include_paths: self.type_loader.compiler_config.include_paths.clone(),
359 library_paths: self.type_loader.compiler_config.library_paths.clone(),
360 style: self.type_loader.compiler_config.style.clone(),
361 open_import_fallback: None, // We need to re-generate this anyway
362 resource_url_mapper: self.type_loader.compiler_config.resource_url_mapper.clone(),
363 }
364 }
365
366 fn element_at_document_and_offset(
367 &self,
368 document: &i_slint_compiler::object_tree::Document,
369 offset: TextSize,
370 ) -> Option<ElementRcNode> {
371 fn element_contains(
372 element: &i_slint_compiler::object_tree::ElementRc,
373 offset: TextSize,
374 ) -> Option<usize> {
375 element
376 .borrow()
377 .debug
378 .iter()
379 .position(|n| n.node.parent().is_some_and(|n| n.text_range().contains(offset)))
380 }
381
382 for component in &document.inner_components {
383 let root_element = component.root_element.clone();
384 let Some(root_debug_index) = element_contains(&root_element, offset) else {
385 continue;
386 };
387
388 let mut element =
389 ElementRcNode { element: root_element, debug_index: root_debug_index };
390 while element.contains_offset(offset) {
391 if let Some((c, i)) = element
392 .element
393 .clone()
394 .borrow()
395 .children
396 .iter()
397 .find_map(|c| element_contains(c, offset).map(|i| (c, i)))
398 {
399 element = ElementRcNode { element: c.clone(), debug_index: i };
400 } else {
401 return Some(element);
402 }
403 }
404 }
405 None
406 }
407
408 pub fn element_at_offset(
409 &self,
410 text_document_uri: &Url,
411 offset: TextSize,
412 ) -> Option<ElementRcNode> {
413 let doc = self.get_document(text_document_uri)?;
414 self.element_at_document_and_offset(doc, offset)
415 }
416
417 pub fn element_at_position(
418 &self,
419 text_document_uri: &Url,
420 pos: &lsp_types::Position,
421 ) -> Option<ElementRcNode> {
422 let (doc, offset) = self.get_document_and_offset(text_document_uri, pos)?;
423 self.element_at_document_and_offset(doc, offset)
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use crate::language::test::complex_document_cache;
430
431 use super::*;
432
433 fn id_at_position(dc: &DocumentCache, url: &Url, line: u32, character: u32) -> Option<String> {
434 let result = dc.element_at_position(url, &lsp_types::Position { line, character })?;
435 let element = result.element.borrow();
436 Some(element.id.to_string())
437 }
438
439 fn base_type_at_position(
440 dc: &DocumentCache,
441 url: &Url,
442 line: u32,
443 character: u32,
444 ) -> Option<String> {
445 let result = dc.element_at_position(url, &lsp_types::Position { line, character })?;
446 let element = result.element.borrow();
447 Some(format!("{}", &element.base_type))
448 }
449
450 #[test]
451 fn test_element_at_position_no_element() {
452 let (dc, url, _) = complex_document_cache();
453 assert_eq!(id_at_position(&dc, &url, 0, 10), None);
454 // TODO: This is past the end of the line and should thus return None
455 assert_eq!(id_at_position(&dc, &url, 42, 90), Some(String::new()));
456 assert_eq!(id_at_position(&dc, &url, 1, 0), None);
457 assert_eq!(id_at_position(&dc, &url, 55, 1), None);
458 assert_eq!(id_at_position(&dc, &url, 56, 5), None);
459 }
460
461 #[test]
462 fn test_document_version() {
463 let (dc, url, _) = complex_document_cache();
464 assert_eq!(dc.document_version(&url), Some(42));
465 }
466
467 #[test]
468 fn test_element_at_position_no_such_document() {
469 let (dc, _, _) = complex_document_cache();
470 assert_eq!(id_at_position(&dc, &Url::parse("https://foo.bar/baz").unwrap(), 5, 0), None);
471 }
472
473 #[test]
474 fn test_element_at_position_root() {
475 let (dc, url, _) = complex_document_cache();
476
477 assert_eq!(id_at_position(&dc, &url, 2, 30), Some("root".to_string()));
478 assert_eq!(id_at_position(&dc, &url, 2, 32), Some("root".to_string()));
479 assert_eq!(id_at_position(&dc, &url, 2, 42), Some("root".to_string()));
480 assert_eq!(id_at_position(&dc, &url, 3, 0), Some("root".to_string()));
481 assert_eq!(id_at_position(&dc, &url, 3, 53), Some("root".to_string()));
482 assert_eq!(id_at_position(&dc, &url, 4, 19), Some("root".to_string()));
483 assert_eq!(id_at_position(&dc, &url, 5, 0), Some("root".to_string()));
484 assert_eq!(id_at_position(&dc, &url, 6, 8), Some("root".to_string()));
485 assert_eq!(id_at_position(&dc, &url, 6, 15), Some("root".to_string()));
486 assert_eq!(id_at_position(&dc, &url, 6, 23), Some("root".to_string()));
487 assert_eq!(id_at_position(&dc, &url, 8, 15), Some("root".to_string()));
488 assert_eq!(id_at_position(&dc, &url, 12, 3), Some("root".to_string())); // right before child // TODO: Seems wrong!
489 assert_eq!(id_at_position(&dc, &url, 51, 5), Some("root".to_string())); // right after child // TODO: Why does this not work?
490 assert_eq!(id_at_position(&dc, &url, 52, 0), Some("root".to_string()));
491 }
492
493 #[test]
494 fn test_element_at_position_child() {
495 let (dc, url, _) = complex_document_cache();
496
497 assert_eq!(base_type_at_position(&dc, &url, 12, 4), Some("VerticalBox".to_string()));
498 assert_eq!(base_type_at_position(&dc, &url, 14, 22), Some("HorizontalBox".to_string()));
499 assert_eq!(base_type_at_position(&dc, &url, 15, 33), Some("Text".to_string()));
500 assert_eq!(base_type_at_position(&dc, &url, 27, 4), Some("VerticalBox".to_string()));
501 assert_eq!(base_type_at_position(&dc, &url, 28, 8), Some("Text".to_string()));
502 assert_eq!(base_type_at_position(&dc, &url, 51, 4), Some("VerticalBox".to_string()));
503 }
504}
505