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:
Tobias Hunger 2023-08-28 10:16:22 +02:00 committed by Tobias Hunger
parent 65f9e6f1eb
commit 4dda627d14
12 changed files with 456 additions and 454 deletions

View file

@ -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() {

View file

@ -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)
} }

View file

@ -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();

View file

@ -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);

View file

@ -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 {

View file

@ -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 {

View file

@ -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)?;

View file

@ -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
View 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);
}
}
})
})
}
}

View file

@ -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};

View file

@ -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()))
} }
} }