mirror of
https://github.com/slint-ui/slint.git
synced 2025-07-08 05:35:24 +00:00
504 lines
18 KiB
Rust
504 lines
18 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
//! Data structures common between LSP and previewer
|
|
|
|
use i_slint_compiler::diagnostics::{BuildDiagnostics, SourceFile};
|
|
use i_slint_compiler::object_tree::Document;
|
|
use i_slint_compiler::parser::{syntax_nodes, TextSize};
|
|
use i_slint_compiler::typeloader::TypeLoader;
|
|
use i_slint_compiler::typeregister::TypeRegister;
|
|
use lsp_types::Url;
|
|
|
|
use std::{
|
|
cell::RefCell,
|
|
collections::HashMap,
|
|
future::Future,
|
|
path::{Path, PathBuf},
|
|
pin::Pin,
|
|
rc::Rc,
|
|
};
|
|
|
|
use crate::common::{file_to_uri, uri_to_file, ElementRcNode, Result};
|
|
use std::collections::HashSet;
|
|
|
|
pub type SourceFileVersion = Option<i32>;
|
|
|
|
pub type SourceFileVersionMap = HashMap<PathBuf, SourceFileVersion>;
|
|
|
|
fn default_cc() -> i_slint_compiler::CompilerConfiguration {
|
|
i_slint_compiler::CompilerConfiguration::new(
|
|
i_slint_compiler::generator::OutputFormat::Interpreter,
|
|
)
|
|
}
|
|
|
|
pub type OpenImportFallback = Option<
|
|
Rc<
|
|
dyn Fn(
|
|
String,
|
|
) -> Pin<
|
|
Box<dyn Future<Output = Option<std::io::Result<(SourceFileVersion, String)>>>>,
|
|
>,
|
|
>,
|
|
>;
|
|
|
|
pub struct CompilerConfiguration {
|
|
pub include_paths: Vec<std::path::PathBuf>,
|
|
pub library_paths: HashMap<String, std::path::PathBuf>,
|
|
pub style: Option<String>,
|
|
pub open_import_fallback: OpenImportFallback,
|
|
pub resource_url_mapper:
|
|
Option<Rc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = Option<String>>>>>>,
|
|
}
|
|
|
|
impl Default for CompilerConfiguration {
|
|
fn default() -> Self {
|
|
let mut cc = default_cc();
|
|
|
|
Self {
|
|
include_paths: std::mem::take(&mut cc.include_paths),
|
|
library_paths: std::mem::take(&mut cc.library_paths),
|
|
style: std::mem::take(&mut cc.style),
|
|
open_import_fallback: None,
|
|
resource_url_mapper: std::mem::take(&mut cc.resource_url_mapper),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CompilerConfiguration {
|
|
fn build(mut self) -> (i_slint_compiler::CompilerConfiguration, OpenImportFallback) {
|
|
let mut result = default_cc();
|
|
result.include_paths = std::mem::take(&mut self.include_paths);
|
|
result.library_paths = std::mem::take(&mut self.library_paths);
|
|
result.style = std::mem::take(&mut self.style);
|
|
result.resource_url_mapper = std::mem::take(&mut self.resource_url_mapper);
|
|
|
|
(result, self.open_import_fallback)
|
|
}
|
|
}
|
|
|
|
/// A cache of loaded documents
|
|
pub struct DocumentCache {
|
|
type_loader: TypeLoader,
|
|
open_import_fallback: OpenImportFallback,
|
|
source_file_versions: Rc<RefCell<SourceFileVersionMap>>,
|
|
}
|
|
|
|
#[cfg(feature = "preview-engine")]
|
|
pub fn document_cache_parts_setup(
|
|
compiler_config: &mut i_slint_compiler::CompilerConfiguration,
|
|
open_import_fallback: OpenImportFallback,
|
|
initial_file_versions: SourceFileVersionMap,
|
|
) -> (OpenImportFallback, Rc<RefCell<SourceFileVersionMap>>) {
|
|
let source_file_versions = Rc::new(RefCell::new(initial_file_versions));
|
|
DocumentCache::wire_up_import_fallback(
|
|
compiler_config,
|
|
open_import_fallback,
|
|
source_file_versions,
|
|
)
|
|
}
|
|
|
|
impl DocumentCache {
|
|
fn wire_up_import_fallback(
|
|
compiler_config: &mut i_slint_compiler::CompilerConfiguration,
|
|
open_import_fallback: OpenImportFallback,
|
|
source_file_versions: Rc<RefCell<SourceFileVersionMap>>,
|
|
) -> (OpenImportFallback, Rc<RefCell<SourceFileVersionMap>>) {
|
|
let sfv = source_file_versions.clone();
|
|
if let Some(open_import_fallback) = open_import_fallback.clone() {
|
|
compiler_config.open_import_fallback = Some(Rc::new(move |file_name: String| {
|
|
let flfb = open_import_fallback(file_name.clone());
|
|
let sfv = sfv.clone();
|
|
Box::pin(async move {
|
|
flfb.await.map(|r| {
|
|
let path = PathBuf::from(file_name);
|
|
match r {
|
|
Ok((v, c)) => {
|
|
sfv.borrow_mut().insert(path, v);
|
|
Ok(c)
|
|
}
|
|
Err(e) => {
|
|
sfv.borrow_mut().remove(&path);
|
|
Err(e)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}))
|
|
}
|
|
|
|
(open_import_fallback, source_file_versions)
|
|
}
|
|
|
|
pub fn new(config: CompilerConfiguration) -> Self {
|
|
let (mut compiler_config, open_import_fallback) = config.build();
|
|
|
|
let (open_import_fallback, source_file_versions) = Self::wire_up_import_fallback(
|
|
&mut compiler_config,
|
|
open_import_fallback,
|
|
Rc::new(RefCell::new(SourceFileVersionMap::default())),
|
|
);
|
|
|
|
Self {
|
|
type_loader: TypeLoader::new(
|
|
i_slint_compiler::typeregister::TypeRegister::builtin(),
|
|
compiler_config,
|
|
&mut BuildDiagnostics::default(),
|
|
),
|
|
open_import_fallback,
|
|
source_file_versions,
|
|
}
|
|
}
|
|
|
|
pub fn new_from_raw_parts(
|
|
mut type_loader: TypeLoader,
|
|
open_import_fallback: OpenImportFallback,
|
|
source_file_versions: Rc<RefCell<SourceFileVersionMap>>,
|
|
) -> Self {
|
|
let (open_import_fallback, source_file_versions) = Self::wire_up_import_fallback(
|
|
&mut type_loader.compiler_config,
|
|
open_import_fallback,
|
|
source_file_versions,
|
|
);
|
|
|
|
Self { type_loader, open_import_fallback, source_file_versions }
|
|
}
|
|
|
|
pub fn snapshot(&self) -> Option<Self> {
|
|
let open_import_fallback = self.open_import_fallback.clone();
|
|
let source_file_versions =
|
|
Rc::new(RefCell::new(self.source_file_versions.borrow().clone()));
|
|
i_slint_compiler::typeloader::snapshot(&self.type_loader)
|
|
.map(|tl| Self::new_from_raw_parts(tl, open_import_fallback, source_file_versions))
|
|
}
|
|
|
|
pub fn resolve_import_path(
|
|
&self,
|
|
import_token: Option<&i_slint_compiler::parser::NodeOrToken>,
|
|
maybe_relative_path_or_url: &str,
|
|
) -> Option<(PathBuf, Option<&'static [u8]>)> {
|
|
self.type_loader.resolve_import_path(import_token, maybe_relative_path_or_url)
|
|
}
|
|
|
|
pub fn document_version(&self, target_uri: &Url) -> SourceFileVersion {
|
|
self.document_version_by_path(&uri_to_file(target_uri).unwrap_or_default())
|
|
}
|
|
|
|
pub fn document_version_by_path(&self, path: &Path) -> SourceFileVersion {
|
|
self.source_file_versions.borrow().get(path).and_then(|v| *v)
|
|
}
|
|
|
|
pub fn get_document<'a>(&'a self, url: &'_ Url) -> Option<&'a Document> {
|
|
let path = uri_to_file(url)?;
|
|
self.type_loader.get_document(&path)
|
|
}
|
|
|
|
fn uses_widgets_impl(&self, doc_path: PathBuf, dedup: &mut HashSet<PathBuf>) -> bool {
|
|
if dedup.contains(&doc_path) {
|
|
return false;
|
|
}
|
|
|
|
if doc_path.starts_with("builtin:/") && doc_path.ends_with("std-widgets.slint") {
|
|
return true;
|
|
}
|
|
|
|
let Some(doc) = self.get_document_by_path(&doc_path) else {
|
|
return false;
|
|
};
|
|
|
|
dedup.insert(doc_path.to_path_buf());
|
|
|
|
for import in doc.imports.iter().map(|i| PathBuf::from(&i.file)) {
|
|
if self.uses_widgets_impl(import, dedup) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Returns true if doc_url uses (possibly indirectly) widgets from "std-widgets.slint"
|
|
pub fn uses_widgets(&self, doc_url: &Url) -> bool {
|
|
let Some(doc_path) = uri_to_file(doc_url) else {
|
|
return false;
|
|
};
|
|
|
|
let mut dedup = HashSet::new();
|
|
|
|
self.uses_widgets_impl(doc_path, &mut dedup)
|
|
}
|
|
|
|
pub fn get_document_by_path<'a>(&'a self, path: &'_ Path) -> Option<&'a Document> {
|
|
self.type_loader.get_document(path)
|
|
}
|
|
|
|
pub fn get_document_for_source_file<'a>(
|
|
&'a self,
|
|
source_file: &'_ SourceFile,
|
|
) -> Option<&'a Document> {
|
|
self.type_loader.get_document(source_file.path())
|
|
}
|
|
|
|
pub fn get_document_and_offset<'a>(
|
|
&'a self,
|
|
text_document_uri: &'_ Url,
|
|
pos: &'_ lsp_types::Position,
|
|
) -> Option<(&'a i_slint_compiler::object_tree::Document, TextSize)> {
|
|
let doc = self.get_document(text_document_uri)?;
|
|
let o = (doc
|
|
.node
|
|
.as_ref()?
|
|
.source_file
|
|
.offset(pos.line as usize + 1, pos.character as usize + 1) as u32)
|
|
.into();
|
|
doc.node.as_ref()?.text_range().contains_inclusive(o).then_some((doc, o))
|
|
}
|
|
|
|
pub fn all_url_documents(&self) -> impl Iterator<Item = (Url, &syntax_nodes::Document)> + '_ {
|
|
self.type_loader.all_file_documents().filter_map(|(p, d)| Some((file_to_uri(p)?, d)))
|
|
}
|
|
|
|
pub fn all_urls(&self) -> impl Iterator<Item = Url> + '_ {
|
|
self.type_loader.all_files().filter_map(|p| file_to_uri(p))
|
|
}
|
|
|
|
pub fn global_type_registry(&self) -> std::cell::Ref<'_, TypeRegister> {
|
|
self.type_loader.global_type_registry.borrow()
|
|
}
|
|
|
|
fn invalidate_everything(&mut self) {
|
|
let all_files = self.type_loader.all_files().cloned().collect::<Vec<_>>();
|
|
|
|
for path in all_files {
|
|
self.type_loader.invalidate_document(&path);
|
|
}
|
|
}
|
|
|
|
pub async fn reconfigure(
|
|
&mut self,
|
|
style: Option<String>,
|
|
include_paths: Option<Vec<PathBuf>>,
|
|
library_paths: Option<HashMap<String, PathBuf>>,
|
|
) -> Result<CompilerConfiguration> {
|
|
if style.is_none() && include_paths.is_none() && library_paths.is_none() {
|
|
return Ok(self.compiler_configuration());
|
|
}
|
|
|
|
if let Some(s) = style {
|
|
if s.is_empty() {
|
|
self.type_loader.compiler_config.style = None;
|
|
} else {
|
|
self.type_loader.compiler_config.style = Some(s);
|
|
}
|
|
}
|
|
|
|
if let Some(ip) = include_paths {
|
|
self.type_loader.compiler_config.include_paths = ip;
|
|
}
|
|
|
|
if let Some(lp) = library_paths {
|
|
self.type_loader.compiler_config.library_paths = lp;
|
|
}
|
|
|
|
self.invalidate_everything();
|
|
|
|
self.preload_builtins().await;
|
|
|
|
Ok(self.compiler_configuration())
|
|
}
|
|
|
|
pub async fn preload_builtins(&mut self) {
|
|
// Always load the widgets so we can auto-complete them
|
|
let mut diag = BuildDiagnostics::default();
|
|
self.type_loader.import_component("std-widgets.slint", "StyleMetrics", &mut diag).await;
|
|
assert!(!diag.has_errors());
|
|
}
|
|
|
|
pub async fn load_url(
|
|
&mut self,
|
|
url: &Url,
|
|
version: SourceFileVersion,
|
|
content: String,
|
|
diag: &mut BuildDiagnostics,
|
|
) -> Result<()> {
|
|
let path =
|
|
uri_to_file(url).ok_or_else(|| format!("Failed to convert path for loading: {url}"))?;
|
|
self.type_loader.load_file(&path, &path, content, false, diag).await;
|
|
self.source_file_versions.borrow_mut().insert(path, version);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn reload_cached_file(&mut self, url: &Url, diag: &mut BuildDiagnostics) {
|
|
let Some(path) = uri_to_file(url) else { return };
|
|
self.type_loader.reload_cached_file(&path, diag).await;
|
|
}
|
|
|
|
pub fn drop_document(&mut self, url: &Url) -> Result<()> {
|
|
let Some(path) = uri_to_file(url) else {
|
|
// This isn't fatal, but we might want to learn about paths/schemes to support in the future.
|
|
eprintln!("Failed to convert path for dropping document: {url}");
|
|
return Ok(());
|
|
};
|
|
Ok(self.type_loader.drop_document(&path)?)
|
|
}
|
|
|
|
/// Invalidate a document and all its dependencies.
|
|
/// return the list of dependencies that were invalidated.
|
|
pub fn invalidate_url(&mut self, url: &Url) -> HashSet<Url> {
|
|
let Some(path) = uri_to_file(url) else { return HashSet::new() };
|
|
self.type_loader
|
|
.invalidate_document(&path)
|
|
.into_iter()
|
|
.filter_map(|x| file_to_uri(&x))
|
|
.collect()
|
|
}
|
|
|
|
pub fn compiler_configuration(&self) -> CompilerConfiguration {
|
|
CompilerConfiguration {
|
|
include_paths: self.type_loader.compiler_config.include_paths.clone(),
|
|
library_paths: self.type_loader.compiler_config.library_paths.clone(),
|
|
style: self.type_loader.compiler_config.style.clone(),
|
|
open_import_fallback: None, // We need to re-generate this anyway
|
|
resource_url_mapper: self.type_loader.compiler_config.resource_url_mapper.clone(),
|
|
}
|
|
}
|
|
|
|
fn element_at_document_and_offset(
|
|
&self,
|
|
document: &i_slint_compiler::object_tree::Document,
|
|
offset: TextSize,
|
|
) -> Option<ElementRcNode> {
|
|
fn element_contains(
|
|
element: &i_slint_compiler::object_tree::ElementRc,
|
|
offset: TextSize,
|
|
) -> Option<usize> {
|
|
element
|
|
.borrow()
|
|
.debug
|
|
.iter()
|
|
.position(|n| n.node.parent().is_some_and(|n| n.text_range().contains(offset)))
|
|
}
|
|
|
|
for component in &document.inner_components {
|
|
let root_element = component.root_element.clone();
|
|
let Some(root_debug_index) = element_contains(&root_element, offset) else {
|
|
continue;
|
|
};
|
|
|
|
let mut element =
|
|
ElementRcNode { element: root_element, debug_index: root_debug_index };
|
|
while element.contains_offset(offset) {
|
|
if let Some((c, i)) = element
|
|
.element
|
|
.clone()
|
|
.borrow()
|
|
.children
|
|
.iter()
|
|
.find_map(|c| element_contains(c, offset).map(|i| (c, i)))
|
|
{
|
|
element = ElementRcNode { element: c.clone(), debug_index: i };
|
|
} else {
|
|
return Some(element);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
pub fn element_at_offset(
|
|
&self,
|
|
text_document_uri: &Url,
|
|
offset: TextSize,
|
|
) -> Option<ElementRcNode> {
|
|
let doc = self.get_document(text_document_uri)?;
|
|
self.element_at_document_and_offset(doc, offset)
|
|
}
|
|
|
|
pub fn element_at_position(
|
|
&self,
|
|
text_document_uri: &Url,
|
|
pos: &lsp_types::Position,
|
|
) -> Option<ElementRcNode> {
|
|
let (doc, offset) = self.get_document_and_offset(text_document_uri, pos)?;
|
|
self.element_at_document_and_offset(doc, offset)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::language::test::complex_document_cache;
|
|
|
|
use super::*;
|
|
|
|
fn id_at_position(dc: &DocumentCache, url: &Url, line: u32, character: u32) -> Option<String> {
|
|
let result = dc.element_at_position(url, &lsp_types::Position { line, character })?;
|
|
let element = result.element.borrow();
|
|
Some(element.id.to_string())
|
|
}
|
|
|
|
fn base_type_at_position(
|
|
dc: &DocumentCache,
|
|
url: &Url,
|
|
line: u32,
|
|
character: u32,
|
|
) -> Option<String> {
|
|
let result = dc.element_at_position(url, &lsp_types::Position { line, character })?;
|
|
let element = result.element.borrow();
|
|
Some(format!("{}", &element.base_type))
|
|
}
|
|
|
|
#[test]
|
|
fn test_element_at_position_no_element() {
|
|
let (dc, url, _) = complex_document_cache();
|
|
assert_eq!(id_at_position(&dc, &url, 0, 10), None);
|
|
// TODO: This is past the end of the line and should thus return None
|
|
assert_eq!(id_at_position(&dc, &url, 42, 90), Some(String::new()));
|
|
assert_eq!(id_at_position(&dc, &url, 1, 0), None);
|
|
assert_eq!(id_at_position(&dc, &url, 55, 1), None);
|
|
assert_eq!(id_at_position(&dc, &url, 56, 5), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_document_version() {
|
|
let (dc, url, _) = complex_document_cache();
|
|
assert_eq!(dc.document_version(&url), Some(42));
|
|
}
|
|
|
|
#[test]
|
|
fn test_element_at_position_no_such_document() {
|
|
let (dc, _, _) = complex_document_cache();
|
|
assert_eq!(id_at_position(&dc, &Url::parse("https://foo.bar/baz").unwrap(), 5, 0), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_element_at_position_root() {
|
|
let (dc, url, _) = complex_document_cache();
|
|
|
|
assert_eq!(id_at_position(&dc, &url, 2, 30), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 2, 32), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 2, 42), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 3, 0), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 3, 53), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 4, 19), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 5, 0), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 6, 8), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 6, 15), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 6, 23), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 8, 15), Some("root".to_string()));
|
|
assert_eq!(id_at_position(&dc, &url, 12, 3), Some("root".to_string())); // right before child // TODO: Seems wrong!
|
|
assert_eq!(id_at_position(&dc, &url, 51, 5), Some("root".to_string())); // right after child // TODO: Why does this not work?
|
|
assert_eq!(id_at_position(&dc, &url, 52, 0), Some("root".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_element_at_position_child() {
|
|
let (dc, url, _) = complex_document_cache();
|
|
|
|
assert_eq!(base_type_at_position(&dc, &url, 12, 4), Some("VerticalBox".to_string()));
|
|
assert_eq!(base_type_at_position(&dc, &url, 14, 22), Some("HorizontalBox".to_string()));
|
|
assert_eq!(base_type_at_position(&dc, &url, 15, 33), Some("Text".to_string()));
|
|
assert_eq!(base_type_at_position(&dc, &url, 27, 4), Some("VerticalBox".to_string()));
|
|
assert_eq!(base_type_at_position(&dc, &url, 28, 8), Some("Text".to_string()));
|
|
assert_eq!(base_type_at_position(&dc, &url, 51, 4), Some("VerticalBox".to_string()));
|
|
}
|
|
}
|