diff --git a/tools/lsp/common.rs b/tools/lsp/common.rs index d0f43ac21..b6b68f664 100644 --- a/tools/lsp/common.rs +++ b/tools/lsp/common.rs @@ -196,13 +196,37 @@ impl PropertyChange { #[allow(unused)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub enum PreviewToLspMessage { - Status { message: String, health: crate::lsp_ext::Health }, - Diagnostics { uri: Url, diagnostics: Vec }, - ShowDocument { file: Url, selection: lsp_types::Range }, - PreviewTypeChanged { is_external: bool }, - RequestState { unused: bool }, // send all documents! - AddComponent { label: Option, component: ComponentAddition }, - UpdateElement { position: VersionedPosition, properties: Vec }, + Status { + message: String, + health: crate::lsp_ext::Health, + }, + Diagnostics { + uri: Url, + diagnostics: Vec, + }, + ShowDocument { + file: Url, + selection: lsp_types::Range, + }, + PreviewTypeChanged { + is_external: bool, + }, + RequestState { + unused: bool, + }, // send all documents! + AddComponent { + label: Option, + component: ComponentAddition, + }, + UpdateElement { + label: Option, + position: VersionedPosition, + properties: Vec, + }, + RemoveElement { + label: Option, + position: VersionedPosition, + }, } /// Information on the Element types available diff --git a/tools/lsp/language.rs b/tools/lsp/language.rs index cfec7303c..5bc045297 100644 --- a/tools/lsp/language.rs +++ b/tools/lsp/language.rs @@ -5,6 +5,7 @@ mod completion; mod component_catalog; +pub mod element_edit; mod formatting; mod goto; mod properties; @@ -12,13 +13,9 @@ mod semantic_tokens; #[cfg(test)] pub mod test; -use crate::common::{ - create_workspace_edit, create_workspace_edit_from_source_file, LspToPreviewMessage, - PreviewComponent, PreviewConfig, Result, VersionedUrl, -}; -use crate::util::{ - find_element_indent, lookup_current_element_type, map_node, map_range, map_token, to_lsp_diag, -}; +use crate::common::{self, Result}; +use crate::util; + #[cfg(target_arch = "wasm32")] use crate::wasm_prelude::*; use i_slint_compiler::object_tree::ElementRc; @@ -84,32 +81,6 @@ fn create_show_preview_command( ) } -#[cfg(any(feature = "preview-external", feature = "preview-engine"))] -pub fn notify_preview_about_text_edit( - server_notifier: &crate::ServerNotifier, - edit: &TextEdit, - source_file: &i_slint_compiler::diagnostics::SourceFile, -) { - let new_length = edit.new_text.len() as u32; - let (start_offset, end_offset) = { - let so = - source_file.offset(edit.range.start.line as usize, edit.range.start.character as usize); - let eo = - source_file.offset(edit.range.end.line as usize, edit.range.end.character as usize); - (std::cmp::min(so, eo) as u32, std::cmp::max(so, eo) as u32) - }; - - let Ok(url) = Url::from_file_path(source_file.path()) else { - return; - }; - server_notifier.send_message_to_preview(LspToPreviewMessage::AdjustSelection { - url: VersionedUrl::new(url, source_file.version()), - start_offset, - end_offset, - new_length, - }); -} - #[cfg(any(feature = "preview-external", feature = "preview-engine"))] pub fn request_state(ctx: &std::rc::Rc) { use i_slint_compiler::diagnostics::Spanned; @@ -125,24 +96,24 @@ pub fn request_state(ctx: &std::rc::Rc) { let Ok(url) = Url::from_file_path(p) else { continue; }; - ctx.server_notifier.send_message_to_preview(LspToPreviewMessage::SetContents { - url: VersionedUrl::new(url, node.source_file().and_then(|sf| sf.version())), + ctx.server_notifier.send_message_to_preview(common::LspToPreviewMessage::SetContents { + url: common::VersionedUrl::new(url, node.source_file().and_then(|sf| sf.version())), contents: node.text().to_string(), }) } } - ctx.server_notifier.send_message_to_preview(LspToPreviewMessage::SetConfiguration { + ctx.server_notifier.send_message_to_preview(common::LspToPreviewMessage::SetConfiguration { config: cache.preview_config.clone(), }); if let Some(c) = ctx.to_show.borrow().clone() { - ctx.server_notifier.send_message_to_preview(LspToPreviewMessage::ShowPreview(c)) + ctx.server_notifier.send_message_to_preview(common::LspToPreviewMessage::ShowPreview(c)) } } /// A cache of loaded documents pub struct DocumentCache { pub(crate) documents: TypeLoader, - preview_config: PreviewConfig, + preview_config: common::PreviewConfig, } impl DocumentCache { @@ -164,7 +135,7 @@ pub struct Context { pub server_notifier: crate::ServerNotifier, pub init_param: InitializeParams, /// The last component for which the user clicked "show preview" - pub to_show: RefCell>, + pub to_show: RefCell>, } #[derive(Default)] @@ -387,9 +358,9 @@ pub fn register_request_handlers(rh: &mut RequestHandler) { if p.kind() == SyntaxKind::QualifiedName && p.parent().map_or(false, |n| n.kind() == SyntaxKind::Element) { - if let Some(range) = map_node(&p) { + if let Some(range) = util::map_node(&p) { ctx.server_notifier.send_message_to_preview( - LspToPreviewMessage::HighlightFromEditor { url: Some(uri), offset }, + common::LspToPreviewMessage::HighlightFromEditor { url: Some(uri), offset }, ); return Ok(Some(vec![lsp_types::DocumentHighlight { range, kind: None }])); } @@ -397,23 +368,22 @@ pub fn register_request_handlers(rh: &mut RequestHandler) { if let Some(value) = find_element_id_for_highlight(&tk, &p) { ctx.server_notifier.send_message_to_preview( - LspToPreviewMessage::HighlightFromEditor { url: None, offset: 0 }, + common::LspToPreviewMessage::HighlightFromEditor { url: None, offset: 0 }, ); return Ok(Some( value .into_iter() .map(|r| lsp_types::DocumentHighlight { - range: map_range(&p.source_file, r), + range: util::map_range(&p.source_file, r), kind: None, }) .collect(), )); } } - ctx.server_notifier.send_message_to_preview(LspToPreviewMessage::HighlightFromEditor { - url: None, - offset: 0, - }); + ctx.server_notifier.send_message_to_preview( + common::LspToPreviewMessage::HighlightFromEditor { url: None, offset: 0 }, + ); Ok(None) }); rh.register::(|params, ctx| async move { @@ -428,11 +398,11 @@ pub fn register_request_handlers(rh: &mut RequestHandler) { let edits: Vec<_> = value .into_iter() .map(|r| TextEdit { - range: map_range(&p.source_file, r), + range: util::map_range(&p.source_file, r), new_text: params.new_name.clone(), }) .collect(); - return Ok(Some(create_workspace_edit(uri, version, edits))); + return Ok(Some(common::create_workspace_edit(uri, version, edits))); } }; Err("This symbol cannot be renamed. (Only element id can be renamed at the moment)".into()) @@ -442,7 +412,7 @@ pub fn register_request_handlers(rh: &mut RequestHandler) { let uri = params.text_document.uri; if let Some((tk, _off)) = token_descr(&mut document_cache, &uri, ¶ms.position) { if find_element_id_for_highlight(&tk, &tk.parent()).is_some() { - return Ok(map_token(&tk).map(PrepareRenameResponse::Range)); + return Ok(util::map_token(&tk).map(PrepareRenameResponse::Range)); } }; Ok(None) @@ -468,9 +438,13 @@ pub fn show_preview_command(params: &[serde_json::Value], ctx: &Rc) -> let component = params.get(1).and_then(|v| v.as_str()).filter(|v| !v.is_empty()).map(|v| v.to_string()); - let c = PreviewComponent { url, component, style: config.style.clone().unwrap_or_default() }; + let c = common::PreviewComponent { + url, + component, + style: config.style.clone().unwrap_or_default(), + }; ctx.to_show.replace(Some(c.clone())); - ctx.server_notifier.send_message_to_preview(LspToPreviewMessage::ShowPreview(c)); + ctx.server_notifier.send_message_to_preview(common::LspToPreviewMessage::ShowPreview(c)); // Update known Components report_known_components(document_cache, ctx); @@ -558,7 +532,7 @@ pub async fn set_binding_command( format!("No element found at the given start position {:?}", &element_range.start) })?; - let node_range = map_node( + let node_range = util::map_node( &element .borrow() .debug @@ -642,7 +616,7 @@ pub async fn remove_binding_command( format!("No element found at the given start position {:?}", &element_range.start) })?; - let node_range = map_node( + let node_range = util::map_node( &element .borrow() .debug @@ -704,8 +678,8 @@ pub(crate) async fn reload_document_impl( } if let Some(ctx) = ctx { - ctx.server_notifier.send_message_to_preview(LspToPreviewMessage::SetContents { - url: VersionedUrl::new(url, version), + ctx.server_notifier.send_message_to_preview(common::LspToPreviewMessage::SetContents { + url: common::VersionedUrl::new(url, version), contents: content.clone(), }); } @@ -727,7 +701,7 @@ pub(crate) async fn reload_document_impl( continue; } let uri = Url::from_file_path(d.source_file().unwrap()).unwrap(); - lsp_diags.entry(uri).or_default().push(to_lsp_diag(&d)); + lsp_diags.entry(uri).or_default().push(util::to_lsp_diag(&d)); } lsp_diags @@ -751,11 +725,11 @@ fn report_known_components(document_cache: &mut DocumentCache, ctx: &Rc component_catalog::file_local_components(document_cache, &file, &mut components); - VersionedUrl::new(url, version) + common::VersionedUrl::new(url, version) }); ctx.server_notifier - .send_message_to_preview(LspToPreviewMessage::KnownComponents { url, components }); + .send_message_to_preview(common::LspToPreviewMessage::KnownComponents { url, components }); } pub async fn reload_document( @@ -907,14 +881,14 @@ fn get_code_actions( } if token.kind() == SyntaxKind::StringLiteral && node.kind() == SyntaxKind::Expression { - let r = map_range(&token.source_file, node.text_range()); + let r = util::map_range(&token.source_file, node.text_range()); let edits = vec![ TextEdit::new(lsp_types::Range::new(r.start, r.start), "@tr(".into()), TextEdit::new(lsp_types::Range::new(r.end, r.end), ")".into()), ]; result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { title: "Wrap in `@tr()`".into(), - edit: create_workspace_edit_from_source_file(&token.source_file, edits), + edit: common::create_workspace_edit_from_source_file(&token.source_file, edits), ..Default::default() })); } else if token.kind() == SyntaxKind::Identifier @@ -928,7 +902,7 @@ fn get_code_actions( .get_document(token.source_file.path()) .map(|doc| &doc.local_registry) .unwrap_or(&global_tr); - lookup_current_element_type(node.clone(), tr).is_none() + util::lookup_current_element_type(node.clone(), tr).is_none() }; if is_lookup_error { // Couldn't lookup the element, there is probably an error. Suggest an edit @@ -941,7 +915,7 @@ fn get_code_actions( result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { title: format!("Add import from \"{file}\""), kind: Some(lsp_types::CodeActionKind::QUICKFIX), - edit: create_workspace_edit_from_source_file( + edit: common::create_workspace_edit_from_source_file( &token.source_file, vec![edit], ), @@ -952,9 +926,9 @@ fn get_code_actions( } if has_experimental_client_capability(client_capabilities, "snippetTextEdit") { - let r = map_range(&token.source_file, node.parent().unwrap().text_range()); + let r = util::map_range(&token.source_file, node.parent().unwrap().text_range()); let element = element_at_position(document_cache, &uri, &r.start); - let element_indent = element.as_ref().and_then(find_element_indent); + let element_indent = element.as_ref().and_then(util::find_element_indent); let indented_lines = node .parent() .unwrap() @@ -976,7 +950,7 @@ fn get_code_actions( result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { title: "Wrap in element".into(), kind: Some(lsp_types::CodeActionKind::REFACTOR), - edit: create_workspace_edit_from_source_file(&token.source_file, edits), + edit: common::create_workspace_edit_from_source_file(&token.source_file, edits), ..Default::default() })); @@ -1040,7 +1014,7 @@ fn get_code_actions( result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { title: "Remove element".into(), kind: Some(lsp_types::CodeActionKind::REFACTOR), - edit: create_workspace_edit_from_source_file(&token.source_file, edits), + edit: common::create_workspace_edit_from_source_file(&token.source_file, edits), ..Default::default() })); } @@ -1063,7 +1037,7 @@ fn get_code_actions( result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { title: "Repeat element".into(), kind: Some(lsp_types::CodeActionKind::REFACTOR), - edit: create_workspace_edit_from_source_file(&token.source_file, edits), + edit: common::create_workspace_edit_from_source_file(&token.source_file, edits), ..Default::default() })); @@ -1074,7 +1048,7 @@ fn get_code_actions( result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { title: "Make conditional".into(), kind: Some(lsp_types::CodeActionKind::REFACTOR), - edit: create_workspace_edit_from_source_file(&token.source_file, edits), + edit: common::create_workspace_edit_from_source_file(&token.source_file, edits), ..Default::default() })); } @@ -1096,7 +1070,7 @@ fn get_document_color( loop { if token.kind() == SyntaxKind::ColorLiteral { (|| -> Option<()> { - let range = map_token(&token)?; + let range = util::map_token(&token)?; let col = i_slint_compiler::literals::parse_color_literal(token.text())?; let shift = |s: u32| -> f32 { ((col >> s) & 0xff) as f32 / 255. }; result.push(ColorInformation { @@ -1141,14 +1115,14 @@ fn get_document_symbols( let root_element = c.root_element.borrow(); let element_node = &root_element.debug.first()?.0; let component_node = syntax_nodes::Component::new(element_node.parent()?)?; - let selection_range = map_node(&component_node.DeclaredIdentifier())?; + let selection_range = util::map_node(&component_node.DeclaredIdentifier())?; if c.id.is_empty() { // Symbols with empty names are invalid return None; } Some(DocumentSymbol { - range: map_node(&component_node)?, + range: util::map_node(&component_node)?, selection_range, name: c.id.clone(), kind: if c.is_global() { @@ -1164,16 +1138,18 @@ fn get_document_symbols( r.extend(inner_types.iter().filter_map(|c| match c { Type::Struct { name: Some(name), node: Some(node), .. } => Some(DocumentSymbol { - range: map_node(node.parent().as_ref()?)?, - selection_range: map_node(&node.parent()?.child_node(SyntaxKind::DeclaredIdentifier)?)?, + range: util::map_node(node.parent().as_ref()?)?, + selection_range: util::map_node( + &node.parent()?.child_node(SyntaxKind::DeclaredIdentifier)?, + )?, name: name.clone(), kind: lsp_types::SymbolKind::STRUCT, ..ds.clone() }), Type::Enumeration(enumeration) => enumeration.node.as_ref().and_then(|node| { Some(DocumentSymbol { - range: map_node(node)?, - selection_range: map_node(&node.DeclaredIdentifier())?, + range: util::map_node(node)?, + selection_range: util::map_node(&node.DeclaredIdentifier())?, name: enumeration.name.clone(), kind: lsp_types::SymbolKind::ENUM, ..ds.clone() @@ -1193,8 +1169,8 @@ fn get_document_symbols( let sub_element_node = element_node.parent()?; debug_assert_eq!(sub_element_node.kind(), SyntaxKind::SubElement); Some(DocumentSymbol { - range: map_node(&sub_element_node)?, - selection_range: map_node(element_node.QualifiedName().as_ref()?)?, + range: util::map_node(&sub_element_node)?, + selection_range: util::map_node(element_node.QualifiedName().as_ref()?)?, name: e.base_type.to_string(), detail: (!e.id.is_empty()).then(|| e.id.clone()), kind: lsp_types::SymbolKind::VARIABLE, @@ -1226,7 +1202,7 @@ fn get_code_lenses( // Handle preview lens r.extend(inner_components.iter().filter(|c| !c.is_global()).filter_map(|c| { Some(CodeLens { - range: map_node(&c.root_element.borrow().debug.first()?.0)?, + range: util::map_node(&c.root_element.borrow().debug.first()?.0)?, command: Some(create_show_preview_command(true, &text_document.uri, c.id.as_str())), data: None, }) @@ -1362,97 +1338,18 @@ pub async fn load_configuration(ctx: &Context) -> Result<()> { document_cache.documents.import_component("std-widgets.slint", "StyleMetrics", &mut diag).await; let cc = &document_cache.documents.compiler_config; - let config = PreviewConfig { + let config = common::PreviewConfig { hide_ui, style: cc.style.clone().unwrap_or_default(), include_paths: cc.include_paths.clone(), library_paths: cc.library_paths.clone(), }; document_cache.preview_config = config.clone(); - ctx.server_notifier.send_message_to_preview(LspToPreviewMessage::SetConfiguration { config }); + ctx.server_notifier + .send_message_to_preview(common::LspToPreviewMessage::SetConfiguration { config }); Ok(()) } -#[cfg(any(feature = "preview-external", feature = "preview-engine"))] -pub fn add_component( - ctx: &Context, - component: crate::common::ComponentAddition, -) -> Result { - let document_url = component.insert_position.url(); - let dc = ctx.document_cache.borrow(); - let file = lsp_types::Url::to_file_path(document_url) - .map_err(|_| "Failed to convert URL to file path".to_string())?; - - if &dc.document_version(document_url) != component.insert_position.version() { - return Err("Document version mismatch.".into()); - } - - let doc = dc - .documents - .get_document(&file) - .ok_or_else(|| "Document URL not found in cache".to_string())?; - let mut edits = Vec::with_capacity(2); - if let Some(edit) = - completion::create_import_edit(doc, &component.component_type, &component.import_path) - { - if let Some(sf) = doc.node.as_ref().map(|n| &n.source_file) { - notify_preview_about_text_edit(&ctx.server_notifier, &edit, sf); - } - edits.push(edit); - } - - let source_file = doc.node.as_ref().unwrap().source_file.clone(); - - let ip = crate::util::map_position(&source_file, component.insert_position.offset().into()); - edits.push(TextEdit { - range: lsp_types::Range::new(ip.clone(), ip), - new_text: component.component_text, - }); - - create_workspace_edit_from_source_file(&source_file, edits) - .ok_or("Could not create workspace edit".into()) -} - -#[cfg(any(feature = "preview-external", feature = "preview-engine"))] -pub fn update_element( - ctx: &Context, - position: crate::common::VersionedPosition, - properties: Vec, -) -> Result { - let mut document_cache = ctx.document_cache.borrow_mut(); - let file = lsp_types::Url::to_file_path(position.url()) - .map_err(|_| "Failed to convert URL to file path".to_string())?; - - if &document_cache.document_version(position.url()) != position.version() { - return Err("Document version mismatch.".into()); - } - - let doc = document_cache - .documents - .get_document(&file) - .ok_or_else(|| "Document not found".to_string())?; - - let source_file = doc - .node - .as_ref() - .map(|n| n.source_file.clone()) - .ok_or_else(|| "Document had no node".to_string())?; - let element_position = crate::util::map_position(&source_file, position.offset().into()); - - let element = element_at_position(&mut document_cache, &position.url(), &element_position) - .ok_or_else(|| { - format!("No element found at the given start position {:?}", &element_position) - })?; - - let (_, e) = crate::language::properties::set_bindings( - &mut document_cache, - position.url(), - &element, - &properties, - )?; - Ok(e.ok_or_else(|| "Failed to create workspace edit".to_string())?) -} - #[cfg(test)] pub mod tests { use super::*; diff --git a/tools/lsp/language/element_edit.rs b/tools/lsp/language/element_edit.rs new file mode 100644 index 000000000..cb928f243 --- /dev/null +++ b/tools/lsp/language/element_edit.rs @@ -0,0 +1,135 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial + +#![cfg(any(feature = "preview-external", feature = "preview-engine"))] + +use crate::Context; + +use crate::common::{self, Result}; +use crate::language::{self, completion}; +use crate::util; + +#[cfg(target_arch = "wasm32")] +use crate::wasm_prelude::*; + +use i_slint_compiler::object_tree::ElementRc; +use lsp_types::{TextEdit, Url, WorkspaceEdit}; + +pub fn notify_preview_about_text_edit( + server_notifier: &crate::ServerNotifier, + edit: &TextEdit, + source_file: &i_slint_compiler::diagnostics::SourceFile, +) { + let new_length = edit.new_text.len() as u32; + let (start_offset, end_offset) = { + let so = + source_file.offset(edit.range.start.line as usize, edit.range.start.character as usize); + let eo = + source_file.offset(edit.range.end.line as usize, edit.range.end.character as usize); + (std::cmp::min(so, eo) as u32, std::cmp::max(so, eo) as u32) + }; + + let Ok(url) = Url::from_file_path(source_file.path()) else { + return; + }; + server_notifier.send_message_to_preview(common::LspToPreviewMessage::AdjustSelection { + url: common::VersionedUrl::new(url, source_file.version()), + start_offset, + end_offset, + new_length, + }); +} + +pub fn element_at_source_code_position( + dc: &mut language::DocumentCache, + position: &common::VersionedPosition, +) -> Result { + let file = Url::to_file_path(position.url()) + .map_err(|_| "Failed to convert URL to file path".to_string())?; + + if &dc.document_version(position.url()) != position.version() { + return Err("Document version mismatch.".into()); + } + + let doc = dc.documents.get_document(&file).ok_or_else(|| "Document not found".to_string())?; + + let source_file = doc + .node + .as_ref() + .map(|n| n.source_file.clone()) + .ok_or_else(|| "Document had no node".to_string())?; + let element_position = util::map_position(&source_file, position.offset().into()); + + Ok(language::element_at_position(dc, &position.url(), &element_position).ok_or_else(|| { + format!("No element found at the given start position {:?}", &element_position) + })?) +} + +pub fn add_component(ctx: &Context, component: common::ComponentAddition) -> Result { + let document_url = component.insert_position.url(); + let dc = ctx.document_cache.borrow(); + let file = Url::to_file_path(document_url) + .map_err(|_| "Failed to convert URL to file path".to_string())?; + + if &dc.document_version(document_url) != component.insert_position.version() { + return Err("Document version mismatch.".into()); + } + + let doc = dc + .documents + .get_document(&file) + .ok_or_else(|| "Document URL not found in cache".to_string())?; + let mut edits = Vec::with_capacity(2); + if let Some(edit) = + completion::create_import_edit(doc, &component.component_type, &component.import_path) + { + if let Some(sf) = doc.node.as_ref().map(|n| &n.source_file) { + notify_preview_about_text_edit(&ctx.server_notifier, &edit, sf); + } + edits.push(edit); + } + + let source_file = doc.node.as_ref().unwrap().source_file.clone(); + + let ip = util::map_position(&source_file, component.insert_position.offset().into()); + edits.push(TextEdit { + range: lsp_types::Range::new(ip.clone(), ip), + new_text: component.component_text, + }); + + common::create_workspace_edit_from_source_file(&source_file, edits) + .ok_or("Could not create workspace edit".into()) +} + +pub fn update_element( + ctx: &Context, + position: common::VersionedPosition, + properties: Vec, +) -> Result { + let element = element_at_source_code_position(&mut ctx.document_cache.borrow_mut(), &position)?; + + let (_, e) = language::properties::set_bindings( + &mut ctx.document_cache.borrow_mut(), + position.url(), + &element, + &properties, + )?; + Ok(e.ok_or_else(|| "Failed to create workspace edit".to_string())?) +} + +pub fn remove_element(ctx: &Context, position: common::VersionedPosition) -> Result { + let element = element_at_source_code_position(&mut ctx.document_cache.borrow_mut(), &position)?; + + let e = element.borrow(); + let Some(node) = e.debug.get(0).map(|(n, _)| n) else { + return Err("No node found".into()); + }; + + let Some(range) = util::map_node(node) else { + return Err("Could not map element node".into()); + }; + let edits = vec![TextEdit { range, new_text: String::new() }]; + + common::create_workspace_edit_from_source_file(&node.source_file, edits) + .ok_or("Could not create workspace edit".into()) +} diff --git a/tools/lsp/main.rs b/tools/lsp/main.rs index 3a83e54c1..dd402f41d 100644 --- a/tools/lsp/main.rs +++ b/tools/lsp/main.rs @@ -14,7 +14,7 @@ pub mod lsp_ext; mod preview; pub mod util; -use common::{LspToPreviewMessage, Result, VersionedUrl}; +use common::Result; use language::*; use i_slint_compiler::CompilerConfiguration; @@ -138,7 +138,7 @@ impl ServerNotifier { })) } - pub fn send_message_to_preview(&self, message: LspToPreviewMessage) { + pub fn send_message_to_preview(&self, message: common::LspToPreviewMessage) { if self.use_external_preview.get() { let _ = self.send_notification("slint/lsp_to_preview".to_string(), message); } else { @@ -277,10 +277,12 @@ fn main_loop(connection: Connection, init_param: InitializeParams, cli_args: Cli let contents = std::fs::read_to_string(&path); if let Ok(contents) = &contents { if let Ok(url) = Url::from_file_path(&path) { - server_notifier.send_message_to_preview(LspToPreviewMessage::SetContents { - url: VersionedUrl::new(url, None), - contents: contents.clone(), - }) + server_notifier.send_message_to_preview( + common::LspToPreviewMessage::SetContents { + url: common::VersionedUrl::new(url, None), + contents: contents.clone(), + }, + ) } } Some(contents) @@ -406,6 +408,28 @@ async fn handle_notification(req: lsp_server::Notification, ctx: &Rc) - } } +#[cfg(any(feature = "preview-external", feature = "preview-engine"))] +async fn send_workspace_edit( + server_notifier: ServerNotifier, + label: Option, + edit: Result, +) -> Result<()> { + let edit = edit?; + + let response = server_notifier + .send_request::( + lsp_types::ApplyWorkspaceEditParams { label, edit }, + )? + .await?; + if !response.applied { + return Err(response + .failure_reason + .unwrap_or("Operation failed, no specific reason given".into()) + .into()); + } + Ok(()) +} + #[cfg(any(feature = "preview-external", feature = "preview-engine"))] async fn handle_preview_to_lsp_message( message: crate::common::PreviewToLspMessage, @@ -442,49 +466,28 @@ async fn handle_preview_to_lsp_message( crate::language::request_state(ctx); } M::AddComponent { label, component } => { - let edit = match crate::language::add_component(ctx, component) { - Ok(edit) => edit, - Err(e) => { - eprintln!("Error: {}", e); - return Ok(()); - } - }; - let response = ctx - .server_notifier - .send_request::( - lsp_types::ApplyWorkspaceEditParams { label, edit }, - )? - .await?; - if !response.applied { - return Err(response - .failure_reason - .unwrap_or("Operation failed, no specific reason given".into()) - .into()); - } + let _ = send_workspace_edit( + ctx.server_notifier.clone(), + label, + element_edit::add_component(ctx, component), + ) + .await; } - M::UpdateElement { position, properties } => { - let edit = match crate::language::update_element(ctx, position, properties) { - Ok(e) => e, - Err(e) => { - eprintln!("Error: {e}"); - return Ok(()); - } - }; - let response = ctx - .server_notifier - .send_request::( - lsp_types::ApplyWorkspaceEditParams { - label: Some("Element update".to_string()), - edit, - }, - )? - .await?; - if !response.applied { - return Err(response - .failure_reason - .unwrap_or("Operation failed, no specific reason given".into()) - .into()); - } + M::UpdateElement { label, position, properties } => { + let _ = send_workspace_edit( + ctx.server_notifier.clone(), + label, + element_edit::update_element(ctx, position, properties), + ) + .await; + } + M::RemoveElement { label, position } => { + let _ = send_workspace_edit( + ctx.server_notifier.clone(), + label, + element_edit::remove_element(ctx, position), + ) + .await; } } Ok(()) diff --git a/tools/lsp/preview.rs b/tools/lsp/preview.rs index 03fea22fb..582e19217 100644 --- a/tools/lsp/preview.rs +++ b/tools/lsp/preview.rs @@ -143,18 +143,32 @@ fn drop_component( } // triggered from the UI, running in UI thread -fn change_geometry_of_selected_element(x: f32, y: f32, width: f32, height: f32) { - let Some(selected) = PREVIEW_STATE.with(move |preview_state| { - let preview_state = preview_state.borrow(); - preview_state.selected.clone() - }) else { +fn delete_selected_element() { + let Some(selected) = selected_element() else { return; }; - let Some(selected_element_struct) = selected_element() else { + let Ok(url) = Url::from_file_path(&selected.path) else { return; }; - let Some(selected_element) = selected_element_struct.as_element() else { + + let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); + let Some((version, _)) = cache.source_code.get(&url).cloned() else { + return; + }; + + send_message_to_lsp(crate::common::PreviewToLspMessage::RemoveElement { + label: Some("Deleting element".to_string()), + position: VersionedPosition::new(VersionedUrl::new(url, version), selected.offset), + }); +} + +// triggered from the UI, running in UI thread +fn change_geometry_of_selected_element(x: f32, y: f32, width: f32, height: f32) { + let Some(selected) = selected_element() else { + return; + }; + let Some(selected_element) = selected.as_element() else { return; }; let Some(component_instance) = component_instance() else { @@ -182,27 +196,32 @@ fn change_geometry_of_selected_element(x: f32, y: f32, width: f32, height: f32) }) .unwrap_or_default(); - let properties = { + let (properties, op) = { let mut p = Vec::with_capacity(4); + let mut op = ""; if geometry.origin.x != x { p.push(crate::common::PropertyChange::new( "x", format!("{}px", (x - parent_x).round()), )); + op = "Moving"; } if geometry.origin.y != y { p.push(crate::common::PropertyChange::new( "y", format!("{}px", (y - parent_y).round()), )); + op = "Moving"; } if geometry.size.width != width { p.push(crate::common::PropertyChange::new("width", format!("{}px", width.round()))); + op = "Resizing"; } if geometry.size.height != height { p.push(crate::common::PropertyChange::new("height", format!("{}px", height.round()))); + op = "Resizing"; } - p + (p, op) }; if !properties.is_empty() { @@ -216,6 +235,7 @@ fn change_geometry_of_selected_element(x: f32, y: f32, width: f32, height: f32) }; send_message_to_lsp(crate::common::PreviewToLspMessage::UpdateElement { + label: Some(format!("{op} element")), position: VersionedPosition::new(VersionedUrl::new(url, version), selected.offset), properties, }); diff --git a/tools/lsp/preview/ui.rs b/tools/lsp/preview/ui.rs index 1ea6fad7e..124795257 100644 --- a/tools/lsp/preview/ui.rs +++ b/tools/lsp/preview/ui.rs @@ -47,6 +47,7 @@ pub fn create_ui(style: String, experimental: bool) -> Result { + root.selected-element-delete(); + } + select-behind(x, y, c, f) => { root.select-behind(x, y, c, f); } } } diff --git a/tools/lsp/ui/main.slint b/tools/lsp/ui/main.slint index 743f25e0d..b0cca4c35 100644 --- a/tools/lsp/ui/main.slint +++ b/tools/lsp/ui/main.slint @@ -26,6 +26,7 @@ export component PreviewUi inherits Window { pure callback can-drop(/* component_type */ string, /* x */ length, /* y */ length) -> bool; callback drop(/* component_type */ string, /* import_path */ string, /* is_layout */ bool, /* x */ length, /* y */ length); callback selected-element-update-geometry(/* x */ length, /* y */ length, /* width */ length, /* height */ length); + callback selected-element-delete(); callback select-at(/* x */ length, /* y */ length, /* enter_component? */ bool); callback select-behind(/* x */ length, /* y */ length, /* enter_component* */ bool, /* reverse */ bool); callback show-document(/* url */ string, /* line */ int, /* column */ int); @@ -65,6 +66,9 @@ export component PreviewUi inherits Window { text: root.experimental ? @tr("Design Mode") : @tr("Pick Mode"); checkable: true; checked <=> root.design-mode; + clicked() => { + key-handler.focus(); + } } Text { @@ -119,6 +123,7 @@ export component PreviewUi inherits Window { select-at(x, y, enter_component) => { root.select-at(x, y, enter_component); } selected-element-update-geometry(x, y, w, h) => { root.selected-element-update-geometry(x, y, w, h); } + selected-element-delete() => { root.selected-element-delete(); } select-behind(x, y, stay_in_file, reverse) => { root.select-behind(x, y, stay_in_file, reverse); } show-document(url, line, column) => { root.show-document(url, line, column); } unselect() => { root.unselect(); } @@ -129,4 +134,16 @@ export component PreviewUi inherits Window { } } } + + key-handler := FocusScope { + enabled: root.design-mode && root.experimental; + + key-released(event) => { + if event.text == Key.Delete { + root.selected-element-delete(); + return accept; + } + reject + } + } } diff --git a/tools/lsp/wasm_main.rs b/tools/lsp/wasm_main.rs index 6904af522..2451fa108 100644 --- a/tools/lsp/wasm_main.rs +++ b/tools/lsp/wasm_main.rs @@ -3,7 +3,7 @@ #![cfg(target_arch = "wasm32")] -mod common; +pub mod common; mod fmt; mod language; pub mod lsp_ext; @@ -14,7 +14,7 @@ pub mod util; use common::{LspToPreviewMessage, Result, VersionedUrl}; use i_slint_compiler::CompilerConfiguration; use js_sys::Function; -pub use language::{Context, DocumentCache, RequestHandler}; +pub use language::{element_edit, Context, DocumentCache, RequestHandler}; use lsp_types::Url; use serde::Serialize; use std::cell::RefCell; @@ -226,6 +226,30 @@ pub fn create( }) } +fn send_workspace_edit( + server_notifier: ServerNotifier, + label: Option, + edit: Result, +) { + let Ok(edit) = edit else { + return; + }; + + wasm_bindgen_futures::spawn_local(async move { + let fut = server_notifier.send_request::( + lsp_types::ApplyWorkspaceEditParams { label, edit }, + ); + if let Ok(fut) = fut { + // We ignore errors: If the LSP can not be reached, then all is lost + // anyway. The other thing that might go wrong is that our Workspace Edit + // refers to some outdated text. In that case the update is most likely + // in flight already and will cause the preview to re-render, which also + // invalidates all our state + let _ = fut.await; + } + }); +} + #[wasm_bindgen] impl SlintServer { #[cfg(all(feature = "preview-engine", feature = "preview-external"))] @@ -272,47 +296,25 @@ impl SlintServer { crate::language::request_state(&self.ctx); } M::AddComponent { label, component } => { - let edit = crate::language::add_component(&self.ctx, component); - let sn = self.ctx.server_notifier.clone(); - - if let Ok(edit) = edit { - wasm_bindgen_futures::spawn_local(async move { - let fut = sn.send_request::( - lsp_types::ApplyWorkspaceEditParams { label, edit }, - ); - if let Ok(fut) = fut { - // We ignore errors: If the LSP can not be reached, then all is lost - // anyway. The other thing that might go wrong is that our Workspace Edit - // refers to some outdated text. In that case the update is most likely - // in flight already and will cause the preview to re-render, which also - // invalidates all our state - let _ = fut.await; - } - }); - } + send_workspace_edit( + self.ctx.server_notifier.clone(), + label, + element_edit::add_component(&self.ctx, component), + ); } - M::UpdateElement { position, properties } => { - let edit = crate::language::update_element(&self.ctx, position, properties); - let sn = self.ctx.server_notifier.clone(); - - if let Ok(edit) = edit { - wasm_bindgen_futures::spawn_local(async move { - let fut = sn.send_request::( - lsp_types::ApplyWorkspaceEditParams { - label: Some("Element update".to_string()), - edit, - }, - ); - if let Ok(fut) = fut { - // We ignore errors: If the LSP can not be reached, then all is lost - // anyway. The other thing that might go wrong is that our Workspace Edit - // refers to some outdated text. In that case the update is most likely - // in flight already and will cause the preview to re-render, which also - // invalidates all our state - let _ = fut.await; - } - }); - } + M::UpdateElement { label, position, properties } => { + send_workspace_edit( + self.ctx.server_notifier.clone(), + label, + element_edit::update_element(&self.ctx, position, properties), + ); + } + M::RemoveElement { label, position } => { + send_workspace_edit( + self.ctx.server_notifier.clone(), + label, + element_edit::remove_element(&self.ctx, position), + ); } } Ok(())