From 6a49a91fccd4fbc76058754fc335c3d2819ce1d9 Mon Sep 17 00:00:00 2001 From: Tobias Hunger Date: Mon, 19 Aug 2024 15:47:30 +0000 Subject: [PATCH 1/3] compiiler: Treat SOT and EOT as whitespace --- internal/compiler/lexer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/compiler/lexer.rs b/internal/compiler/lexer.rs index 2ae5b3b7694..7e657ecf9ab 100644 --- a/internal/compiler/lexer.rs +++ b/internal/compiler/lexer.rs @@ -44,7 +44,7 @@ pub fn lex_whitespace(text: &str, _: &mut LexState) -> usize { let mut len = 0; let chars = text.chars(); for c in chars { - if !c.is_whitespace() { + if !c.is_whitespace() && !['\u{0002}', '\u{0003}'].contains(&c) { break; } len += c.len_utf8(); From ae616d4b6e37b0988f2165492732d5d3454a2aac Mon Sep 17 00:00:00 2001 From: Tobias Hunger Date: Mon, 19 Aug 2024 15:47:30 +0000 Subject: [PATCH 2/3] compiler: Mark up areas covered by slint macros Add `Start-of-text` ASCII value in the place of the opening brace of the slint macro and `End-of-text` in place of the closing bracket. ASCII chars are one byte, so they will always work :-) --- internal/compiler/lexer.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/compiler/lexer.rs b/internal/compiler/lexer.rs index 7e657ecf9ab..83582f26bf8 100644 --- a/internal/compiler/lexer.rs +++ b/internal/compiler/lexer.rs @@ -421,6 +421,14 @@ pub fn extract_rust_macro(rust_source: String) -> Option { *c = b' ' } } + + if start > 0 { + bytes[start - 1] = 2; + } + if end < bytes.len() { + bytes[end] = 3; + } + for c in &mut bytes[end..] { if *c != b'\n' { *c = b' ' From 34bceaf712792ed765c0c3494f99cbe98a86cae3 Mon Sep 17 00:00:00 2001 From: Tobias Hunger Date: Wed, 6 Mar 2024 13:16:18 +0100 Subject: [PATCH 3/3] lsp: Offer to populate empty documents --- examples/maps/main.rs | 75 ------------ internal/compiler/lexer.rs | 5 +- tools/lsp/language.rs | 242 +++++++++++++++++++++++++++++++++++-- 3 files changed, 236 insertions(+), 86 deletions(-) diff --git a/examples/maps/main.rs b/examples/maps/main.rs index fa167aad530..dd4a5ebc26a 100644 --- a/examples/maps/main.rs +++ b/examples/maps/main.rs @@ -16,81 +16,6 @@ use std::rc::Rc; const TILE_SIZE: isize = 256; -slint::slint! { -import { Slider } from "std-widgets.slint"; -export struct Tile { x: length, y: length, tile: image} - -export component MainUI inherits Window { - callback flicked(length, length); - callback zoom-changed(float); - callback zoom-in(length, length); - callback zoom-out(length, length); - callback link-clicked(); - min-height: 500px; - min-width: 500px; - - out property visible_width: fli.width; - out property visible_height: fli.height; - - in-out property zoom <=> sli.value; - - in property <[Tile]> tiles; - - public function set_viewport(ox: length, oy: length, width: length, height: length) { - fli.viewport-x = ox; - fli.viewport-y = oy; - fli.viewport-width = width; - fli.viewport-height = height; - } - - VerticalLayout { - fli := Flickable { - for t in tiles: Image { - x: t.x; - y: t.y; - source: t.tile; - } - flicked => { - root.flicked(fli.viewport-x, fli.viewport-y); - } - TouchArea { - scroll-event(e) => { - if e.delta-y > 0 { - root.zoom-in(self.mouse-x + fli.viewport-x, self.mouse-y + fli.viewport-y); - return accept; - } else if e.delta-y < 0 { - root.zoom-out(self.mouse-x + fli.viewport-x, self.mouse-y + fli.viewport-y); - return accept; - } - return reject; - } - } - } - - HorizontalLayout { - sli := Slider { - minimum: 1; - maximum: 19; - released => { - zoom-changed(self.value); - } - } - } - } - - Text { - text: "Map data from OpenStreetMap"; - x: fli.x + (fli.width) - (self.width) - 3px; - y: fli.y + (fli.height) - (self.height) - 3px; - TouchArea { - clicked => { - root.link-clicked(); - } - } - } -} -} - #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] struct TileCoordinate { z: u32, diff --git a/internal/compiler/lexer.rs b/internal/compiler/lexer.rs index 83582f26bf8..339a6b4f7ac 100644 --- a/internal/compiler/lexer.rs +++ b/internal/compiler/lexer.rs @@ -415,6 +415,7 @@ fn test_locate_rust_macro() { /// string to preserve line and column number. pub fn extract_rust_macro(rust_source: String) -> Option { let core::ops::Range { start, end } = locate_slint_macro(&rust_source).next()?; + eprintln!("slint macro covers offsets: {start} - {end}"); let mut bytes = rust_source.into_bytes(); for c in &mut bytes[..start] { if *c != b'\n' { @@ -423,13 +424,15 @@ pub fn extract_rust_macro(rust_source: String) -> Option { } if start > 0 { + eprintln!("Placed SOT at {}", start - 1); bytes[start - 1] = 2; } if end < bytes.len() { + eprintln!("Placed EOT at {}", end); bytes[end] = 3; } - for c in &mut bytes[end..] { + for c in &mut bytes[end + 1..] { if *c != b'\n' { *c = b' ' } diff --git a/tools/lsp/language.rs b/tools/lsp/language.rs index 7f1d1576103..fad18d67d9a 100644 --- a/tools/lsp/language.rs +++ b/tools/lsp/language.rs @@ -16,7 +16,9 @@ use crate::util; #[cfg(target_arch = "wasm32")] use crate::wasm_prelude::*; 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, TextRange, +}; use i_slint_compiler::{diagnostics::BuildDiagnostics, langtype::Type}; use lsp_types::request::{ CodeActionRequest, CodeLensRequest, ColorPresentationRequest, Completion, DocumentColor, @@ -38,10 +40,12 @@ use std::path::PathBuf; use std::pin::Pin; use std::rc::Rc; +const POPULATE_COMMAND: &str = "slint/populate"; const SHOW_PREVIEW_COMMAND: &str = "slint/showPreview"; fn command_list() -> Vec { vec![ + POPULATE_COMMAND.into(), #[cfg(any(feature = "preview-builtin", feature = "preview-external"))] SHOW_PREVIEW_COMMAND.into(), ] @@ -60,6 +64,20 @@ fn create_show_preview_command( ) } +fn create_populate_command( + uri: lsp_types::Url, + version: i_slint_compiler::diagnostics::SourceFileVersion, + title: String, + text: String, +) -> Command { + let text_document = lsp_types::OptionalVersionedTextDocumentIdentifier { uri, version }; + Command::new( + title, + POPULATE_COMMAND.into(), + Some(vec![serde_json::to_value(text_document).unwrap(), text.into()]), + ) +} + #[cfg(any(feature = "preview-external", feature = "preview-engine"))] pub fn request_state(ctx: &std::rc::Rc) { let document_cache = ctx.document_cache.borrow(); @@ -281,10 +299,14 @@ pub fn register_request_handlers(rh: &mut RequestHandler) { }); Ok(result) }); - rh.register::(|params, _ctx| async move { + rh.register::(|params, ctx| async move { if params.command.as_str() == SHOW_PREVIEW_COMMAND { #[cfg(any(feature = "preview-builtin", feature = "preview-external"))] - show_preview_command(¶ms.arguments, &_ctx)?; + show_preview_command(¶ms.arguments, &ctx)?; + return Ok(None::); + } + if params.command.as_str() == POPULATE_COMMAND { + populate_command(¶ms.arguments, &ctx).await?; return Ok(None::); } Ok(None::) @@ -484,6 +506,125 @@ pub fn show_preview_command( Ok(()) } +fn populate_command_range(node: &SyntaxNode) -> Option { + let range = node.text_range(); + + let start_offset = node + .text() + .find_char('\u{0002}') + .and_then(|s| s.checked_add(1.into())) + .unwrap_or(range.start()); + let end_offset = node.text().find_char('\u{0003}').unwrap_or(range.end()); + + eprintln!(" Populate range: {start_offset:?} - {end_offset:?}"); + + (start_offset <= end_offset) + .then_some(util::map_range(&node.source_file, TextRange::new(start_offset, end_offset))) +} + +pub async fn populate_command( + params: &[serde_json::Value], + ctx: &Rc, +) -> Result { + let text_document = + serde_json::from_value::( + params + .first() + .ok_or_else(|| LspError { + code: LspErrorCode::InvalidParameter, + message: "No textdocument provided".into(), + })? + .clone(), + ) + .map_err(|_| LspError { + code: LspErrorCode::InvalidParameter, + message: "First paramater is not a OptionalVersionedTextDocumentIdentifier".into(), + })?; + let new_text = serde_json::from_value::( + params + .get(1) + .ok_or_else(|| LspError { + code: LspErrorCode::InvalidParameter, + message: "No code to insert".into(), + })? + .clone(), + ) + .map_err(|_| LspError { + code: LspErrorCode::InvalidParameter, + message: "Invalid second parameter".into(), + })?; + + let edit = { + let document_cache = &mut ctx.document_cache.borrow_mut(); + let uri = text_document.uri; + let version = document_cache.document_version(&uri); + + if let Some(source_version) = text_document.version { + if let Some(current_version) = version { + if current_version != source_version { + return Err(LspError { + code: LspErrorCode::InvalidParameter, + message: "Document version mismatch. Please refresh your command data" + .into(), + }); + } + } else { + return Err(LspError { + code: LspErrorCode::InvalidParameter, + message: format!("Document with uri {uri} not found in cache").into(), + }); + } + } + + let Some(doc) = document_cache.get_document(&uri) else { + return Err(LspError { + code: LspErrorCode::InvalidParameter, + message: "Document not in cache".into(), + }); + }; + let Some(node) = &doc.node else { + return Err(LspError { + code: LspErrorCode::InvalidParameter, + message: "Document has no node".into(), + }); + }; + + let Some(range) = populate_command_range(&node) else { + return Err(LspError { + code: LspErrorCode::InvalidParameter, + message: "No slint code range in document".into(), + }); + }; + + let edit = lsp_types::TextEdit { range, new_text }; + common::create_workspace_edit(uri, version, vec![edit]) + }; + + let response = ctx + .server_notifier + .send_request::( + lsp_types::ApplyWorkspaceEditParams { label: Some("Populate empty file".into()), edit }, + ) + .map_err(|_| LspError { + code: LspErrorCode::RequestFailed, + message: "Failed to send populate edit".into(), + })? + .await + .map_err(|_| LspError { + code: LspErrorCode::RequestFailed, + message: "Failed to send populate edit".into(), + })?; + + if !response.applied { + return Err(LspError { + code: LspErrorCode::RequestFailed, + message: "Failed to apply population edit".into(), + }); + } + + Ok(serde_json::to_value(()).expect("Failed to serialize ()!")) +} + pub(crate) async fn reload_document_impl( ctx: Option<&Rc>, mut content: String, @@ -942,25 +1083,106 @@ fn get_code_lenses( document_cache: &mut DocumentCache, text_document: &lsp_types::TextDocumentIdentifier, ) -> Option> { - if cfg!(any(feature = "preview-builtin", feature = "preview-external")) { - let doc = document_cache.get_document(&text_document.uri)?; + let doc = document_cache.get_document(&text_document.uri)?; + let version = document_cache.document_version(&text_document.uri); - let inner_components = doc.inner_components.clone(); + let mut result = vec![]; - let mut r = vec![]; + if cfg!(any(feature = "preview-builtin", feature = "preview-external")) { + let inner_components = doc.inner_components.clone(); // Handle preview lens - r.extend(inner_components.iter().filter(|c| !c.is_global()).filter_map(|c| { + result.extend(inner_components.iter().filter(|c| !c.is_global()).filter_map(|c| { Some(CodeLens { range: util::map_node(&c.root_element.borrow().debug.first()?.node)?, command: Some(create_show_preview_command(true, &text_document.uri, c.id.as_str())), data: None, }) })); + } - Some(r) - } else { + if let Some(node) = &doc.node { + eprintln!("Code lenses: Have a document"); + let has_non_ws_token = node.children_with_tokens().any(|nt| { + eprintln!(" token or node of kind: {:?}", nt.kind()); + nt.kind() != SyntaxKind::Whitespace && nt.kind() != SyntaxKind::Eof + }); + if !has_non_ws_token { + eprintln!("Code lenses: Have a document, without any contents"); + if let Some(range) = populate_command_range(&node) { + eprintln!("Code lenses: Have a document, without any contents, range: {range:?}"); + result.push(CodeLens { + range: range.clone(), + command: Some(create_populate_command( + text_document.uri.clone(), + version.clone(), + "Start with Hello World!".to_string(), + r#" +import { AboutSlint, Button, VerticalBox } from "std-widgets.slint"; + +export component Demo { + VerticalBox { + alignment: start; + Text { + text: "Hello World!"; + font-size: 24px; + horizontal-alignment: center; + } + AboutSlint { + preferred-height: 150px; + } + HorizontalLayout { alignment: center; Button { text: "OK!"; } } + } +} +"# + .to_string(), + )), + data: None, + }); + result.push(CodeLens { + range, + command: Some(create_populate_command( + text_document.uri.clone(), + version.clone(), + "Start with Window".to_string(), + r#" +export component MainWindow inherits Window { + title: "Slint Main Window"; + + min-width: 200px; + min-height: 200px; +} +"# + .to_string(), + )), + data: None, + }); + result.push(CodeLens { + range, + command: Some(create_populate_command( + text_document.uri.clone(), + version.clone(), + "Start with fixed size Window".to_string(), + r#" +export component MainWindow inherits Window { + title: "Slint Main Window"; + + width: 200px; + height: 200px; +} +"# + .to_string(), + )), + data: None, + }); + } + } + } + + if result.is_empty() { None + } else { + Some(result) } }