mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-02 14:51:15 +00:00
LSP: Move code around
No behavior should change in this patch! Move all the code directly related to the LSP into a `language` module, with `server_loop.rs` becoming `language.rs`, managing that module. All the preview related code is moved into `preview`, with `preview.rs` basically forwarding to `native.rs` and `wasm.rs`. Code accessed from both `language` and `preview` stayed where it was.
This commit is contained in:
parent
65f9e6f1eb
commit
4dda627d14
12 changed files with 456 additions and 454 deletions
|
@ -3,11 +3,18 @@
|
||||||
|
|
||||||
// cSpell: ignore descr rfind
|
// cSpell: ignore descr rfind
|
||||||
|
|
||||||
|
mod completion;
|
||||||
|
mod goto;
|
||||||
|
mod properties;
|
||||||
|
mod semantic_tokens;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
use crate::common::PreviewApi;
|
use crate::common::PreviewApi;
|
||||||
use crate::util::{map_node, map_range, map_token};
|
use crate::util::{map_node, map_range, map_token, to_lsp_diag};
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::wasm_prelude::*;
|
use crate::wasm_prelude::*;
|
||||||
use crate::{completion, goto, semantic_tokens, util};
|
|
||||||
|
|
||||||
use i_slint_compiler::object_tree::ElementRc;
|
use i_slint_compiler::object_tree::ElementRc;
|
||||||
use i_slint_compiler::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken};
|
use i_slint_compiler::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken};
|
||||||
|
@ -432,8 +439,6 @@ pub fn query_properties_command(
|
||||||
) -> Result<serde_json::Value, Error> {
|
) -> Result<serde_json::Value, Error> {
|
||||||
let document_cache = &mut ctx.document_cache.borrow_mut();
|
let document_cache = &mut ctx.document_cache.borrow_mut();
|
||||||
|
|
||||||
use crate::properties;
|
|
||||||
|
|
||||||
let text_document_uri = serde_json::from_value::<lsp_types::TextDocumentIdentifier>(
|
let text_document_uri = serde_json::from_value::<lsp_types::TextDocumentIdentifier>(
|
||||||
params.get(0).ok_or("No text document provided")?.clone(),
|
params.get(0).ok_or("No text document provided")?.clone(),
|
||||||
)?
|
)?
|
||||||
|
@ -468,8 +473,6 @@ pub async fn set_binding_command(
|
||||||
params: &[serde_json::Value],
|
params: &[serde_json::Value],
|
||||||
ctx: &Rc<Context>,
|
ctx: &Rc<Context>,
|
||||||
) -> Result<serde_json::Value, Error> {
|
) -> Result<serde_json::Value, Error> {
|
||||||
use crate::properties;
|
|
||||||
|
|
||||||
let text_document = serde_json::from_value::<lsp_types::OptionalVersionedTextDocumentIdentifier>(
|
let text_document = serde_json::from_value::<lsp_types::OptionalVersionedTextDocumentIdentifier>(
|
||||||
params.get(0).ok_or("No text document provided")?.clone(),
|
params.get(0).ok_or("No text document provided")?.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
@ -561,8 +564,6 @@ pub async fn remove_binding_command(
|
||||||
params: &[serde_json::Value],
|
params: &[serde_json::Value],
|
||||||
ctx: &Rc<Context>,
|
ctx: &Rc<Context>,
|
||||||
) -> Result<serde_json::Value, Error> {
|
) -> Result<serde_json::Value, Error> {
|
||||||
use crate::properties;
|
|
||||||
|
|
||||||
let text_document = serde_json::from_value::<lsp_types::OptionalVersionedTextDocumentIdentifier>(
|
let text_document = serde_json::from_value::<lsp_types::OptionalVersionedTextDocumentIdentifier>(
|
||||||
params.get(0).ok_or("No text document provided")?.clone(),
|
params.get(0).ok_or("No text document provided")?.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
@ -679,7 +680,7 @@ pub(crate) async fn reload_document_impl(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let uri = Url::from_file_path(d.source_file().unwrap()).unwrap();
|
let uri = Url::from_file_path(d.source_file().unwrap()).unwrap();
|
||||||
lsp_diags.entry(uri).or_default().push(util::to_lsp_diag(&d));
|
lsp_diags.entry(uri).or_default().push(to_lsp_diag(&d));
|
||||||
}
|
}
|
||||||
|
|
||||||
lsp_diags
|
lsp_diags
|
||||||
|
@ -1113,7 +1114,7 @@ pub async fn load_configuration(ctx: &Context) -> Result<(), Error> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::test::{complex_document_cache, loaded_document_cache};
|
use test::{complex_document_cache, loaded_document_cache};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_reload_document_invalid_contents() {
|
fn test_reload_document_invalid_contents() {
|
|
@ -3,8 +3,9 @@
|
||||||
|
|
||||||
// cSpell: ignore rfind
|
// cSpell: ignore rfind
|
||||||
|
|
||||||
use crate::server_loop::DocumentCache;
|
use super::DocumentCache;
|
||||||
use crate::util::{lookup_current_element_type, map_position};
|
use crate::util::{lookup_current_element_type, map_position, with_lookup_ctx};
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::wasm_prelude::*;
|
use crate::wasm_prelude::*;
|
||||||
use i_slint_compiler::diagnostics::Spanned;
|
use i_slint_compiler::diagnostics::Spanned;
|
||||||
|
@ -131,7 +132,7 @@ pub(crate) fn completion_at(
|
||||||
} else if let Some(n) = syntax_nodes::Binding::new(node.clone()) {
|
} else if let Some(n) = syntax_nodes::Binding::new(node.clone()) {
|
||||||
if let Some(colon) = n.child_token(SyntaxKind::Colon) {
|
if let Some(colon) = n.child_token(SyntaxKind::Colon) {
|
||||||
if offset >= colon.text_range().end().into() {
|
if offset >= colon.text_range().end().into() {
|
||||||
return crate::util::with_lookup_ctx(document_cache, node, |ctx| {
|
return with_lookup_ctx(document_cache, node, |ctx| {
|
||||||
resolve_expression_scope(ctx).map(Into::into)
|
resolve_expression_scope(ctx).map(Into::into)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
@ -218,7 +219,7 @@ pub(crate) fn completion_at(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return crate::util::with_lookup_ctx(document_cache, node, |ctx| {
|
return with_lookup_ctx(document_cache, node, |ctx| {
|
||||||
resolve_expression_scope(ctx).map(Into::into)
|
resolve_expression_scope(ctx).map(Into::into)
|
||||||
})?;
|
})?;
|
||||||
} else if let Some(q) = syntax_nodes::QualifiedName::new(node.clone()) {
|
} else if let Some(q) = syntax_nodes::QualifiedName::new(node.clone()) {
|
||||||
|
@ -260,7 +261,7 @@ pub(crate) fn completion_at(
|
||||||
return resolve_type_scope(token, document_cache).map(Into::into);
|
return resolve_type_scope(token, document_cache).map(Into::into);
|
||||||
}
|
}
|
||||||
SyntaxKind::Expression => {
|
SyntaxKind::Expression => {
|
||||||
return crate::util::with_lookup_ctx(document_cache, node, |ctx| {
|
return with_lookup_ctx(document_cache, node, |ctx| {
|
||||||
let it = q.children_with_tokens().filter_map(|t| t.into_token());
|
let it = q.children_with_tokens().filter_map(|t| t.into_token());
|
||||||
let mut it = it.skip_while(|t| {
|
let mut it = it.skip_while(|t| {
|
||||||
t.kind() != SyntaxKind::Identifier && t.token != token.token
|
t.kind() != SyntaxKind::Identifier && t.token != token.token
|
||||||
|
@ -679,15 +680,16 @@ fn add_components_to_import(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Given a source text containing the unicode emoji `🔺`, the emoji will be removed and then an autocompletion request will be done as if the cursor was there
|
/// Given a source text containing the unicode emoji `🔺`, the emoji will be removed and then an autocompletion request will be done as if the cursor was there
|
||||||
fn get_completions(file: &str) -> Option<Vec<CompletionItem>> {
|
fn get_completions(file: &str) -> Option<Vec<CompletionItem>> {
|
||||||
const CURSOR_EMOJI: char = '🔺';
|
const CURSOR_EMOJI: char = '🔺';
|
||||||
let offset = file.find(CURSOR_EMOJI).unwrap() as u32;
|
let offset = file.find(CURSOR_EMOJI).unwrap() as u32;
|
||||||
let source = file.replace(CURSOR_EMOJI, "");
|
let source = file.replace(CURSOR_EMOJI, "");
|
||||||
let (mut dc, uri, _) = crate::test::loaded_document_cache(source);
|
let (mut dc, uri, _) = crate::language::test::loaded_document_cache(source);
|
||||||
|
|
||||||
let doc = dc.documents.get_document(&uri.to_file_path().unwrap()).unwrap();
|
let doc = dc.documents.get_document(&uri.to_file_path().unwrap()).unwrap();
|
||||||
let token = crate::server_loop::token_at_offset(doc.node.as_ref().unwrap(), offset)?;
|
let token = crate::language::token_at_offset(doc.node.as_ref().unwrap(), offset)?;
|
||||||
|
|
||||||
completion_at(&mut dc, token, offset, None)
|
completion_at(&mut dc, token, offset, None)
|
||||||
}
|
}
|
|
@ -1,11 +1,8 @@
|
||||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
||||||
|
|
||||||
use crate::util::map_node_and_url;
|
use super::DocumentCache;
|
||||||
|
use crate::util::{lookup_current_element_type, map_node_and_url, with_lookup_ctx};
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use crate::server_loop::DocumentCache;
|
|
||||||
|
|
||||||
use i_slint_compiler::diagnostics::Spanned;
|
use i_slint_compiler::diagnostics::Spanned;
|
||||||
use i_slint_compiler::expression_tree::Expression;
|
use i_slint_compiler::expression_tree::Expression;
|
||||||
|
@ -14,6 +11,7 @@ use i_slint_compiler::lookup::{LookupObject, LookupResult};
|
||||||
use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxNode, SyntaxToken};
|
use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxNode, SyntaxToken};
|
||||||
|
|
||||||
use lsp_types::{GotoDefinitionResponse, LocationLink, Range};
|
use lsp_types::{GotoDefinitionResponse, LocationLink, Range};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
pub fn goto_definition(
|
pub fn goto_definition(
|
||||||
document_cache: &mut DocumentCache,
|
document_cache: &mut DocumentCache,
|
||||||
|
@ -47,7 +45,7 @@ pub fn goto_definition(
|
||||||
if token.kind() != SyntaxKind::Identifier {
|
if token.kind() != SyntaxKind::Identifier {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let lr = crate::util::with_lookup_ctx(document_cache, node, |ctx| {
|
let lr = with_lookup_ctx(document_cache, node, |ctx| {
|
||||||
let mut it = n
|
let mut it = n
|
||||||
.children_with_tokens()
|
.children_with_tokens()
|
||||||
.filter_map(|t| t.into_token())
|
.filter_map(|t| t.into_token())
|
||||||
|
@ -191,7 +189,7 @@ fn find_property_declaration_in_base(
|
||||||
.map(|doc| &doc.local_registry)
|
.map(|doc| &doc.local_registry)
|
||||||
.unwrap_or(&global_tr);
|
.unwrap_or(&global_tr);
|
||||||
|
|
||||||
let mut element_type = crate::util::lookup_current_element_type((*element).clone(), tr)?;
|
let mut element_type = lookup_current_element_type((*element).clone(), tr)?;
|
||||||
while let ElementType::Component(com) = element_type {
|
while let ElementType::Component(com) = element_type {
|
||||||
if let Some(p) = com.root_element.borrow().property_declarations.get(prop_name) {
|
if let Some(p) = com.root_element.borrow().property_declarations.get(prop_name) {
|
||||||
return p.node.clone();
|
return p.node.clone();
|
|
@ -1,10 +1,13 @@
|
||||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
||||||
|
|
||||||
use crate::server_loop::DocumentCache;
|
use super::DocumentCache;
|
||||||
|
use crate::util::{
|
||||||
|
map_node, map_node_and_url, map_position, map_range, to_lsp_diag, with_property_lookup_ctx,
|
||||||
|
ExpressionContextInfo,
|
||||||
|
};
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
use crate::util::{map_node, map_node_and_url, map_position, map_range, ExpressionContextInfo};
|
|
||||||
use i_slint_compiler::diagnostics::{BuildDiagnostics, Spanned};
|
use i_slint_compiler::diagnostics::{BuildDiagnostics, Spanned};
|
||||||
use i_slint_compiler::langtype::{ElementType, Type};
|
use i_slint_compiler::langtype::{ElementType, Type};
|
||||||
use i_slint_compiler::object_tree::{Element, ElementRc, PropertyDeclaration, PropertyVisibility};
|
use i_slint_compiler::object_tree::{Element, ElementRc, PropertyDeclaration, PropertyVisibility};
|
||||||
|
@ -492,9 +495,7 @@ fn set_binding_on_existing_property(
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
SetBindingResponse {
|
SetBindingResponse { diagnostics: diag.iter().map(to_lsp_diag).collect::<Vec<_>>() },
|
||||||
diagnostics: diag.iter().map(crate::util::to_lsp_diag).collect::<Vec<_>>(),
|
|
||||||
},
|
|
||||||
workspace_edit,
|
workspace_edit,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -613,9 +614,7 @@ fn set_binding_on_known_property(
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
SetBindingResponse {
|
SetBindingResponse { diagnostics: diag.iter().map(to_lsp_diag).collect::<Vec<_>>() },
|
||||||
diagnostics: diag.iter().map(crate::util::to_lsp_diag).collect::<Vec<_>>(),
|
|
||||||
},
|
|
||||||
workspace_edit,
|
workspace_edit,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -661,7 +660,7 @@ pub(crate) fn set_binding(
|
||||||
if let Some(node) = element.node.as_ref() {
|
if let Some(node) = element.node.as_ref() {
|
||||||
let expr_context_info =
|
let expr_context_info =
|
||||||
ExpressionContextInfo::new(node.clone(), property_name.to_string(), false);
|
ExpressionContextInfo::new(node.clone(), property_name.to_string(), false);
|
||||||
crate::util::with_property_lookup_ctx(document_cache, &expr_context_info, |ctx| {
|
with_property_lookup_ctx(document_cache, &expr_context_info, |ctx| {
|
||||||
let expression =
|
let expression =
|
||||||
i_slint_compiler::expression_tree::Expression::from_binding_expression_node(
|
i_slint_compiler::expression_tree::Expression::from_binding_expression_node(
|
||||||
expression_node,
|
expression_node,
|
||||||
|
@ -688,7 +687,7 @@ pub(crate) fn set_binding(
|
||||||
);
|
);
|
||||||
return Ok((
|
return Ok((
|
||||||
SetBindingResponse {
|
SetBindingResponse {
|
||||||
diagnostics: diag.iter().map(crate::util::to_lsp_diag).collect::<Vec<_>>(),
|
diagnostics: diag.iter().map(to_lsp_diag).collect::<Vec<_>>(),
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
));
|
));
|
||||||
|
@ -808,9 +807,8 @@ pub(crate) fn remove_binding(
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::server_loop;
|
use crate::language;
|
||||||
|
use crate::language::test::{complex_document_cache, loaded_document_cache};
|
||||||
use crate::test::{complex_document_cache, loaded_document_cache};
|
|
||||||
|
|
||||||
fn find_property<'a>(
|
fn find_property<'a>(
|
||||||
properties: &'a [PropertyInformation],
|
properties: &'a [PropertyInformation],
|
||||||
|
@ -826,7 +824,7 @@ mod tests {
|
||||||
url: &lsp_types::Url,
|
url: &lsp_types::Url,
|
||||||
) -> Option<(ElementRc, Vec<PropertyInformation>)> {
|
) -> Option<(ElementRc, Vec<PropertyInformation>)> {
|
||||||
let element =
|
let element =
|
||||||
server_loop::element_at_position(dc, url, &lsp_types::Position { line, character })?;
|
language::element_at_position(dc, url, &lsp_types::Position { line, character })?;
|
||||||
Some((element.clone(), get_properties(&element)))
|
Some((element.clone(), get_properties(&element)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -874,8 +872,7 @@ mod tests {
|
||||||
fn test_element_information() {
|
fn test_element_information() {
|
||||||
let (mut dc, url, _) = complex_document_cache();
|
let (mut dc, url, _) = complex_document_cache();
|
||||||
let element =
|
let element =
|
||||||
server_loop::element_at_position(&mut dc, &url, &lsp_types::Position::new(33, 4))
|
language::element_at_position(&mut dc, &url, &lsp_types::Position::new(33, 4)).unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = get_element_information(&element);
|
let result = get_element_information(&element);
|
||||||
|
|
|
@ -8,7 +8,7 @@ use lsp_types::{
|
||||||
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, SemanticTokensResult,
|
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, SemanticTokensResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::server_loop::DocumentCache;
|
use super::DocumentCache;
|
||||||
|
|
||||||
/// Give all the used types/modifier a number in an indexed array
|
/// Give all the used types/modifier a number in an indexed array
|
||||||
macro_rules! declare_legend {
|
macro_rules! declare_legend {
|
|
@ -7,7 +7,7 @@ use lsp_types::{Diagnostic, Url};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::server_loop::{reload_document_impl, DocumentCache};
|
use crate::language::{reload_document_impl, DocumentCache};
|
||||||
|
|
||||||
/// Create an empty `DocumentCache`
|
/// Create an empty `DocumentCache`
|
||||||
pub fn empty_document_cache() -> DocumentCache {
|
pub fn empty_document_cache() -> DocumentCache {
|
|
@ -4,25 +4,20 @@
|
||||||
#![cfg(not(target_arch = "wasm32"))]
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
mod completion;
|
mod language;
|
||||||
mod goto;
|
pub mod lsp_ext;
|
||||||
mod lsp_ext;
|
|
||||||
#[cfg(feature = "preview")]
|
#[cfg(feature = "preview")]
|
||||||
mod preview;
|
mod preview;
|
||||||
mod properties;
|
pub mod util;
|
||||||
mod semantic_tokens;
|
|
||||||
mod server_loop;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test;
|
|
||||||
mod util;
|
|
||||||
|
|
||||||
use common::PreviewApi;
|
use common::PreviewApi;
|
||||||
|
use language::*;
|
||||||
|
|
||||||
use i_slint_compiler::CompilerConfiguration;
|
use i_slint_compiler::CompilerConfiguration;
|
||||||
use lsp_types::notification::{
|
use lsp_types::notification::{
|
||||||
DidChangeConfiguration, DidChangeTextDocument, DidOpenTextDocument, Notification,
|
DidChangeConfiguration, DidChangeTextDocument, DidOpenTextDocument, Notification,
|
||||||
};
|
};
|
||||||
use lsp_types::{DidChangeTextDocumentParams, DidOpenTextDocumentParams, InitializeParams};
|
use lsp_types::{DidChangeTextDocumentParams, DidOpenTextDocumentParams, InitializeParams};
|
||||||
use server_loop::*;
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use lsp_server::{Connection, ErrorCode, Message, RequestId, Response};
|
use lsp_server::{Connection, ErrorCode, Message, RequestId, Response};
|
||||||
|
@ -230,7 +225,7 @@ pub fn run_lsp_server() -> Result<(), Error> {
|
||||||
|
|
||||||
let init_param: InitializeParams = serde_json::from_value(params).unwrap();
|
let init_param: InitializeParams = serde_json::from_value(params).unwrap();
|
||||||
let initialize_result =
|
let initialize_result =
|
||||||
serde_json::to_value(server_loop::server_initialize_result(&init_param.capabilities))?;
|
serde_json::to_value(language::server_initialize_result(&init_param.capabilities))?;
|
||||||
connection.initialize_finish(id, initialize_result)?;
|
connection.initialize_finish(id, initialize_result)?;
|
||||||
|
|
||||||
main_loop(connection, init_param)?;
|
main_loop(connection, init_param)?;
|
||||||
|
|
|
@ -1,390 +1,12 @@
|
||||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
||||||
|
|
||||||
// cSpell: ignore condvar
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod wasm;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub use wasm::*;
|
||||||
|
|
||||||
use crate::common::{PostLoadBehavior, PreviewComponent};
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::lsp_ext::{Health, ServerStatusNotification, ServerStatusParams};
|
mod native;
|
||||||
use lsp_types::notification::Notification;
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use once_cell::sync::Lazy;
|
pub use native::*;
|
||||||
use slint_interpreter::ComponentHandle;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::future::Future;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::{Condvar, Mutex};
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
enum RequestedGuiEventLoopState {
|
|
||||||
/// The UI event loop hasn't been started yet because no preview has been requested
|
|
||||||
Uninitialized,
|
|
||||||
/// The LSP thread requested the UI loop to start because a preview was requested,
|
|
||||||
/// But the loop hasn't been started yet
|
|
||||||
StartLoop,
|
|
||||||
/// The Loop is now started so the LSP thread can start posting events
|
|
||||||
LoopStated,
|
|
||||||
/// The LSP thread requested the application to be terminated
|
|
||||||
QuitLoop,
|
|
||||||
}
|
|
||||||
|
|
||||||
static GUI_EVENT_LOOP_NOTIFIER: Lazy<Condvar> = Lazy::new(Condvar::new);
|
|
||||||
static GUI_EVENT_LOOP_STATE_REQUEST: Lazy<Mutex<RequestedGuiEventLoopState>> =
|
|
||||||
Lazy::new(|| Mutex::new(RequestedGuiEventLoopState::Uninitialized));
|
|
||||||
|
|
||||||
fn run_in_ui_thread<F: Future<Output = ()> + 'static>(
|
|
||||||
create_future: impl Send + FnOnce() -> F + 'static,
|
|
||||||
) {
|
|
||||||
// Wake up the main thread to start the event loop, if possible
|
|
||||||
{
|
|
||||||
let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap();
|
|
||||||
if *state_request == RequestedGuiEventLoopState::Uninitialized {
|
|
||||||
*state_request = RequestedGuiEventLoopState::StartLoop;
|
|
||||||
GUI_EVENT_LOOP_NOTIFIER.notify_one();
|
|
||||||
}
|
|
||||||
// We don't want to call post_event before the loop is properly initialized
|
|
||||||
while *state_request == RequestedGuiEventLoopState::StartLoop {
|
|
||||||
state_request = GUI_EVENT_LOOP_NOTIFIER.wait(state_request).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i_slint_core::api::invoke_from_event_loop(move || {
|
|
||||||
i_slint_core::future::spawn_local(create_future()).unwrap();
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_ui_event_loop() {
|
|
||||||
{
|
|
||||||
let mut state_requested = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap();
|
|
||||||
|
|
||||||
while *state_requested == RequestedGuiEventLoopState::Uninitialized {
|
|
||||||
state_requested = GUI_EVENT_LOOP_NOTIFIER.wait(state_requested).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if *state_requested == RequestedGuiEventLoopState::QuitLoop {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if *state_requested == RequestedGuiEventLoopState::StartLoop {
|
|
||||||
// make sure the backend is initialized
|
|
||||||
i_slint_backend_selector::with_platform(|_| Ok(())).unwrap();
|
|
||||||
// Send an event so that once the loop is started, we notify the LSP thread that it can send more events
|
|
||||||
i_slint_core::api::invoke_from_event_loop(|| {
|
|
||||||
let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap();
|
|
||||||
if *state_request == RequestedGuiEventLoopState::StartLoop {
|
|
||||||
*state_request = RequestedGuiEventLoopState::LoopStated;
|
|
||||||
GUI_EVENT_LOOP_NOTIFIER.notify_one();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i_slint_backend_selector::with_platform(|b| {
|
|
||||||
b.set_event_loop_quit_on_last_window_closed(false);
|
|
||||||
b.run_event_loop()
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn quit_ui_event_loop() {
|
|
||||||
// Wake up the main thread, in case it wasn't woken up earlier. If it wasn't, then don't request
|
|
||||||
// a start of the event loop.
|
|
||||||
{
|
|
||||||
let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap();
|
|
||||||
*state_request = RequestedGuiEventLoopState::QuitLoop;
|
|
||||||
GUI_EVENT_LOOP_NOTIFIER.notify_one();
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = i_slint_core::api::quit_event_loop();
|
|
||||||
|
|
||||||
// Make sure then sender channel gets dropped
|
|
||||||
if let Some(cache) = CONTENT_CACHE.get() {
|
|
||||||
let mut cache = cache.lock().unwrap();
|
|
||||||
cache.sender = None;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_preview(
|
|
||||||
sender: crate::ServerNotifier,
|
|
||||||
component: PreviewComponent,
|
|
||||||
post_load_behavior: PostLoadBehavior,
|
|
||||||
) {
|
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
|
||||||
static PENDING_EVENTS: AtomicU32 = AtomicU32::new(0);
|
|
||||||
if PENDING_EVENTS.load(Ordering::SeqCst) > 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PENDING_EVENTS.fetch_add(1, Ordering::SeqCst);
|
|
||||||
run_in_ui_thread(move || async move {
|
|
||||||
PENDING_EVENTS.fetch_sub(1, Ordering::SeqCst);
|
|
||||||
reload_preview(sender, component, post_load_behavior).await
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct ContentCache {
|
|
||||||
source_code: HashMap<PathBuf, String>,
|
|
||||||
dependency: HashSet<PathBuf>,
|
|
||||||
current: PreviewComponent,
|
|
||||||
sender: Option<crate::ServerNotifier>,
|
|
||||||
highlight: Option<(PathBuf, u32)>,
|
|
||||||
design_mode: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
static CONTENT_CACHE: once_cell::sync::OnceCell<Mutex<ContentCache>> =
|
|
||||||
once_cell::sync::OnceCell::new();
|
|
||||||
|
|
||||||
pub fn set_contents(path: &Path, content: String) {
|
|
||||||
let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
|
||||||
cache.source_code.insert(path.to_owned(), content);
|
|
||||||
if cache.dependency.contains(path) {
|
|
||||||
let current = cache.current.clone();
|
|
||||||
let sender = cache.sender.clone();
|
|
||||||
drop(cache);
|
|
||||||
if let Some(sender) = sender {
|
|
||||||
load_preview(sender, current, PostLoadBehavior::DoNothing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config_changed(style: &str, include_paths: &[PathBuf]) {
|
|
||||||
if let Some(cache) = CONTENT_CACHE.get() {
|
|
||||||
let mut cache = cache.lock().unwrap();
|
|
||||||
let style = style.to_string();
|
|
||||||
if cache.current.style != style || cache.current.include_paths != include_paths {
|
|
||||||
cache.current.style = style;
|
|
||||||
cache.current.include_paths = include_paths.to_vec();
|
|
||||||
let current = cache.current.clone();
|
|
||||||
let sender = cache.sender.clone();
|
|
||||||
drop(cache);
|
|
||||||
if let Some(sender) = sender {
|
|
||||||
load_preview(sender, current, PostLoadBehavior::DoNothing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the file is in the cache, returns it.
|
|
||||||
/// In any was, register it as a dependency
|
|
||||||
fn get_file_from_cache(path: PathBuf) -> Option<String> {
|
|
||||||
let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
|
||||||
let r = cache.source_code.get(&path).cloned();
|
|
||||||
cache.dependency.insert(path);
|
|
||||||
r
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct PreviewState {
|
|
||||||
handle: Option<slint_interpreter::ComponentInstance>,
|
|
||||||
}
|
|
||||||
thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();}
|
|
||||||
|
|
||||||
pub fn design_mode() -> bool {
|
|
||||||
let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
|
||||||
cache.design_mode
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_design_mode(sender: crate::ServerNotifier, enable: bool) {
|
|
||||||
let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
|
||||||
cache.design_mode = enable;
|
|
||||||
|
|
||||||
configure_design_mode(enable, &sender);
|
|
||||||
send_notification(
|
|
||||||
&sender,
|
|
||||||
if enable { "Design mode enabled." } else { "Design mode disabled." },
|
|
||||||
Health::Ok,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_document_request_from_element_callback(
|
|
||||||
file: &str,
|
|
||||||
start_line: u32,
|
|
||||||
start_column: u32,
|
|
||||||
_end_line: u32,
|
|
||||||
_end_column: u32,
|
|
||||||
) -> Option<lsp_types::ShowDocumentParams> {
|
|
||||||
use lsp_types::{Position, Range, ShowDocumentParams, Url};
|
|
||||||
|
|
||||||
let start_pos = Position::new(start_line - 1, start_column);
|
|
||||||
// let end_pos = Position::new(end_line - 1, end_column);
|
|
||||||
// Place the cursor at the start of the range and do not mark up the entire range!
|
|
||||||
let selection = Some(Range::new(start_pos, start_pos));
|
|
||||||
|
|
||||||
Url::from_file_path(file).ok().map(|uri| ShowDocumentParams {
|
|
||||||
uri,
|
|
||||||
external: Some(false),
|
|
||||||
take_focus: Some(true),
|
|
||||||
selection,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configure_design_mode(enabled: bool, sender: &crate::ServerNotifier) {
|
|
||||||
let sender = sender.clone();
|
|
||||||
run_in_ui_thread(move || async move {
|
|
||||||
PREVIEW_STATE.with(|preview_state| {
|
|
||||||
let preview_state = preview_state.borrow();
|
|
||||||
if let Some(handle) = &preview_state.handle {
|
|
||||||
handle.set_design_mode(enabled);
|
|
||||||
|
|
||||||
handle.on_element_selected(Box::new(
|
|
||||||
move |file: &str,
|
|
||||||
start_line: u32,
|
|
||||||
start_column: u32,
|
|
||||||
end_line: u32,
|
|
||||||
end_column: u32| {
|
|
||||||
let Some(params) = show_document_request_from_element_callback(
|
|
||||||
file,
|
|
||||||
start_line,
|
|
||||||
start_column - 1,
|
|
||||||
end_line,
|
|
||||||
end_column - 1,
|
|
||||||
) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Ok(fut) =
|
|
||||||
sender.send_request::<lsp_types::request::ShowDocument>(params)
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
i_slint_core::future::spawn_local(fut).unwrap();
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn reload_preview(
|
|
||||||
sender: crate::ServerNotifier,
|
|
||||||
preview_component: PreviewComponent,
|
|
||||||
post_load_behavior: PostLoadBehavior,
|
|
||||||
) {
|
|
||||||
send_notification(&sender, "Loading Preview…", Health::Ok);
|
|
||||||
|
|
||||||
let design_mode;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
|
||||||
cache.dependency.clear();
|
|
||||||
cache.current = preview_component.clone();
|
|
||||||
design_mode = cache.design_mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut builder = slint_interpreter::ComponentCompiler::default();
|
|
||||||
|
|
||||||
if !preview_component.style.is_empty() {
|
|
||||||
builder.set_style(preview_component.style);
|
|
||||||
}
|
|
||||||
builder.set_include_paths(preview_component.include_paths);
|
|
||||||
|
|
||||||
builder.set_file_loader(|path| {
|
|
||||||
let path = path.to_owned();
|
|
||||||
Box::pin(async move { get_file_from_cache(path).map(Result::Ok) })
|
|
||||||
});
|
|
||||||
|
|
||||||
let compiled = if let Some(mut from_cache) = get_file_from_cache(preview_component.path.clone())
|
|
||||||
{
|
|
||||||
if let Some(component) = &preview_component.component {
|
|
||||||
from_cache =
|
|
||||||
format!("{}\nexport component _Preview inherits {} {{ }}\n", from_cache, component);
|
|
||||||
}
|
|
||||||
builder.build_from_source(from_cache, preview_component.path).await
|
|
||||||
} else {
|
|
||||||
builder.build_from_path(preview_component.path).await
|
|
||||||
};
|
|
||||||
|
|
||||||
notify_diagnostics(builder.diagnostics(), &sender);
|
|
||||||
|
|
||||||
if let Some(compiled) = compiled {
|
|
||||||
PREVIEW_STATE.with(|preview_state| {
|
|
||||||
let mut preview_state = preview_state.borrow_mut();
|
|
||||||
let handle = if let Some(handle) = preview_state.handle.take() {
|
|
||||||
let window = handle.window();
|
|
||||||
let handle = compiled.create_with_existing_window(window).unwrap();
|
|
||||||
match post_load_behavior {
|
|
||||||
PostLoadBehavior::ShowAfterLoad => handle.show().unwrap(),
|
|
||||||
PostLoadBehavior::DoNothing => {}
|
|
||||||
}
|
|
||||||
handle
|
|
||||||
} else {
|
|
||||||
let handle = compiled.create().unwrap();
|
|
||||||
handle.show().unwrap();
|
|
||||||
handle
|
|
||||||
};
|
|
||||||
if let Some((path, offset)) =
|
|
||||||
CONTENT_CACHE.get().and_then(|c| c.lock().unwrap().highlight.clone())
|
|
||||||
{
|
|
||||||
handle.highlight(path, offset);
|
|
||||||
}
|
|
||||||
preview_state.handle = Some(handle);
|
|
||||||
});
|
|
||||||
send_notification(&sender, "Preview Loaded", Health::Ok);
|
|
||||||
} else {
|
|
||||||
send_notification(&sender, "Preview not updated", Health::Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_design_mode(design_mode, &sender);
|
|
||||||
|
|
||||||
CONTENT_CACHE.get_or_init(Default::default).lock().unwrap().sender.replace(sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify_diagnostics(
|
|
||||||
diagnostics: &[slint_interpreter::Diagnostic],
|
|
||||||
sender: &crate::ServerNotifier,
|
|
||||||
) -> Option<()> {
|
|
||||||
let mut lsp_diags: HashMap<lsp_types::Url, Vec<lsp_types::Diagnostic>> = Default::default();
|
|
||||||
for d in diagnostics {
|
|
||||||
if d.source_file().map_or(true, |f| f.is_relative()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let uri = lsp_types::Url::from_file_path(d.source_file().unwrap()).unwrap();
|
|
||||||
lsp_diags.entry(uri).or_default().push(crate::util::to_lsp_diag(d));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uri, diagnostics) in lsp_diags {
|
|
||||||
sender
|
|
||||||
.send_notification(
|
|
||||||
"textDocument/publishDiagnostics".into(),
|
|
||||||
lsp_types::PublishDiagnosticsParams { uri, diagnostics, version: None },
|
|
||||||
)
|
|
||||||
.ok()?;
|
|
||||||
}
|
|
||||||
Some(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_notification(sender: &crate::ServerNotifier, arg: &str, health: Health) {
|
|
||||||
sender
|
|
||||||
.send_notification(
|
|
||||||
ServerStatusNotification::METHOD.into(),
|
|
||||||
ServerStatusParams { health, quiescent: false, message: Some(arg.into()) },
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|e| eprintln!("Error sending notification: {:?}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlight the element pointed at the offset in the path.
|
|
||||||
/// When path is None, remove the highlight.
|
|
||||||
pub fn highlight(path: Option<PathBuf>, offset: u32) {
|
|
||||||
let highlight = path.map(|x| (x, offset));
|
|
||||||
let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
|
||||||
|
|
||||||
if cache.highlight == highlight {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cache.highlight = highlight;
|
|
||||||
|
|
||||||
if cache.highlight.as_ref().map_or(true, |(path, _)| cache.dependency.contains(path)) {
|
|
||||||
run_in_ui_thread(move || async move {
|
|
||||||
PREVIEW_STATE.with(|preview_state| {
|
|
||||||
let preview_state = preview_state.borrow();
|
|
||||||
if let (Some(cache), Some(handle)) =
|
|
||||||
(CONTENT_CACHE.get(), preview_state.handle.as_ref())
|
|
||||||
{
|
|
||||||
if let Some((path, offset)) = cache.lock().unwrap().highlight.clone() {
|
|
||||||
handle.highlight(path, offset);
|
|
||||||
} else {
|
|
||||||
handle.highlight(PathBuf::default(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
391
tools/lsp/preview/native.rs
Normal file
391
tools/lsp/preview/native.rs
Normal file
|
@ -0,0 +1,391 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
||||||
|
|
||||||
|
// cSpell: ignore condvar
|
||||||
|
|
||||||
|
use crate::common::{PostLoadBehavior, PreviewComponent};
|
||||||
|
use crate::lsp_ext::{Health, ServerStatusNotification, ServerStatusParams};
|
||||||
|
|
||||||
|
use lsp_types::notification::Notification;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use slint_interpreter::ComponentHandle;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Condvar, Mutex};
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
enum RequestedGuiEventLoopState {
|
||||||
|
/// The UI event loop hasn't been started yet because no preview has been requested
|
||||||
|
Uninitialized,
|
||||||
|
/// The LSP thread requested the UI loop to start because a preview was requested,
|
||||||
|
/// But the loop hasn't been started yet
|
||||||
|
StartLoop,
|
||||||
|
/// The Loop is now started so the LSP thread can start posting events
|
||||||
|
LoopStated,
|
||||||
|
/// The LSP thread requested the application to be terminated
|
||||||
|
QuitLoop,
|
||||||
|
}
|
||||||
|
|
||||||
|
static GUI_EVENT_LOOP_NOTIFIER: Lazy<Condvar> = Lazy::new(Condvar::new);
|
||||||
|
static GUI_EVENT_LOOP_STATE_REQUEST: Lazy<Mutex<RequestedGuiEventLoopState>> =
|
||||||
|
Lazy::new(|| Mutex::new(RequestedGuiEventLoopState::Uninitialized));
|
||||||
|
|
||||||
|
fn run_in_ui_thread<F: Future<Output = ()> + 'static>(
|
||||||
|
create_future: impl Send + FnOnce() -> F + 'static,
|
||||||
|
) {
|
||||||
|
// Wake up the main thread to start the event loop, if possible
|
||||||
|
{
|
||||||
|
let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap();
|
||||||
|
if *state_request == RequestedGuiEventLoopState::Uninitialized {
|
||||||
|
*state_request = RequestedGuiEventLoopState::StartLoop;
|
||||||
|
GUI_EVENT_LOOP_NOTIFIER.notify_one();
|
||||||
|
}
|
||||||
|
// We don't want to call post_event before the loop is properly initialized
|
||||||
|
while *state_request == RequestedGuiEventLoopState::StartLoop {
|
||||||
|
state_request = GUI_EVENT_LOOP_NOTIFIER.wait(state_request).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i_slint_core::api::invoke_from_event_loop(move || {
|
||||||
|
i_slint_core::future::spawn_local(create_future()).unwrap();
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_ui_event_loop() {
|
||||||
|
{
|
||||||
|
let mut state_requested = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap();
|
||||||
|
|
||||||
|
while *state_requested == RequestedGuiEventLoopState::Uninitialized {
|
||||||
|
state_requested = GUI_EVENT_LOOP_NOTIFIER.wait(state_requested).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if *state_requested == RequestedGuiEventLoopState::QuitLoop {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if *state_requested == RequestedGuiEventLoopState::StartLoop {
|
||||||
|
// make sure the backend is initialized
|
||||||
|
i_slint_backend_selector::with_platform(|_| Ok(())).unwrap();
|
||||||
|
// Send an event so that once the loop is started, we notify the LSP thread that it can send more events
|
||||||
|
i_slint_core::api::invoke_from_event_loop(|| {
|
||||||
|
let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap();
|
||||||
|
if *state_request == RequestedGuiEventLoopState::StartLoop {
|
||||||
|
*state_request = RequestedGuiEventLoopState::LoopStated;
|
||||||
|
GUI_EVENT_LOOP_NOTIFIER.notify_one();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i_slint_backend_selector::with_platform(|b| {
|
||||||
|
b.set_event_loop_quit_on_last_window_closed(false);
|
||||||
|
b.run_event_loop()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quit_ui_event_loop() {
|
||||||
|
// Wake up the main thread, in case it wasn't woken up earlier. If it wasn't, then don't request
|
||||||
|
// a start of the event loop.
|
||||||
|
{
|
||||||
|
let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap();
|
||||||
|
*state_request = RequestedGuiEventLoopState::QuitLoop;
|
||||||
|
GUI_EVENT_LOOP_NOTIFIER.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = i_slint_core::api::quit_event_loop();
|
||||||
|
|
||||||
|
// Make sure then sender channel gets dropped
|
||||||
|
if let Some(cache) = CONTENT_CACHE.get() {
|
||||||
|
let mut cache = cache.lock().unwrap();
|
||||||
|
cache.sender = None;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_preview(
|
||||||
|
sender: crate::ServerNotifier,
|
||||||
|
component: PreviewComponent,
|
||||||
|
post_load_behavior: PostLoadBehavior,
|
||||||
|
) {
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
static PENDING_EVENTS: AtomicU32 = AtomicU32::new(0);
|
||||||
|
if PENDING_EVENTS.load(Ordering::SeqCst) > 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PENDING_EVENTS.fetch_add(1, Ordering::SeqCst);
|
||||||
|
run_in_ui_thread(move || async move {
|
||||||
|
PENDING_EVENTS.fetch_sub(1, Ordering::SeqCst);
|
||||||
|
reload_preview(sender, component, post_load_behavior).await
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ContentCache {
|
||||||
|
source_code: HashMap<PathBuf, String>,
|
||||||
|
dependency: HashSet<PathBuf>,
|
||||||
|
current: PreviewComponent,
|
||||||
|
sender: Option<crate::ServerNotifier>,
|
||||||
|
highlight: Option<(PathBuf, u32)>,
|
||||||
|
design_mode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static CONTENT_CACHE: once_cell::sync::OnceCell<Mutex<ContentCache>> =
|
||||||
|
once_cell::sync::OnceCell::new();
|
||||||
|
|
||||||
|
pub fn set_contents(path: &Path, content: String) {
|
||||||
|
let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
||||||
|
cache.source_code.insert(path.to_owned(), content);
|
||||||
|
if cache.dependency.contains(path) {
|
||||||
|
let current = cache.current.clone();
|
||||||
|
let sender = cache.sender.clone();
|
||||||
|
drop(cache);
|
||||||
|
if let Some(sender) = sender {
|
||||||
|
load_preview(sender, current, PostLoadBehavior::DoNothing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config_changed(style: &str, include_paths: &[PathBuf]) {
|
||||||
|
if let Some(cache) = CONTENT_CACHE.get() {
|
||||||
|
let mut cache = cache.lock().unwrap();
|
||||||
|
let style = style.to_string();
|
||||||
|
if cache.current.style != style || cache.current.include_paths != include_paths {
|
||||||
|
cache.current.style = style;
|
||||||
|
cache.current.include_paths = include_paths.to_vec();
|
||||||
|
let current = cache.current.clone();
|
||||||
|
let sender = cache.sender.clone();
|
||||||
|
drop(cache);
|
||||||
|
if let Some(sender) = sender {
|
||||||
|
load_preview(sender, current, PostLoadBehavior::DoNothing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the file is in the cache, returns it.
|
||||||
|
/// In any was, register it as a dependency
|
||||||
|
fn get_file_from_cache(path: PathBuf) -> Option<String> {
|
||||||
|
let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
||||||
|
let r = cache.source_code.get(&path).cloned();
|
||||||
|
cache.dependency.insert(path);
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct PreviewState {
|
||||||
|
handle: Option<slint_interpreter::ComponentInstance>,
|
||||||
|
}
|
||||||
|
thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();}
|
||||||
|
|
||||||
|
pub fn design_mode() -> bool {
|
||||||
|
let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
||||||
|
cache.design_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_design_mode(sender: crate::ServerNotifier, enable: bool) {
|
||||||
|
let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
||||||
|
cache.design_mode = enable;
|
||||||
|
|
||||||
|
configure_design_mode(enable, &sender);
|
||||||
|
send_notification(
|
||||||
|
&sender,
|
||||||
|
if enable { "Design mode enabled." } else { "Design mode disabled." },
|
||||||
|
Health::Ok,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_document_request_from_element_callback(
|
||||||
|
file: &str,
|
||||||
|
start_line: u32,
|
||||||
|
start_column: u32,
|
||||||
|
_end_line: u32,
|
||||||
|
_end_column: u32,
|
||||||
|
) -> Option<lsp_types::ShowDocumentParams> {
|
||||||
|
use lsp_types::{Position, Range, ShowDocumentParams, Url};
|
||||||
|
|
||||||
|
let start_pos = Position::new(start_line - 1, start_column);
|
||||||
|
// let end_pos = Position::new(end_line - 1, end_column);
|
||||||
|
// Place the cursor at the start of the range and do not mark up the entire range!
|
||||||
|
let selection = Some(Range::new(start_pos, start_pos));
|
||||||
|
|
||||||
|
Url::from_file_path(file).ok().map(|uri| ShowDocumentParams {
|
||||||
|
uri,
|
||||||
|
external: Some(false),
|
||||||
|
take_focus: Some(true),
|
||||||
|
selection,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_design_mode(enabled: bool, sender: &crate::ServerNotifier) {
|
||||||
|
let sender = sender.clone();
|
||||||
|
run_in_ui_thread(move || async move {
|
||||||
|
PREVIEW_STATE.with(|preview_state| {
|
||||||
|
let preview_state = preview_state.borrow();
|
||||||
|
if let Some(handle) = &preview_state.handle {
|
||||||
|
handle.set_design_mode(enabled);
|
||||||
|
|
||||||
|
handle.on_element_selected(Box::new(
|
||||||
|
move |file: &str,
|
||||||
|
start_line: u32,
|
||||||
|
start_column: u32,
|
||||||
|
end_line: u32,
|
||||||
|
end_column: u32| {
|
||||||
|
let Some(params) = show_document_request_from_element_callback(
|
||||||
|
file,
|
||||||
|
start_line,
|
||||||
|
start_column - 1,
|
||||||
|
end_line,
|
||||||
|
end_column - 1,
|
||||||
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(fut) =
|
||||||
|
sender.send_request::<lsp_types::request::ShowDocument>(params)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
i_slint_core::future::spawn_local(fut).unwrap();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reload_preview(
|
||||||
|
sender: crate::ServerNotifier,
|
||||||
|
preview_component: PreviewComponent,
|
||||||
|
post_load_behavior: PostLoadBehavior,
|
||||||
|
) {
|
||||||
|
send_notification(&sender, "Loading Preview…", Health::Ok);
|
||||||
|
|
||||||
|
let design_mode;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
||||||
|
cache.dependency.clear();
|
||||||
|
cache.current = preview_component.clone();
|
||||||
|
design_mode = cache.design_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut builder = slint_interpreter::ComponentCompiler::default();
|
||||||
|
|
||||||
|
if !preview_component.style.is_empty() {
|
||||||
|
builder.set_style(preview_component.style);
|
||||||
|
}
|
||||||
|
builder.set_include_paths(preview_component.include_paths);
|
||||||
|
|
||||||
|
builder.set_file_loader(|path| {
|
||||||
|
let path = path.to_owned();
|
||||||
|
Box::pin(async move { get_file_from_cache(path).map(Result::Ok) })
|
||||||
|
});
|
||||||
|
|
||||||
|
let compiled = if let Some(mut from_cache) = get_file_from_cache(preview_component.path.clone())
|
||||||
|
{
|
||||||
|
if let Some(component) = &preview_component.component {
|
||||||
|
from_cache =
|
||||||
|
format!("{}\nexport component _Preview inherits {} {{ }}\n", from_cache, component);
|
||||||
|
}
|
||||||
|
builder.build_from_source(from_cache, preview_component.path).await
|
||||||
|
} else {
|
||||||
|
builder.build_from_path(preview_component.path).await
|
||||||
|
};
|
||||||
|
|
||||||
|
notify_diagnostics(builder.diagnostics(), &sender);
|
||||||
|
|
||||||
|
if let Some(compiled) = compiled {
|
||||||
|
PREVIEW_STATE.with(|preview_state| {
|
||||||
|
let mut preview_state = preview_state.borrow_mut();
|
||||||
|
let handle = if let Some(handle) = preview_state.handle.take() {
|
||||||
|
let window = handle.window();
|
||||||
|
let handle = compiled.create_with_existing_window(window).unwrap();
|
||||||
|
match post_load_behavior {
|
||||||
|
PostLoadBehavior::ShowAfterLoad => handle.show().unwrap(),
|
||||||
|
PostLoadBehavior::DoNothing => {}
|
||||||
|
}
|
||||||
|
handle
|
||||||
|
} else {
|
||||||
|
let handle = compiled.create().unwrap();
|
||||||
|
handle.show().unwrap();
|
||||||
|
handle
|
||||||
|
};
|
||||||
|
if let Some((path, offset)) =
|
||||||
|
CONTENT_CACHE.get().and_then(|c| c.lock().unwrap().highlight.clone())
|
||||||
|
{
|
||||||
|
handle.highlight(path, offset);
|
||||||
|
}
|
||||||
|
preview_state.handle = Some(handle);
|
||||||
|
});
|
||||||
|
send_notification(&sender, "Preview Loaded", Health::Ok);
|
||||||
|
} else {
|
||||||
|
send_notification(&sender, "Preview not updated", Health::Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_design_mode(design_mode, &sender);
|
||||||
|
|
||||||
|
CONTENT_CACHE.get_or_init(Default::default).lock().unwrap().sender.replace(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_diagnostics(
|
||||||
|
diagnostics: &[slint_interpreter::Diagnostic],
|
||||||
|
sender: &crate::ServerNotifier,
|
||||||
|
) -> Option<()> {
|
||||||
|
let mut lsp_diags: HashMap<lsp_types::Url, Vec<lsp_types::Diagnostic>> = Default::default();
|
||||||
|
for d in diagnostics {
|
||||||
|
if d.source_file().map_or(true, |f| f.is_relative()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let uri = lsp_types::Url::from_file_path(d.source_file().unwrap()).unwrap();
|
||||||
|
lsp_diags.entry(uri).or_default().push(crate::util::to_lsp_diag(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uri, diagnostics) in lsp_diags {
|
||||||
|
sender
|
||||||
|
.send_notification(
|
||||||
|
"textDocument/publishDiagnostics".into(),
|
||||||
|
lsp_types::PublishDiagnosticsParams { uri, diagnostics, version: None },
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_notification(sender: &crate::ServerNotifier, arg: &str, health: Health) {
|
||||||
|
sender
|
||||||
|
.send_notification(
|
||||||
|
ServerStatusNotification::METHOD.into(),
|
||||||
|
ServerStatusParams { health, quiescent: false, message: Some(arg.into()) },
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|e| eprintln!("Error sending notification: {:?}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highlight the element pointed at the offset in the path.
|
||||||
|
/// When path is None, remove the highlight.
|
||||||
|
pub fn highlight(path: Option<PathBuf>, offset: u32) {
|
||||||
|
let highlight = path.map(|x| (x, offset));
|
||||||
|
let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
|
||||||
|
|
||||||
|
if cache.highlight == highlight {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cache.highlight = highlight;
|
||||||
|
|
||||||
|
if cache.highlight.as_ref().map_or(true, |(path, _)| cache.dependency.contains(path)) {
|
||||||
|
run_in_ui_thread(move || async move {
|
||||||
|
PREVIEW_STATE.with(|preview_state| {
|
||||||
|
let preview_state = preview_state.borrow();
|
||||||
|
if let (Some(cache), Some(handle)) =
|
||||||
|
(CONTENT_CACHE.get(), preview_state.handle.as_ref())
|
||||||
|
{
|
||||||
|
if let Some((path, offset)) = cache.lock().unwrap().highlight.clone() {
|
||||||
|
handle.highlight(path, offset);
|
||||||
|
} else {
|
||||||
|
handle.highlight(PathBuf::default(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
||||||
|
|
||||||
use crate::server_loop::DocumentCache;
|
use super::DocumentCache;
|
||||||
|
|
||||||
use i_slint_compiler::diagnostics::{DiagnosticLevel, SourceFile, Spanned};
|
use i_slint_compiler::diagnostics::{DiagnosticLevel, SourceFile, Spanned};
|
||||||
use i_slint_compiler::langtype::{ElementType, Type};
|
use i_slint_compiler::langtype::{ElementType, Type};
|
||||||
|
|
|
@ -4,21 +4,17 @@
|
||||||
#![cfg(target_arch = "wasm32")]
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
mod completion;
|
mod language;
|
||||||
mod goto;
|
pub mod lsp_ext;
|
||||||
mod lsp_ext;
|
|
||||||
mod properties;
|
|
||||||
mod semantic_tokens;
|
|
||||||
mod server_loop;
|
|
||||||
mod util;
|
|
||||||
#[cfg(feature = "preview")]
|
#[cfg(feature = "preview")]
|
||||||
mod wasm_preview;
|
mod preview;
|
||||||
|
pub mod util;
|
||||||
|
|
||||||
use common::PreviewApi;
|
use common::PreviewApi;
|
||||||
use i_slint_compiler::CompilerConfiguration;
|
use i_slint_compiler::CompilerConfiguration;
|
||||||
use js_sys::Function;
|
use js_sys::Function;
|
||||||
|
pub use language::{Context, DocumentCache, Error, RequestHandler};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
pub use server_loop::{Context, DocumentCache, Error, RequestHandler};
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
@ -229,7 +225,7 @@ pub fn create(
|
||||||
let reentry_guard = Rc::new(RefCell::new(ReentryGuard::default()));
|
let reentry_guard = Rc::new(RefCell::new(ReentryGuard::default()));
|
||||||
|
|
||||||
let mut rh = RequestHandler::default();
|
let mut rh = RequestHandler::default();
|
||||||
server_loop::register_request_handlers(&mut rh);
|
language::register_request_handlers(&mut rh);
|
||||||
|
|
||||||
Ok(SlintServer {
|
Ok(SlintServer {
|
||||||
ctx: Rc::new(Context {
|
ctx: Rc::new(Context {
|
||||||
|
@ -247,7 +243,7 @@ pub fn create(
|
||||||
impl SlintServer {
|
impl SlintServer {
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn server_initialize_result(&self, cap: JsValue) -> Result<JsValue, JsError> {
|
pub fn server_initialize_result(&self, cap: JsValue) -> Result<JsValue, JsError> {
|
||||||
Ok(to_value(&server_loop::server_initialize_result(&serde_wasm_bindgen::from_value(cap)?))?)
|
Ok(to_value(&language::server_initialize_result(&serde_wasm_bindgen::from_value(cap)?))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
@ -257,7 +253,7 @@ impl SlintServer {
|
||||||
wasm_bindgen_futures::future_to_promise(async move {
|
wasm_bindgen_futures::future_to_promise(async move {
|
||||||
let _lock = ReentryGuard::lock(guard).await;
|
let _lock = ReentryGuard::lock(guard).await;
|
||||||
let uri: lsp_types::Url = serde_wasm_bindgen::from_value(uri)?;
|
let uri: lsp_types::Url = serde_wasm_bindgen::from_value(uri)?;
|
||||||
server_loop::reload_document(
|
language::reload_document(
|
||||||
&ctx,
|
&ctx,
|
||||||
content,
|
content,
|
||||||
uri.clone(),
|
uri.clone(),
|
||||||
|
@ -272,7 +268,7 @@ impl SlintServer {
|
||||||
|
|
||||||
/* #[wasm_bindgen]
|
/* #[wasm_bindgen]
|
||||||
pub fn show_preview(&self, params: JsValue) -> Result<(), JsError> {
|
pub fn show_preview(&self, params: JsValue) -> Result<(), JsError> {
|
||||||
server_loop::show_preview_command(
|
language::show_preview_command(
|
||||||
&serde_wasm_bindgen::from_value(params)?,
|
&serde_wasm_bindgen::from_value(params)?,
|
||||||
&ServerNotifier,
|
&ServerNotifier,
|
||||||
&mut self.0.borrow_mut(),
|
&mut self.0.borrow_mut(),
|
||||||
|
@ -296,7 +292,7 @@ impl SlintServer {
|
||||||
pub async fn reload_config(&self) -> Result<(), JsError> {
|
pub async fn reload_config(&self) -> Result<(), JsError> {
|
||||||
let guard = self.reentry_guard.clone();
|
let guard = self.reentry_guard.clone();
|
||||||
let _lock = ReentryGuard::lock(guard).await;
|
let _lock = ReentryGuard::lock(guard).await;
|
||||||
server_loop::load_configuration(&self.ctx).await.map_err(|e| JsError::new(&e.to_string()))
|
language::load_configuration(&self.ctx).await.map_err(|e| JsError::new(&e.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue