From d53cebd3e1f1f6df4139429346e199f4c9819bd2 Mon Sep 17 00:00:00 2001 From: Tobias Hunger Date: Mon, 11 Sep 2023 15:00:32 +0200 Subject: [PATCH] lsp: Get signalled by the preview --- editors/vscode/package.json | 5 - editors/vscode/src/browser.ts | 16 +- editors/vscode/src/common.ts | 54 ++- editors/vscode/src/extension.ts | 45 +- editors/vscode/src/wasm_preview.ts | 415 +++++-------------- internal/compiler/typeloader.rs | 7 + tools/lsp/Cargo.toml | 22 +- tools/lsp/common.rs | 39 ++ tools/lsp/language.rs | 22 +- tools/lsp/main.rs | 197 ++++++++- tools/lsp/preview.rs | 115 +++++- tools/lsp/preview/native.rs | 87 +--- tools/lsp/preview/ui.rs | 12 +- tools/lsp/preview/wasm.rs | 477 ++++------------------ tools/lsp/wasm_main.rs | 88 +++- tools/slintpad/package.json | 5 +- tools/slintpad/src/editor_widget.ts | 124 +----- tools/slintpad/src/index.ts | 55 +-- tools/slintpad/src/lsp.ts | 25 +- tools/slintpad/src/preview.ts | 3 +- tools/slintpad/src/preview_widget.ts | 239 +---------- tools/slintpad/src/shared/lsp_commands.ts | 10 - tools/slintpad/src/worker/lsp_worker.ts | 127 +++--- tools/slintpad/tsconfig.default.json | 3 +- tools/slintpad/vite.config.ts | 4 + 25 files changed, 776 insertions(+), 1420 deletions(-) diff --git a/editors/vscode/package.json b/editors/vscode/package.json index fcb0146afc..e7d2c430a3 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -65,11 +65,6 @@ "category": "Slint", "icon": "$(preview)" }, - { - "command": "slint.toggleDesignMode", - "title": "Toggle Design Mode in Slint Preview (experimental)", - "category": "Slint" - }, { "command": "slint.reload", "title": "Restart server", diff --git a/editors/vscode/src/browser.ts b/editors/vscode/src/browser.ts index d7d6d2f246..995863d79a 100644 --- a/editors/vscode/src/browser.ts +++ b/editors/vscode/src/browser.ts @@ -24,21 +24,7 @@ function startClient( //let args = vscode.workspace.getConfiguration('slint').get<[string]>('lsp-args'); // Options to control the language client - const clientOptions = common.languageClientOptions( - (args: any) => { - wasm_preview.showPreview( - context, - vscode.Uri.parse(args[0], true), - args[1], - ); - return true; - }, - (_) => { - wasm_preview.toggleDesignMode(); - return true; - }, - ); - + const clientOptions = common.languageClientOptions(); clientOptions.synchronize = {}; clientOptions.initializationOptions = {}; diff --git a/editors/vscode/src/common.ts b/editors/vscode/src/common.ts index 717bcc8743..b6225e1a37 100644 --- a/editors/vscode/src/common.ts +++ b/editors/vscode/src/common.ts @@ -1,6 +1,8 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial +// cSpell: ignore codespaces + // This file is common code shared by both vscode plugin entry points import * as vscode from "vscode"; @@ -40,7 +42,7 @@ export class ClientHandle { set client(c: BaseLanguageClient | null) { this.#client = c; for (let u of this.#updaters) { - u(c); + u(this.#client); } } @@ -50,12 +52,18 @@ export class ClientHandle { } async stop() { - if (this.#client) { + let to_stop = this.client; + this.client = null; + for (let u of this.#updaters) { + u(this.#client); + } + + if (to_stop) { // mark as stopped so that we don't detect it as a crash - Object.defineProperty(this.#client, "slint_stopped", { + Object.defineProperty(to_stop, "slint_stopped", { value: true, }); - await this.#client.stop(); + await to_stop.stop(); } } } @@ -94,25 +102,10 @@ export function setServerStatus( // Set up our middleware. It is used to redirect/forward to the WASM preview // as needed and makes the triggering side so much simpler! -export function languageClientOptions( - showPreview: (args: any) => boolean, - toggleDesignMode: (args: any) => boolean, -): LanguageClientOptions { +export function languageClientOptions(): LanguageClientOptions { return { documentSelector: [{ language: "slint" }, { language: "rust" }], middleware: { - executeCommand(command: string, args: any, next: any) { - if (command === "slint/showPreview") { - if (showPreview(args)) { - return; - } - } else if (command === "slint/toggleDesignMode") { - if (toggleDesignMode(args)) { - return; - } - } - return next(command, args); - }, async provideCodeActions( document: vscode.TextDocument, range: vscode.Range, @@ -160,7 +153,7 @@ export function activate( setServerStatus(params, statusBar), ); } - wasm_preview.initClientForPreview(cl); + wasm_preview.initClientForPreview(context, cl); properties_provider.refresh_view(); }); @@ -171,7 +164,7 @@ export function activate( "workspace/didChangeConfiguration", { settings: "" }, ); - wasm_preview.refreshPreview(); + wasm_preview.update_configuration(); } }); @@ -182,7 +175,7 @@ export function activate( }); context.subscriptions.push( - vscode.commands.registerCommand("slint.showPreview", function () { + vscode.commands.registerCommand("slint.showPreview", async function () { let ae = vscode.window.activeTextEditor; if (!ae) { return; @@ -191,11 +184,6 @@ export function activate( lsp_commands.showPreview(ae.document.uri.toString(), ""); }), ); - context.subscriptions.push( - vscode.commands.registerCommand("slint.toggleDesignMode", function () { - lsp_commands.toggleDesignMode(); - }), - ); context.subscriptions.push( vscode.commands.registerCommand("slint.reload", async function () { @@ -225,7 +213,6 @@ export function activate( ) { return; } - wasm_preview.refreshPreview(ev); // Send a request for properties information after passing through the // event loop once to make sure the LSP got signaled to update. @@ -234,6 +221,15 @@ export function activate( }, 1); }); + vscode.workspace.onDidChangeConfiguration(async (ev) => { + if (ev.affectsConfiguration("slint")) { + client.client?.sendNotification( + "workspace/didChangeConfiguration", + { settings: "" }, + ); + } + }); + return [statusBar, properties_provider]; } diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index e990797a35..46efe76502 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -3,14 +3,13 @@ // This file is the entry point for the vscode extension (not the browser one) -// cSpell: ignore codespaces gnueabihf vsix +// cSpell: ignore codespace codespaces gnueabihf vsix import * as path from "path"; import { existsSync } from "fs"; import * as vscode from "vscode"; import { PropertiesViewProvider } from "./properties_webview"; -import * as wasm_preview from "./wasm_preview"; import * as common from "./common"; import { @@ -141,36 +140,12 @@ function startClient( debug: { command: serverModule, options: options, args: args }, }; - const clientOptions = common.languageClientOptions( - (args: any) => { - if ( - vscode.workspace - .getConfiguration("slint") - .get("preview.providedByEditor") - ) { - wasm_preview.showPreview( - context, - vscode.Uri.parse(args[0], true), - args[1], - ); - return true; - } - return false; - }, - (_) => { - if ( - vscode.workspace - .getConfiguration("slint") - .get("preview.providedByEditor") - ) { - wasm_preview.toggleDesignMode(); - return true; - } - return false; - }, - ); - + // Add setup common between native and wasm LSP to common.setup_client_handle! client.add_updater((cl) => { + cl?.onNotification(common.serverStatus, (params: any) => + common.setServerStatus(params, statusBar), + ); + cl?.onDidChangeState((event) => { let properly_stopped = cl.hasOwnProperty("slint_stopped"); if ( @@ -194,7 +169,7 @@ function startClient( "slint-lsp", "Slint LSP", serverOptions, - clientOptions, + common.languageClientOptions(), ); common.prepare_client(cl); @@ -203,6 +178,11 @@ function startClient( } export function activate(context: vscode.ExtensionContext) { + // Disable native preview in Codespace. + // + // We want to have a good default (WASM preview), but we also need to + // support users that have special setup in place that allows them to run + // the native previewer remotely. if (process.env.hasOwnProperty("CODESPACES")) { vscode.workspace .getConfiguration("slint") @@ -212,6 +192,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.ConfigurationTarget.Global, ); } + [statusBar, properties_provider] = common.activate(context, (cl, ctx) => startClient(cl, ctx), ); diff --git a/editors/vscode/src/wasm_preview.ts b/editors/vscode/src/wasm_preview.ts index e22c963901..005926bcf7 100644 --- a/editors/vscode/src/wasm_preview.ts +++ b/editors/vscode/src/wasm_preview.ts @@ -1,201 +1,92 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial -import { Uri, TextDocumentShowOptions } from "vscode"; +import { Uri } from "vscode"; + import * as vscode from "vscode"; import { BaseLanguageClient } from "vscode-languageclient"; let previewPanel: vscode.WebviewPanel | null = null; -let previewUrl: Uri | null = null; -let previewAccessedFiles = new Set(); -let previewComponent: string = ""; -let queuedPreviewMsg: any | null = null; -let previewBusy = false; -let uriMapping = new Map(); +let to_lsp_queue: object[] = []; + +let language_client: BaseLanguageClient | null = null; + +function use_wasm_preview(): boolean { + return vscode.workspace + .getConfiguration("slint") + .get("preview.providedByEditor", false); +} + +export function update_configuration() { + if (language_client) { + send_to_lsp({ + PreviewTypeChanged: { + is_external: previewPanel !== null || use_wasm_preview(), + }, + }); + } +} /// Initialize the callback on the client to make the web preview work -export function initClientForPreview(client: BaseLanguageClient | null) { - client?.onRequest("slint/preview_message", async (msg: any) => { - if (previewPanel) { - // map urls to webview URL - if (msg.command === "highlight") { - msg.data.path = previewPanel.webview - .asWebviewUri(Uri.parse(msg.data.path, true)) - .toString(); - } - previewPanel.webview.postMessage(msg); - } - return; - }); -} - -function urlConvertToWebview(webview: vscode.Webview, url: Uri): Uri { - let webview_uri = webview.asWebviewUri(url); - uriMapping.set(webview_uri.toString(), url.toString()); - return webview_uri; -} - -function reload_preview(url: Uri, content: string, component: string) { - if (!previewPanel) { - return; - } - if (component) { - content += - "\nexport component _Preview inherits " + component + " {}\n"; - } - previewAccessedFiles.clear(); - uriMapping.clear(); - - let webview_uri = urlConvertToWebview(previewPanel.webview, url).toString(); - previewAccessedFiles.add(webview_uri); - const style = vscode.workspace - .getConfiguration("slint") - .get<[string]>("preview.style"); - const msg = { - command: "preview", - base_url: url.toString(), - webview_uri: webview_uri, - component: component, - content: content, - style: style, - }; - if (previewBusy) { - queuedPreviewMsg = msg; - } else { - previewPanel.webview.postMessage(msg); - previewBusy = true; - } -} - -export async function refreshPreview(event?: vscode.TextDocumentChangeEvent) { - if (!previewPanel || !previewUrl) { - return; - } - if ( - event && - !previewAccessedFiles.has( - urlConvertToWebview( - previewPanel.webview, - event.document.uri, - ).toString(), - ) - ) { - return; - } - - let content_str; - if (event && event.document.uri === previewUrl) { - content_str = event.document.getText(); - if (event.document.languageId === "rust") { - content_str = extract_rust_macro(content_str); - } - } else { - content_str = await getDocumentSource(previewUrl); - } - reload_preview(previewUrl, content_str, previewComponent); -} - -/// Show the preview for the given path and component -export async function toggleDesignMode() { - previewPanel?.webview.postMessage({ - command: "toggle_design_mode", - }); -} - -/// Show the preview for the given path and component -export async function showPreview( +export function initClientForPreview( context: vscode.ExtensionContext, - url: Uri, - component: string, + client: BaseLanguageClient | null, ) { - previewUrl = url; - previewComponent = component; + language_client = client; - if (previewPanel) { - previewPanel.reveal(vscode.ViewColumn.Beside); - } else { - // Create and show a new webview - const panel = vscode.window.createWebviewPanel( - "slint-preview", - "Slint Preview", - vscode.ViewColumn.Beside, - { enableScripts: true, retainContextWhenHidden: true }, - ); - initPreviewPanel(context, panel); - } + if (client) { + update_configuration(); - let content_str = await getDocumentSource(url); - reload_preview(url, content_str, previewComponent); -} - -async function getDocumentSource(url: Uri): Promise { - // FIXME: is there a faster way to get the document - let x = vscode.workspace.textDocuments.find( - (d) => d.uri.toString() === url.toString(), - ); - let source; - if (x) { - source = x.getText(); - if (x.languageId === "rust") { - source = extract_rust_macro(source); - } - } else { - source = new TextDecoder().decode( - await vscode.workspace.fs.readFile(url), - ); - if (url.path.endsWith(".rs")) { - source = extract_rust_macro(source); - } - } - return source; -} - -function extract_rust_macro(source: string): string { - let match; - const re = /slint!\s*([\{\(\[])/g; - - let last = 0; - let result = ""; - - while ((match = re.exec(source)) !== null) { - let start = match.index + match[0].length; - let end = source.length; - let level = 0; - let open = match[1]; - let close; - switch (open) { - case "(": - close = ")"; - break; - case "{": - close = "}"; - break; - case "[": - close = "]"; - break; - } - for (let i = start; i < source.length; i++) { - if (source.charAt(i) === open) { - level++; - } else if (source.charAt(i) === close) { - level--; - if (level < 0) { - end = i; - break; + client.onNotification("slint/lsp_to_preview", async (message: any) => { + if ("ShowPreview" in message) { + if (open_preview(context)) { + return; } } - } - result += source.slice(last, start).replace(/[^\n]/g, " "); - result += source.slice(start, end); - last = end; + previewPanel?.webview.postMessage({ + command: "slint/lsp_to_preview", + params: message, + }); + }); + + // Send messages that got queued while LS was down... + for (const m of to_lsp_queue) { + send_to_lsp(m); + } + to_lsp_queue = []; } - result += source.slice(last).replace(/[^\n]/g, " "); - return result; +} + +function send_to_lsp(message: any): boolean { + if (language_client) { + language_client.sendNotification("slint/preview_to_lsp", message); + } else { + to_lsp_queue.push(message); + } + + return language_client !== null; +} + +function open_preview(context: vscode.ExtensionContext): boolean { + if (previewPanel !== null) { + return false; + } + + // Create and show a new webview + const panel = vscode.window.createWebviewPanel( + "slint-preview", + "Slint Preview", + vscode.ViewColumn.Beside, + { enableScripts: true, retainContextWhenHidden: true }, + ); + previewPanel = initPreviewPanel(context, panel); + + return true; } function getPreviewHtml(slint_wasm_preview_url: Uri): string { - return ` + const result = ` @@ -203,181 +94,72 @@ function getPreviewHtml(slint_wasm_preview_url: Uri): string { Slint Preview -
- + `; + + return result; } export class PreviewSerializer implements vscode.WebviewPanelSerializer { context: vscode.ExtensionContext; + constructor(context: vscode.ExtensionContext) { this.context = context; } + async deserializeWebviewPanel( webviewPanel: vscode.WebviewPanel, - state: any, + _state: any, ) { - initPreviewPanel(this.context, webviewPanel); - if (state) { - previewUrl = Uri.parse(state.base_url, true); - - if (previewUrl) { - let content_str = await getDocumentSource(previewUrl); - previewComponent = state.component ?? ""; - reload_preview(previewUrl, content_str, previewComponent); - } - } + previewPanel = initPreviewPanel(this.context, webviewPanel); + //// How can we load this state? We can not query the necessary data... } } function initPreviewPanel( context: vscode.ExtensionContext, panel: vscode.WebviewPanel, -) { - previewPanel = panel; +): vscode.WebviewPanel { // we will get a preview_ready when the html is loaded and message are ready to be sent - previewBusy = true; panel.webview.onDidReceiveMessage( async (message) => { switch (message.command) { - case "load_file": - let canonical = Uri.parse(message.url, true).toString(); - previewAccessedFiles.add(canonical); - let content_str = undefined; - let x = vscode.workspace.textDocuments.find( - (d) => - urlConvertToWebview( - panel.webview, - d.uri, - ).toString() === canonical, - ); - if (x) { - content_str = x.getText(); - } - panel.webview.postMessage({ - command: "file_loaded", - url: message.url, - content: content_str, - }); - return; case "preview_ready": - if (queuedPreviewMsg) { - panel.webview.postMessage(queuedPreviewMsg); - queuedPreviewMsg = null; - } else { - previewBusy = false; - } + send_to_lsp({ RequestState: { unused: true } }); return; - case "element_selected": { - const d = message.data; - - const inside_uri = Uri.parse(d.url); - const range = new vscode.Range( - new vscode.Position( - d.start.line - 1, - d.start.column - 1, - ), - new vscode.Position( - d.start.line - 1, - d.start.column - 1, - ), // Do not use range! - ); - const outside_uri = Uri.parse( - uriMapping.get(d.url) ?? - Uri.file(inside_uri.fsPath).toString(), - ); - if (outside_uri.scheme !== "invalid") { - vscode.window.showTextDocument(outside_uri, { - selection: range, - preserveFocus: false, - } as TextDocumentShowOptions); - } + case "slint/preview_to_lsp": + send_to_lsp(message.params); return; - } } }, undefined, @@ -393,8 +175,11 @@ function initPreviewPanel( panel.onDidDispose( () => { previewPanel = null; + update_configuration(); }, undefined, context.subscriptions, ); + + return panel; } diff --git a/internal/compiler/typeloader.rs b/internal/compiler/typeloader.rs index bf9eb2f7bd..9dfc7da7f9 100644 --- a/internal/compiler/typeloader.rs +++ b/internal/compiler/typeloader.rs @@ -647,6 +647,13 @@ impl TypeLoader { pub fn all_documents(&self) -> impl Iterator + '_ { self.all_documents.docs.values() } + + /// Returns an iterator over all the loaded documents + pub fn all_file_documents( + &self, + ) -> impl Iterator + '_ { + self.all_documents.docs.iter() + } } fn get_native_style(all_loaded_files: &mut Vec) -> String { diff --git a/tools/lsp/Cargo.toml b/tools/lsp/Cargo.toml index 522bd006fc..897b6344b8 100644 --- a/tools/lsp/Cargo.toml +++ b/tools/lsp/Cargo.toml @@ -59,14 +59,20 @@ renderer-winit-skia-opengl= ["renderer-skia-opengl"] renderer-winit-skia-vulkan= ["renderer-skia-vulkan"] renderer-winit-software = ["renderer-software"] -## Enable the built-in preview, that will popup in a native window -preview = ["dep:slint", "dep:slint-interpreter", "dep:i-slint-core", "dep:i-slint-backend-selector", "dep:image", "preview-lense", "preview-api"] -## Enable the "Show Preview" lenses and action on components. -## When this feature is enabled without the "preview" feature, the lenses do nothing, but the client can still interpret the command -## to show the actual preview +## Enable support for previewing .slint files +preview = ["preview-builtin", "preview-external", "preview-engine"] +## [deprecated] Used to enable the "Show Preview" lenses and action on components. preview-lense = [] -## Open a notification channel so that the LSP can communicate with the preview (when the preview is handled by the client) -preview-api = [] +## [deprecated] Used to enable partial support for external previewers. +## Use "preview-external" (and maybe "preview-engine" if you want the LSP binary +## to provide an implementation of the external preview API when building for WASM) +preview-api = ["preview-external"] +## Build in the actual code to act as a preview for slint files. +preview-engine = ["dep:slint", "dep:slint-interpreter", "dep:i-slint-core", "dep:i-slint-backend-selector", "dep:image"] +## Build in the actual code to act as a preview for slint files. Does nothing in WASM! +preview-builtin = ["preview-engine"] +## Support the external preview optionally used by e.g. the VSCode plugin +preview-external = [] default = ["backend-qt", "backend-winit", "renderer-femtovg", "preview"] @@ -79,7 +85,7 @@ rowan = "0.15.5" serde = "1.0.118" serde_json = "1.0.60" -# for the preview +# for the preview-engine feature i-slint-backend-selector = { workspace = true, features = ["default"], optional = true } i-slint-core = { workspace = true, features = ["std"], optional = true } slint = { workspace = true, features = ["compat-1-2"], optional = true } diff --git a/tools/lsp/common.rs b/tools/lsp/common.rs index 3232f6df5d..03072afb16 100644 --- a/tools/lsp/common.rs +++ b/tools/lsp/common.rs @@ -6,6 +6,7 @@ use std::{ collections::HashMap, path::{Path, PathBuf}, + rc::Rc, }; pub type Error = Box; @@ -14,6 +15,8 @@ pub type Result = std::result::Result; /// API used by the LSP to talk to the Preview. The other direction uses the /// ServerNotifier pub trait PreviewApi { + fn set_use_external_previewer(&self, use_external: bool); + fn request_state(&self, ctx: &Rc); fn set_contents(&self, path: &Path, contents: &str); fn load_preview(&self, component: PreviewComponent); fn config_changed( @@ -69,3 +72,39 @@ pub enum LspToPreviewMessage { offset: u32, }, } + +#[allow(unused)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct Diagnostic { + pub message: String, + pub file: Option, + pub line: usize, + pub column: usize, + pub level: String, +} + +#[allow(unused)] +#[derive(Clone, serde::Deserialize, serde::Serialize)] +pub enum PreviewToLspMessage { + Status { + message: String, + health: crate::lsp_ext::Health, + }, + Diagnostics { + uri: lsp_types::Url, + diagnostics: Vec, + }, + ShowDocument { + file: String, + start_line: u32, + start_column: u32, + end_line: u32, + end_column: u32, + }, + PreviewTypeChanged { + is_external: bool, + }, + RequestState { + unused: bool, + }, // send all documents! +} diff --git a/tools/lsp/language.rs b/tools/lsp/language.rs index 0691a6a365..979316144b 100644 --- a/tools/lsp/language.rs +++ b/tools/lsp/language.rs @@ -58,7 +58,7 @@ fn command_list() -> Vec { vec![ QUERY_PROPERTIES_COMMAND.into(), REMOVE_BINDING_COMMAND.into(), - #[cfg(any(feature = "preview", feature = "preview-lense"))] + #[cfg(any(feature = "preview-builtin", feature = "preview-external"))] SHOW_PREVIEW_COMMAND.into(), SET_BINDING_COMMAND.into(), ] @@ -256,7 +256,7 @@ pub fn register_request_handlers(rh: &mut RequestHandler) { }); rh.register::(|params, ctx| async move { if params.command.as_str() == SHOW_PREVIEW_COMMAND { - #[cfg(feature = "preview")] + #[cfg(any(feature = "preview-builtin", feature = "preview-external"))] show_preview_command(¶ms.arguments, &ctx)?; return Ok(None::); } @@ -378,7 +378,7 @@ pub fn register_request_handlers(rh: &mut RequestHandler) { }); } -#[cfg(feature = "preview")] +#[cfg(any(feature = "preview-builtin", feature = "preview-external"))] pub fn show_preview_command(params: &[serde_json::Value], ctx: &Rc) -> Result<()> { let document_cache = &mut ctx.document_cache.borrow_mut(); let config = &document_cache.documents.compiler_config; @@ -780,7 +780,7 @@ fn get_code_actions( .and_then(syntax_nodes::Component::new) }); - #[cfg(feature = "preview-lense")] + #[cfg(any(feature = "preview-builtin", feature = "preview-external"))] { if let Some(component) = &component { if let Some(component_name) = @@ -847,12 +847,12 @@ fn get_code_actions( // whitespace in between for substituting the parent element with its // sub-elements, dropping its own properties, callbacks etc. fn is_sub_element(kind: SyntaxKind) -> bool { - match kind { - SyntaxKind::SubElement => true, - SyntaxKind::RepeatedElement => true, - SyntaxKind::ConditionalElement => true, - _ => false, - } + matches!( + kind, + SyntaxKind::SubElement + | SyntaxKind::RepeatedElement + | SyntaxKind::ConditionalElement + ) } let sub_elements = node .parent() @@ -1087,7 +1087,7 @@ fn get_code_lenses( document_cache: &mut DocumentCache, text_document: &lsp_types::TextDocumentIdentifier, ) -> Option> { - if cfg!(feature = "preview-lense") { + if cfg!(any(feature = "preview-builtin", feature = "preview-external")) { let filepath = uri_to_file(&text_document.uri)?; let doc = document_cache.documents.get_document(&filepath)?; diff --git a/tools/lsp/main.rs b/tools/lsp/main.rs index 43ce00ed37..789be01558 100644 --- a/tools/lsp/main.rs +++ b/tools/lsp/main.rs @@ -3,10 +3,13 @@ #![cfg(not(target_arch = "wasm32"))] +#[cfg(all(feature = "preview-engine", not(feature = "preview-builtin")))] +compile_error!("Feature preview-engine and preview-builtin need to be enabled together when building native LSP"); + mod common; mod language; pub mod lsp_ext; -#[cfg(feature = "preview")] +#[cfg(feature = "preview-engine")] mod preview; pub mod util; @@ -33,19 +36,93 @@ use std::task::{Poll, Waker}; struct Previewer { #[allow(unused)] server_notifier: ServerNotifier, + use_external_previewer: RefCell, + to_show: RefCell>, } impl PreviewApi for Previewer { - fn set_contents(&self, _path: &std::path::Path, _contents: &str) { - #[cfg(feature = "preview")] - preview::set_contents(_path, _contents.to_string()); + fn set_use_external_previewer(&self, _use_external: bool) { + // Only allow switching if both options are available + #[cfg(all(feature = "preview-builtin", feature = "preview-external"))] + { + self.use_external_previewer.replace(_use_external); + + if _use_external { + preview::close_ui(); + } + } } - fn load_preview(&self, _component: common::PreviewComponent) { - #[cfg(feature = "preview")] + fn request_state(&self, _ctx: &Rc) { + #[cfg(any(feature = "preview-builtin", feature = "preview-external"))] { - preview::open_ui(&self.server_notifier); - preview::load_preview(_component); + let documents = &_ctx.document_cache.borrow().documents; + + for (p, d) in documents.all_file_documents() { + let Some(node) = &d.node else { + continue; + }; + self.set_contents(p, &node.text().to_string()); + } + let cc = &documents.compiler_config; + let empty = String::new(); + self.config_changed( + cc.style.as_ref().unwrap_or(&empty), + &cc.include_paths, + &cc.library_paths, + ); + + if let Some(c) = self.to_show.take() { + self.load_preview(c); + } + } + } + + fn set_contents(&self, _path: &std::path::Path, _contents: &str) { + if *self.use_external_previewer.borrow() { + #[cfg(feature = "preview-external")] + let _ = self.server_notifier.send_notification( + "slint/lsp_to_preview".to_string(), + crate::common::LspToPreviewMessage::SetContents { + path: _path.to_string_lossy().to_string(), + contents: _contents.to_string(), + }, + ); + } else { + #[cfg(feature = "preview-builtin")] + preview::set_contents(_path, _contents.to_string()); + } + } + + fn load_preview(&self, component: common::PreviewComponent) { + self.to_show.replace(Some(component.clone())); + + if *self.use_external_previewer.borrow() { + #[cfg(feature = "preview-external")] + let _ = self.server_notifier.send_notification( + "slint/lsp_to_preview".to_string(), + crate::common::LspToPreviewMessage::ShowPreview { + path: component.path.to_string_lossy().to_string(), + component: component.component, + style: component.style.to_string(), + include_paths: component + .include_paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(), + library_paths: component + .library_paths + .iter() + .map(|(n, p)| (n.clone(), p.to_string_lossy().to_string())) + .collect(), + }, + ); + } else { + #[cfg(feature = "preview-builtin")] + { + preview::open_ui(&self.server_notifier); + preview::load_preview(component); + } } } @@ -55,14 +132,46 @@ impl PreviewApi for Previewer { _include_paths: &[PathBuf], _library_paths: &HashMap, ) { - #[cfg(feature = "preview")] - preview::config_changed(_style, _include_paths, _library_paths); + if *self.use_external_previewer.borrow() { + #[cfg(feature = "preview-external")] + let _ = self.server_notifier.send_notification( + "slint/lsp_to_preview".to_string(), + crate::common::LspToPreviewMessage::SetConfiguration { + style: _style.to_string(), + include_paths: _include_paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(), + library_paths: _library_paths + .iter() + .map(|(n, p)| (n.clone(), p.to_string_lossy().to_string())) + .collect(), + }, + ); + } else { + #[cfg(feature = "preview-builtin")] + preview::config_changed(_style, _include_paths, _library_paths); + } } fn highlight(&self, _path: Option, _offset: u32) -> Result<()> { - #[cfg(feature = "preview")] - preview::highlight(_path, _offset); - Ok(()) + { + if *self.use_external_previewer.borrow() { + #[cfg(feature = "preview-external")] + self.server_notifier.send_notification( + "slint/lsp_to_preview".to_string(), + crate::common::LspToPreviewMessage::HighlightFromEditor { + path: _path.as_ref().map(|p| p.to_string_lossy().to_string()), + offset: _offset, + }, + )?; + Ok(()) + } else { + #[cfg(feature = "preview-builtin")] + preview::highlight(&_path, _offset); + Ok(()) + } + } } } @@ -169,7 +278,7 @@ fn main() { std::env::set_var("SLINT_BACKEND", &args.backend); } - #[cfg(feature = "preview")] + #[cfg(feature = "preview-engine")] { let lsp_thread = std::thread::Builder::new() .name("LanguageServer".into()) @@ -199,7 +308,7 @@ fn main() { preview::start_ui_event_loop(); lsp_thread.join().unwrap(); } - #[cfg(not(feature = "preview"))] + #[cfg(not(feature = "preview-engine"))] match run_lsp_server() { Ok(threads) => threads.join().unwrap(), Err(error) => { @@ -240,7 +349,18 @@ fn main_loop(connection: Connection, init_param: InitializeParams) -> Result<()> document_cache: RefCell::new(DocumentCache::new(compiler_config)), server_notifier: server_notifier.clone(), init_param, - preview: Box::new(Previewer { server_notifier }), + preview: Box::new(Previewer { + server_notifier, + #[cfg(all(not(feature = "preview-builtin"), not(feature = "preview-external")))] + use_external_previewer: RefCell::new(false), // No preview, pick any. + #[cfg(all(not(feature = "preview-builtin"), feature = "preview-external"))] + use_external_previewer: RefCell::new(true), // external only + #[cfg(all(feature = "preview-builtin", not(feature = "preview-external")))] + use_external_previewer: RefCell::new(false), // internal only + #[cfg(all(feature = "preview-builtin", feature = "preview-external"))] + use_external_previewer: RefCell::new(false), // prefer internal + to_show: RefCell::new(None), + }), }); let mut futures = Vec::>>>>::new(); @@ -312,7 +432,7 @@ async fn handle_notification(req: lsp_server::Notification, ctx: &Rc) - DidOpenTextDocument::METHOD => { let params: DidOpenTextDocumentParams = serde_json::from_value(req.params)?; reload_document( - &ctx, + ctx, params.text_document.text, params.text_document.uri, params.text_document.version, @@ -323,7 +443,7 @@ async fn handle_notification(req: lsp_server::Notification, ctx: &Rc) - DidChangeTextDocument::METHOD => { let mut params: DidChangeTextDocumentParams = serde_json::from_value(req.params)?; reload_document( - &ctx, + ctx, params.content_changes.pop().unwrap().text, params.text_document.uri, params.text_document.version, @@ -335,9 +455,46 @@ async fn handle_notification(req: lsp_server::Notification, ctx: &Rc) - load_configuration(ctx).await?; } - #[cfg(feature = "preview")] + #[cfg(any(feature = "preview-builtin", feature = "preview-external"))] "slint/showPreview" => { - show_preview_command(req.params.as_array().map_or(&[], |x| x.as_slice()), ctx)?; + language::show_preview_command( + req.params.as_array().map_or(&[], |x| x.as_slice()), + ctx, + )?; + } + + #[cfg(all(feature = "preview-external", feature = "preview-engine"))] + "slint/preview_to_lsp" => { + use common::PreviewToLspMessage as M; + let params: M = serde_json::from_value(req.params)?; + match params { + M::Status { message, health } => { + crate::preview::send_status_notification( + &ctx.server_notifier, + &message, + health, + ); + } + M::Diagnostics { uri, diagnostics } => { + crate::preview::notify_lsp_diagnostics(&ctx.server_notifier, uri, diagnostics); + } + M::ShowDocument { file, start_line, start_column, end_line, end_column } => { + crate::preview::ask_editor_to_show_document( + &ctx.server_notifier, + &file, + start_line, + start_column, + end_line, + end_column, + ); + } + M::PreviewTypeChanged { is_external } => { + ctx.preview.set_use_external_previewer(is_external); + } + M::RequestState { .. } => { + ctx.preview.request_state(ctx); + } + } } _ => (), } diff --git a/tools/lsp/preview.rs b/tools/lsp/preview.rs index 83efa85c46..59aea67ba8 100644 --- a/tools/lsp/preview.rs +++ b/tools/lsp/preview.rs @@ -11,14 +11,19 @@ use crate::{common::PreviewComponent, lsp_ext::Health}; use i_slint_core::component_factory::FactoryContext; use slint_interpreter::{ComponentDefinition, ComponentHandle, ComponentInstance}; +use lsp_types::notification::Notification; + +#[cfg(target_arch = "wasm32")] +use crate::wasm_prelude::*; + mod ui; -#[cfg(target_arch = "wasm32")] +#[cfg(all(target_arch = "wasm32", feature = "preview-external"))] mod wasm; -#[cfg(target_arch = "wasm32")] +#[cfg(all(target_arch = "wasm32", feature = "preview-external"))] pub use wasm::*; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "preview-builtin"))] mod native; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "preview-builtin"))] pub use native::*; #[derive(Default)] @@ -90,7 +95,7 @@ pub fn config_changed( } /// If the file is in the cache, returns it. -/// In any was, register it as a dependency +/// In any way, register it as a dependency fn get_file_from_cache(path: PathBuf) -> Option { let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); let r = cache.source_code.get(&path).cloned(); @@ -113,6 +118,12 @@ async fn reload_preview(preview_component: PreviewComponent) { let mut builder = slint_interpreter::ComponentCompiler::default(); + #[cfg(target_arch = "wasm32")] + { + let cc = builder.compiler_configuration(i_slint_core::InternalToken); + cc.resource_url_mapper = resource_url_mapper(); + } + if !preview_component.style.is_empty() { builder.set_style(preview_component.style); } @@ -173,7 +184,7 @@ pub fn set_preview_factory( /// Highlight the element pointed at the offset in the path. /// When path is None, remove the highlight. -pub fn highlight(path: Option, offset: u32) { +pub fn highlight(path: &Option, offset: u32) { let highlight = path.clone().map(|x| (x, offset)); let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); @@ -183,7 +194,97 @@ pub fn highlight(path: Option, offset: u32) { cache.highlight = highlight; if cache.highlight.as_ref().map_or(true, |(path, _)| cache.dependency.contains(path)) { - let path = path.unwrap_or_default(); + let path = path.clone().unwrap_or_default(); update_highlight(path, offset); } } + +pub fn show_document_request_from_element_callback( + file: &str, + start_line: u32, + start_column: u32, + _end_line: u32, + end_column: u32, +) -> Option { + use lsp_types::{Position, Range, ShowDocumentParams, Url}; + + if file.is_empty() || start_column == 0 || end_column == 0 { + return None; + } + + let start_pos = Position::new(start_line.saturating_sub(1), start_column.saturating_sub(1)); + // let end_pos = Position::new(end_line.saturating_sub(1), end_column.saturating_sub(1)); + // 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, + }) +} + +pub fn convert_diagnostics( + diagnostics: &[slint_interpreter::Diagnostic], +) -> HashMap> { + let mut result: HashMap> = 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(); + result.entry(uri).or_default().push(crate::util::to_lsp_diag(d)); + } + result +} + +pub fn notify_lsp_diagnostics( + sender: &crate::ServerNotifier, + uri: lsp_types::Url, + diagnostics: Vec, +) -> Option<()> { + sender + .send_notification( + "textDocument/publishDiagnostics".into(), + lsp_types::PublishDiagnosticsParams { uri, diagnostics, version: None }, + ) + .ok() +} + +pub fn send_status_notification(sender: &crate::ServerNotifier, message: &str, health: Health) { + sender + .send_notification( + crate::lsp_ext::ServerStatusNotification::METHOD.into(), + crate::lsp_ext::ServerStatusParams { + health, + quiescent: false, + message: Some(message.into()), + }, + ) + .unwrap_or_else(|e| eprintln!("Error sending notification: {:?}", e)); +} + +#[cfg(feature = "preview-external")] +pub fn ask_editor_to_show_document( + sender: &crate::ServerNotifier, + file: &str, + start_line: u32, + start_column: u32, + end_line: u32, + end_column: u32, +) { + let Some(params) = crate::preview::show_document_request_from_element_callback( + file, + start_line, + start_column, + end_line, + end_column, + ) else { + return; + }; + let Ok(fut) = sender.send_request::(params) else { + return; + }; + i_slint_core::future::spawn_local(fut).unwrap(); +} diff --git a/tools/lsp/preview/native.rs b/tools/lsp/preview/native.rs index 78cf576a05..8526575f9f 100644 --- a/tools/lsp/preview/native.rs +++ b/tools/lsp/preview/native.rs @@ -4,14 +4,12 @@ // cSpell: ignore condvar use crate::common::PreviewComponent; -use crate::lsp_ext::{Health, ServerStatusNotification, ServerStatusParams}; +use crate::lsp_ext::Health; use crate::ServerNotifier; -use lsp_types::notification::Notification; use once_cell::sync::Lazy; use slint_interpreter::ComponentHandle; use std::cell::RefCell; -use std::collections::HashMap; use std::future::Future; use std::path::PathBuf; use std::rc::Rc; @@ -102,7 +100,7 @@ pub fn quit_ui_event_loop() { let _ = i_slint_core::api::quit_event_loop(); - // Make sure then sender channel gets dropped + // Make sure then sender channel gets dropped. if let Some(sender) = SERVER_NOTIFIER.get() { let mut sender = sender.lock().unwrap(); *sender = None; @@ -137,7 +135,7 @@ pub fn open_ui(sender: &ServerNotifier) { i_slint_core::api::invoke_from_event_loop(move || { PREVIEW_STATE.with(|preview_state| { let mut preview_state = preview_state.borrow_mut(); - open_ui_impl(&mut preview_state) + open_ui_impl(&mut preview_state); }); }) .unwrap(); @@ -162,12 +160,9 @@ pub fn close_ui() { { let mut cache = super::CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); if !cache.ui_is_visible { - return; // UI is already up! + return; // UI is already down! } cache.ui_is_visible = false; - - let mut sender = SERVER_NOTIFIER.get_or_init(Default::default).lock().unwrap(); - *sender = None; } i_slint_core::api::invoke_from_event_loop(move || { @@ -209,17 +204,25 @@ struct PreviewState { } thread_local! {static PREVIEW_STATE: std::cell::RefCell = Default::default();} +pub fn notify_diagnostics(diagnostics: &[slint_interpreter::Diagnostic]) -> Option<()> { + let Some(sender) = SERVER_NOTIFIER.get_or_init(Default::default).lock().unwrap().clone() else { + return Some(()); + }; + + let lsp_diags = crate::preview::convert_diagnostics(diagnostics); + + for (url, diagnostics) in lsp_diags { + crate::preview::notify_lsp_diagnostics(&sender, url, diagnostics)?; + } + Some(()) +} + pub fn send_status(message: &str, health: Health) { let Some(sender) = SERVER_NOTIFIER.get_or_init(Default::default).lock().unwrap().clone() else { return; }; - sender - .send_notification( - ServerStatusNotification::METHOD.into(), - ServerStatusParams { health, quiescent: false, message: Some(message.into()) }, - ) - .unwrap_or_else(|e| eprintln!("Error sending notification: {:?}", e)); + crate::preview::send_status_notification(&sender, message, health) } pub fn ask_editor_to_show_document( @@ -233,7 +236,7 @@ pub fn ask_editor_to_show_document( return; }; - let Some(params) = show_document_request_from_element_callback( + let Some(params) = super::show_document_request_from_element_callback( file, start_line, start_column, @@ -248,32 +251,6 @@ pub fn ask_editor_to_show_document( i_slint_core::future::spawn_local(fut).unwrap(); } -fn show_document_request_from_element_callback( - file: &str, - start_line: u32, - start_column: u32, - _end_line: u32, - end_column: u32, -) -> Option { - use lsp_types::{Position, Range, ShowDocumentParams, Url}; - - if file.is_empty() || start_column == 0 || end_column == 0 { - return None; - } - - let start_pos = Position::new(start_line.saturating_sub(1), start_column.saturating_sub(1)); - // let end_pos = Position::new(end_line.saturating_sub(1), end_column.saturating_sub(1)); - // 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, - }) -} - pub fn configure_design_mode(enabled: bool) { run_in_ui_thread(move || async move { PREVIEW_STATE.with(|preview_state| { @@ -321,35 +298,9 @@ pub fn update_preview_area(compiled: slint_interpreter::ComponentDefinition) { }); } -pub fn notify_diagnostics(diagnostics: &[slint_interpreter::Diagnostic]) -> Option<()> { - let Some(sender) = SERVER_NOTIFIER.get_or_init(Default::default).lock().unwrap().clone() else { - return Some(()); - }; - - let mut lsp_diags: HashMap> = 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(()) -} - /// Highlight the element pointed at the offset in the path. /// When path is None, remove the highlight. pub fn update_highlight(path: PathBuf, offset: u32) { - let path = path.to_path_buf(); run_in_ui_thread(move || async move { PREVIEW_STATE.with(|preview_state| { let preview_state = preview_state.borrow(); diff --git a/tools/lsp/preview/ui.rs b/tools/lsp/preview/ui.rs index ac21f835a7..c7ab867bbe 100644 --- a/tools/lsp/preview/ui.rs +++ b/tools/lsp/preview/ui.rs @@ -11,11 +11,11 @@ export component PreviewUi inherits Window { callback design_mode_changed(bool); VerticalBox { - design_mode_toggle := Button { - text: "Design Mode"; - checkable: true; - clicked => { root.design_mode_changed(self.checked); } - } + // Button { + // text: "Design Mode"; + // checkable: true; + // clicked => { root.design_mode_changed(self.checked); } + // } preview_area_container := ComponentContainer {} } } @@ -23,6 +23,6 @@ export component PreviewUi inherits Window { pub fn create_ui() -> Result { let ui = PreviewUi::new()?; - ui.on_design_mode_changed(|design_mode| super::set_design_mode(design_mode)); + ui.on_design_mode_changed(super::set_design_mode); Ok(ui) } diff --git a/tools/lsp/preview/wasm.rs b/tools/lsp/preview/wasm.rs index ccc5b7fe82..c3db74b04b 100644 --- a/tools/lsp/preview/wasm.rs +++ b/tools/lsp/preview/wasm.rs @@ -4,14 +4,7 @@ //! This wasm library can be loaded from JS to load and display the content of .slint files #![cfg(target_arch = "wasm32")] -use std::{ - cell::RefCell, - collections::HashMap, - future::Future, - path::{Path, PathBuf}, - pin::Pin, - rc::Rc, -}; +use std::{cell::RefCell, collections::HashMap, future::Future, path::PathBuf, pin::Pin, rc::Rc}; use wasm_bindgen::prelude::*; @@ -19,375 +12,21 @@ use slint_interpreter::ComponentHandle; use crate::{common::PreviewComponent, lsp_ext::Health}; -#[wasm_bindgen] -#[allow(dead_code)] -pub struct CompilationResult { - component: Option, - diagnostics: js_sys::Array, - error_string: String, -} - -#[wasm_bindgen] -impl CompilationResult { - #[wasm_bindgen(getter)] - pub fn component(&self) -> Option { - self.component.clone() - } - #[wasm_bindgen(getter)] - pub fn diagnostics(&self) -> js_sys::Array { - self.diagnostics.clone() - } - #[wasm_bindgen(getter)] - pub fn error_string(&self) -> String { - self.error_string.clone() - } -} - #[wasm_bindgen(typescript_custom_section)] const CALLBACK_FUNCTION_SECTION: &'static str = r#" export type ResourceUrlMapperFunction = (url: string) => Promise; -type ImportCallbackFunction = (url: string) => Promise; -type CurrentElementInformationCallbackFunction = (url: string, start_line: number, start_column: number, end_line: number, end_column: number) => void; +export type SignalLspFunction = (data: any) => void; "#; #[wasm_bindgen] extern "C" { #[wasm_bindgen(typescript_type = "ResourceUrlMapperFunction")] pub type ResourceUrlMapperFunction; + #[wasm_bindgen(typescript_type = "SignalLspFunction")] + pub type SignalLspFunction; - #[wasm_bindgen(typescript_type = "ImportCallbackFunction")] - pub type ImportCallbackFunction; - - #[wasm_bindgen(typescript_type = "CurrentElementInformationCallbackFunction")] - pub type CurrentElementInformationCallbackFunction; - #[wasm_bindgen(typescript_type = "Promise")] - pub type InstancePromise; #[wasm_bindgen(typescript_type = "Promise")] pub type PreviewConnectorPromise; - - // Make console.log available: - #[allow(unused)] - #[wasm_bindgen(js_namespace = console)] - fn log(s: &str); -} - -fn resource_url_mapper_from_js( - rum: ResourceUrlMapperFunction, -) -> Option Pin>>>>> { - let callback = js_sys::Function::from((*rum).clone()); - - Some(Rc::new(move |url: &str| { - let Some(promise) = callback.call1(&JsValue::UNDEFINED, &url.into()).ok() else { - return Box::pin(std::future::ready(None)); - }; - let future = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise)); - Box::pin(async move { future.await.ok().and_then(|v| v.as_string()) }) - })) -} - -/// Compile the content of a string. -/// -/// Returns a promise to a compiled component which can be run with ".run()" -#[wasm_bindgen] -pub async fn compile_from_string( - source: String, - base_url: String, - resource_url_mapper: Option, - optional_import_callback: Option, -) -> Result { - compile_from_string_with_style( - source, - base_url, - String::new(), - resource_url_mapper, - optional_import_callback, - ) - .await -} - -/// Same as [`compile_from_string`], but also takes a style parameter -#[wasm_bindgen] -pub async fn compile_from_string_with_style( - source: String, - base_url: String, - style: String, - resource_url_mapper: Option, - optional_import_callback: Option, -) -> Result { - console_error_panic_hook::set_once(); - - let mut compiler = slint_interpreter::ComponentCompiler::default(); - - #[cfg(target_arch = "wasm32")] - if let Some(rum) = resource_url_mapper { - let cc = compiler.compiler_configuration(i_slint_core::InternalToken); - cc.resource_url_mapper = resource_url_mapper_from_js(rum); - } - - if !style.is_empty() { - compiler.set_style(style) - } - - if let Some(load_callback) = optional_import_callback { - let open_import_fallback = move |file_name: &Path| -> core::pin::Pin< - Box>>>, - > { - Box::pin({ - let load_callback = js_sys::Function::from(load_callback.clone()); - let file_name: String = file_name.to_string_lossy().into(); - async move { - let result = load_callback.call1(&JsValue::UNDEFINED, &file_name.into()); - let promise: js_sys::Promise = result.unwrap().into(); - let future = wasm_bindgen_futures::JsFuture::from(promise); - match future.await { - Ok(js_ok) => Some(Ok(js_ok.as_string().unwrap_or_default())), - Err(js_err) => Some(Err(std::io::Error::new( - std::io::ErrorKind::Other, - js_err.as_string().unwrap_or_default(), - ))), - } - } - }) - }; - compiler.set_file_loader(open_import_fallback); - } - - let c = compiler.build_from_source(source, base_url.into()).await; - - let line_key = JsValue::from_str("lineNumber"); - let column_key = JsValue::from_str("columnNumber"); - let message_key = JsValue::from_str("message"); - let file_key = JsValue::from_str("fileName"); - let level_key = JsValue::from_str("level"); - let mut error_as_string = String::new(); - let array = js_sys::Array::new(); - for d in compiler.diagnostics().into_iter() { - let filename = - d.source_file().as_ref().map_or(String::new(), |sf| sf.to_string_lossy().into()); - - let filename_js = JsValue::from_str(&filename); - let (line, column) = d.line_column(); - - if d.level() == slint_interpreter::DiagnosticLevel::Error { - if !error_as_string.is_empty() { - error_as_string.push_str("\n"); - } - use std::fmt::Write; - - write!(&mut error_as_string, "{}:{}:{}", filename, line, d).unwrap(); - } - - let error_obj = js_sys::Object::new(); - js_sys::Reflect::set(&error_obj, &message_key, &JsValue::from_str(&d.message()))?; - js_sys::Reflect::set(&error_obj, &line_key, &JsValue::from_f64(line as f64))?; - js_sys::Reflect::set(&error_obj, &column_key, &JsValue::from_f64(column as f64))?; - js_sys::Reflect::set(&error_obj, &file_key, &filename_js)?; - js_sys::Reflect::set(&error_obj, &level_key, &JsValue::from_f64(d.level() as i8 as f64))?; - array.push(&error_obj); - } - - Ok(CompilationResult { - component: c.map(|c| WrappedCompiledComp(c)), - diagnostics: array, - error_string: error_as_string, - }) -} - -#[wasm_bindgen] -#[derive(Clone)] -pub struct WrappedCompiledComp(slint_interpreter::ComponentDefinition); - -#[wasm_bindgen] -impl WrappedCompiledComp { - /// Run this compiled component in a canvas. - /// The HTML must contains a element with the given `canvas_id` - /// where the result is gonna be rendered - #[wasm_bindgen] - pub fn run(&self, canvas_id: String) { - let component = self.0.create_with_canvas_id(&canvas_id).unwrap(); - component.show().unwrap(); - slint_interpreter::spawn_event_loop().unwrap(); - } - /// Creates this compiled component in a canvas, wrapped in a promise. - /// The HTML must contains a element with the given `canvas_id` - /// where the result is gonna be rendered. - /// You need to call `show()` on the returned instance for rendering. - /// - /// Note that the promise will only be resolved after calling `slint.run_event_loop()`. - #[wasm_bindgen] - pub fn create(&self, canvas_id: String) -> Result { - Ok(JsValue::from(js_sys::Promise::new(&mut |resolve, reject| { - let comp = send_wrapper::SendWrapper::new(self.0.clone()); - let canvas_id = canvas_id.clone(); - let resolve = send_wrapper::SendWrapper::new(resolve); - if let Err(e) = slint::invoke_from_event_loop(move || { - let instance = - WrappedInstance(comp.take().create_with_canvas_id(&canvas_id).unwrap()); - resolve.take().call1(&JsValue::UNDEFINED, &JsValue::from(instance)).unwrap_throw(); - }) { - reject - .call1( - &JsValue::UNDEFINED, - &JsValue::from( - format!("internal error: Failed to queue closure for event loop invocation: {e}"), - ), - ) - .unwrap_throw(); - } - })).unchecked_into::()) - } - /// Creates this compiled component in the canvas of the provided instance, wrapped in a promise. - /// For this to work, the provided instance needs to be visible (show() must've been - /// called) and the event loop must be running (`slint.run_event_loop()`). After this - /// call the provided instance is not rendered anymore and can be discarded. - /// - /// Note that the promise will only be resolved after calling `slint.run_event_loop()`. - #[wasm_bindgen] - pub fn create_with_existing_window( - &self, - instance: WrappedInstance, - ) -> Result { - Ok(JsValue::from(js_sys::Promise::new(&mut |resolve, reject| { - let params = send_wrapper::SendWrapper::new((self.0.clone(), instance.0.clone_strong(), resolve)); - if let Err(e) = slint_interpreter::invoke_from_event_loop(move || { - let (comp, instance, resolve) = params.take(); - let instance = - WrappedInstance(comp.create_with_existing_window(instance.window()).unwrap()); - resolve.call1(&JsValue::UNDEFINED, &JsValue::from(instance)).unwrap_throw(); - }) { - reject - .call1( - &JsValue::UNDEFINED, - &JsValue::from( - format!("internal error: Failed to queue closure for event loop invocation: {e}"), - ), - ) - .unwrap_throw(); - } - })).unchecked_into::()) - } -} - -#[wasm_bindgen] -pub struct WrappedInstance(slint_interpreter::ComponentInstance); - -impl Clone for WrappedInstance { - fn clone(&self) -> Self { - Self(self.0.clone_strong()) - } -} - -#[wasm_bindgen] -impl WrappedInstance { - /// Marks this instance for rendering and input handling. - /// - /// Note that the promise will only be resolved after calling `slint.run_event_loop()`. - #[wasm_bindgen] - pub fn show(&self) -> Result { - self.invoke_from_event_loop_wrapped_in_promise(|instance| instance.show()) - } - /// Hides this instance and prevents further updates of the canvas element. - /// - /// Note that the promise will only be resolved after calling `slint.run_event_loop()`. - #[wasm_bindgen] - pub fn hide(&self) -> Result { - self.invoke_from_event_loop_wrapped_in_promise(|instance| instance.hide()) - } - - fn invoke_from_event_loop_wrapped_in_promise( - &self, - callback: impl FnOnce( - &slint_interpreter::ComponentInstance, - ) -> Result<(), slint_interpreter::PlatformError> - + 'static, - ) -> Result { - let callback = std::cell::RefCell::new(Some(callback)); - Ok(js_sys::Promise::new(&mut |resolve, reject| { - let inst_weak = self.0.as_weak(); - - if let Err(e) = slint_interpreter::invoke_from_event_loop({ - let params = send_wrapper::SendWrapper::new(( - resolve, - reject.clone(), - callback.take().unwrap(), - )); - move || { - let (resolve, reject, callback) = params.take(); - match inst_weak.upgrade() { - Some(instance) => match callback(&instance) { - Ok(()) => { - resolve.call0(&JsValue::UNDEFINED).unwrap_throw(); - } - Err(e) => { - reject - .call1( - &JsValue::UNDEFINED, - &JsValue::from(format!( - "Invocation on ComponentInstance from within event loop failed: {e}" - )), - ) - .unwrap_throw(); - } - }, - None => { - reject - .call1( - &JsValue::UNDEFINED, - &JsValue::from(format!( - "Invocation on ComponentInstance failed because instance was deleted too soon" - )), - ) - .unwrap_throw(); - } - } - } - }) { - reject - .call1( - &JsValue::UNDEFINED, - &JsValue::from( - format!("internal error: Failed to queue closure for event loop invocation: {e}"), - ), - ) - .unwrap_throw(); - } - })) - } - - /// THIS FUNCTION IS NOT PART THE PUBLIC API! - /// Highlights instances of the requested component - #[wasm_bindgen] - pub fn highlight(&self, _path: &str, _offset: u32) { - self.0.highlight(_path.into(), _offset); - let _ = slint_interpreter::invoke_from_event_loop(|| {}); // wake event loop - } - - /// THIS FUNCTION IS NOT PART THE PUBLIC API! - /// Request information on what to highlight in the editor based on clicks in the UI - #[wasm_bindgen] - pub fn set_design_mode(&self, active: bool) { - self.0.set_design_mode(active); - let _ = slint_interpreter::invoke_from_event_loop(|| {}); // wake event loop - } - - /// THIS FUNCTION IS NOT PART THE PUBLIC API! - /// Request information on what to highlight in the editor based on clicks in the UI - #[wasm_bindgen] - pub fn on_element_selected(&self, callback: CurrentElementInformationCallbackFunction) { - self.0.on_element_selected(Box::new( - move |url: &str, start_line: u32, start_column: u32, end_line: u32, end_column: u32| { - let args = js_sys::Array::of5( - &url.into(), - &start_line.into(), - &start_column.into(), - &end_line.into(), - &end_column.into(), - ); - let callback = js_sys::Function::from(callback.clone()); - let _ = callback.apply(&JsValue::UNDEFINED, &args); - }, - )); - } } /// Register DOM event handlers on all instance and set up the event loop for that. @@ -402,21 +41,29 @@ pub fn run_event_loop() -> Result<(), JsValue> { struct PreviewState { ui: Option, handle: Rc>>, + lsp_notifier: Option, + resource_url_mapper: Option, } thread_local! {static PREVIEW_STATE: std::cell::RefCell = Default::default();} #[wasm_bindgen] -pub struct PreviewConnector { - current_previewed_component: RefCell>, -} +pub struct PreviewConnector {} #[wasm_bindgen] impl PreviewConnector { #[wasm_bindgen] - pub fn create() -> Result { + pub fn create( + lsp_notifier: SignalLspFunction, + resource_url_mapper: ResourceUrlMapperFunction, + ) -> Result { console_error_panic_hook::set_once(); - Ok(JsValue::from(js_sys::Promise::new(&mut |resolve, reject| { + PREVIEW_STATE.with(|preview_state| { + preview_state.borrow_mut().lsp_notifier = Some(lsp_notifier); + preview_state.borrow_mut().resource_url_mapper = Some(resource_url_mapper); + }); + + Ok(JsValue::from(js_sys::Promise::new(&mut move |resolve, reject| { let resolve = send_wrapper::SendWrapper::new(resolve); let reject_c = send_wrapper::SendWrapper::new(reject.clone()); if let Err(e) = slint_interpreter::invoke_from_event_loop(move || { @@ -429,7 +76,7 @@ impl PreviewConnector { Ok(ui) => { preview_state.borrow_mut().ui = Some(ui); resolve.take().call1(&JsValue::UNDEFINED, - &JsValue::from(Self { current_previewed_component: RefCell::new(None) })).unwrap_throw() + &JsValue::from(Self { })).unwrap_throw() } Err(e) => reject_c.take().call1(&JsValue::UNDEFINED, &JsValue::from(format!("Failed to construct Preview UI: {e}"))).unwrap_throw(), @@ -459,7 +106,7 @@ impl PreviewConnector { } #[wasm_bindgen] - pub async fn process_lsp_to_preview_message(&self, value: JsValue) -> Result<(), JsValue> { + pub fn process_lsp_to_preview_message(&self, value: JsValue) -> Result<(), JsValue> { use crate::common::LspToPreviewMessage as M; let message: M = serde_wasm_bindgen::from_value(value) @@ -467,21 +114,10 @@ impl PreviewConnector { match message { M::SetContents { path, contents } => { super::set_contents(&PathBuf::from(&path), contents); - if self.current_previewed_component.borrow().is_none() { - let pc = PreviewComponent { - path: PathBuf::from(path), - component: None, - style: Default::default(), - include_paths: Default::default(), - library_paths: Default::default(), - }; - *self.current_previewed_component.borrow_mut() = Some(pc.clone()); - load_preview(pc); - } Ok(()) } M::SetConfiguration { style, include_paths, library_paths } => { - let ip: Vec = include_paths.iter().map(|p| PathBuf::from(p)).collect(); + let ip: Vec = include_paths.iter().map(PathBuf::from).collect(); let lp: HashMap = library_paths.iter().map(|(n, p)| (n.clone(), PathBuf::from(p))).collect(); super::config_changed(&style, &ip, &lp); @@ -492,18 +128,17 @@ impl PreviewConnector { path: PathBuf::from(path), component, style, - include_paths: include_paths.iter().map(|p| PathBuf::from(p)).collect(), + include_paths: include_paths.iter().map(PathBuf::from).collect(), library_paths: library_paths .iter() .map(|(n, p)| (n.clone(), PathBuf::from(p))) .collect(), }; - *self.current_previewed_component.borrow_mut() = Some(pc.clone()); load_preview(pc); Ok(()) } M::HighlightFromEditor { path, offset } => { - super::highlight(path.map(|s| PathBuf::from(s)), offset); + super::highlight(&path.map(PathBuf::from), offset); Ok(()) } } @@ -547,9 +182,7 @@ fn invoke_from_event_loop_wrapped_in_promise( reject .call1( &JsValue::UNDEFINED, - &JsValue::from(format!( - "Invocation on PreviewUi failed because instance was deleted too soon" - )), + &JsValue::from("Invocation on PreviewUi failed because instance was deleted too soon"), ) .unwrap_throw(); } @@ -612,23 +245,65 @@ pub fn load_preview(component: PreviewComponent) { .unwrap(); } -pub fn send_status(_message: &str, _health: Health) { - // Do nothing for now... +pub fn resource_url_mapper( +) -> Option Pin>>>>> { + let callback = PREVIEW_STATE.with(|preview_state| { + preview_state + .borrow() + .resource_url_mapper + .as_ref() + .map(|rum| js_sys::Function::from((*rum).clone())) + })?; + + Some(Rc::new(move |url: &str| { + let Some(promise) = callback.call1(&JsValue::UNDEFINED, &url.into()).ok() else { + return Box::pin(std::future::ready(None)); + }; + let future = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise)); + Box::pin(async move { future.await.ok().and_then(|v| v.as_string()) }) + })) } -pub fn notify_diagnostics(_diagnostics: &[slint_interpreter::Diagnostic]) -> Option<()> { - // Do nothing for now... +pub fn send_message_to_lsp(message: crate::common::PreviewToLspMessage) { + PREVIEW_STATE.with(|preview_state| { + if let Some(callback) = &preview_state.borrow().lsp_notifier { + let callback = js_sys::Function::from((*callback).clone()); + let value = serde_wasm_bindgen::to_value(&message).unwrap(); + let _ = callback.call1(&JsValue::UNDEFINED, &value); + } + }) +} + +pub fn send_status(message: &str, health: Health) { + send_message_to_lsp(crate::common::PreviewToLspMessage::Status { + message: message.to_string(), + health, + }); +} + +pub fn notify_diagnostics(diagnostics: &[slint_interpreter::Diagnostic]) -> Option<()> { + let diags = crate::preview::convert_diagnostics(diagnostics); + + for (uri, diagnostics) in diags { + send_message_to_lsp(crate::common::PreviewToLspMessage::Diagnostics { uri, diagnostics }); + } Some(()) } pub fn ask_editor_to_show_document( - _file: &str, - _start_line: u32, - _start_column: u32, - _end_line: u32, - _end_column: u32, + file: &str, + start_line: u32, + start_column: u32, + end_line: u32, + end_column: u32, ) { - // Do nothing for now... + send_message_to_lsp(crate::common::PreviewToLspMessage::ShowDocument { + file: file.to_string(), + start_line, + start_column, + end_line, + end_column, + }) } pub fn update_preview_area(compiled: slint_interpreter::ComponentDefinition) { @@ -653,7 +328,7 @@ pub fn update_highlight(path: PathBuf, offset: u32) { let preview_state = preview_state.borrow(); let handle = preview_state.handle.borrow(); if let Some(handle) = &*handle { - handle.highlight(path, offset); + handle.highlight(path.to_path_buf(), offset); } }) }) diff --git a/tools/lsp/wasm_main.rs b/tools/lsp/wasm_main.rs index 8c83d9e840..a03158c431 100644 --- a/tools/lsp/wasm_main.rs +++ b/tools/lsp/wasm_main.rs @@ -6,7 +6,7 @@ mod common; mod language; pub mod lsp_ext; -#[cfg(feature = "preview")] +#[cfg(feature = "preview-engine")] mod preview; pub mod util; @@ -48,7 +48,32 @@ struct Previewer { } impl PreviewApi for Previewer { + fn set_use_external_previewer(&self, _use_external: bool) { + // The WASM LSP always needs to use the WASM preview! + } + + fn request_state(&self, ctx: &std::rc::Rc) { + #[cfg(feature = "preview-external")] + { + let documents = &ctx.document_cache.borrow().documents; + + for (p, d) in documents.all_file_documents() { + let Some(node) = &d.node else { + continue; + }; + self.set_contents(p, &node.text().to_string()); + } + let style = documents.compiler_config.style.clone().unwrap_or_default(); + self.config_changed( + &style, + &documents.compiler_config.include_paths, + &documents.compiler_config.library_paths, + ); + } + } + fn set_contents(&self, path: &std::path::Path, contents: &str) { + #[cfg(feature = "preview-external")] let _ = self.server_notifier.send_notification( "slint/lsp_to_preview".to_string(), crate::common::LspToPreviewMessage::SetContents { @@ -59,6 +84,7 @@ impl PreviewApi for Previewer { } fn load_preview(&self, component: common::PreviewComponent) { + #[cfg(feature = "preview-external")] let _ = self.server_notifier.send_notification( "slint/lsp_to_preview".to_string(), crate::common::LspToPreviewMessage::ShowPreview { @@ -85,6 +111,7 @@ impl PreviewApi for Previewer { include_paths: &[PathBuf], library_paths: &HashMap, ) { + #[cfg(feature = "preview-external")] let _ = self.server_notifier.send_notification( "slint/lsp_to_preview".to_string(), crate::common::LspToPreviewMessage::SetConfiguration { @@ -102,6 +129,7 @@ impl PreviewApi for Previewer { } fn highlight(&self, path: Option, offset: u32) -> Result<()> { + #[cfg(feature = "preview-external")] self.server_notifier.send_notification( "slint/lsp_to_preview".to_string(), crate::common::LspToPreviewMessage::HighlightFromEditor { @@ -221,6 +249,11 @@ extern "C" { #[wasm_bindgen(typescript_type = "HighlightInPreviewFunction")] pub type HighlightInPreviewFunction; + + // Make console.log available: + #[allow(unused)] + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); } #[wasm_bindgen] @@ -269,6 +302,49 @@ pub fn create( #[wasm_bindgen] impl SlintServer { + #[cfg(all(feature = "preview-engine", feature = "preview-external"))] + #[wasm_bindgen] + pub async fn process_preview_to_lsp_message( + &self, + value: JsValue, + ) -> std::result::Result<(), JsValue> { + use crate::common::PreviewToLspMessage as M; + + let Ok(message) = serde_wasm_bindgen::from_value::(value) else { + return Err(JsValue::from("Failed to convert value to PreviewToLspMessage")); + }; + + match message { + M::Status { message, health } => { + crate::preview::send_status_notification( + &self.ctx.server_notifier, + &message, + health, + ); + } + M::Diagnostics { diagnostics, uri } => { + crate::preview::notify_lsp_diagnostics(&self.ctx.server_notifier, uri, diagnostics); + } + M::ShowDocument { file, start_line, start_column, end_line, end_column } => { + crate::preview::ask_editor_to_show_document( + &self.ctx.server_notifier, + &file, + start_line, + start_column, + end_line, + end_column, + ) + } + M::PreviewTypeChanged { is_external: _ } => { + // Nothing to do! + } + M::RequestState { .. } => { + // Nothing to do! + } + } + Ok(()) + } + #[wasm_bindgen] pub fn server_initialize_result(&self, cap: JsValue) -> JsResult { Ok(to_value(&language::server_initialize_result(&serde_wasm_bindgen::from_value(cap)?))?) @@ -294,16 +370,6 @@ impl SlintServer { }) } - /* #[wasm_bindgen] - pub fn show_preview(&self, params: JsValue) -> JsResult<()> { - language::show_preview_command( - &serde_wasm_bindgen::from_value(params)?, - &ServerNotifier, - &mut self.0.borrow_mut(), - ) - .map_err(|e| JsError::new(&e.to_string())); - }*/ - #[wasm_bindgen] pub fn handle_request(&self, _id: JsValue, method: String, params: JsValue) -> js_sys::Promise { let guard = self.reentry_guard.clone(); diff --git a/tools/slintpad/package.json b/tools/slintpad/package.json index 93d167322d..00a0cbfc6a 100644 --- a/tools/slintpad/package.json +++ b/tools/slintpad/package.json @@ -8,6 +8,8 @@ "build": "npm run clean && npx vite build", "build:wasm_lsp": "wasm-pack build --dev --target web ../lsp -- --no-default-features --features backend-winit,renderer-femtovg,preview", "build:wasm_lsp-release": "wasm-pack build --release --target web ../lsp -- --no-default-features --features backend-winit,renderer-femtovg,preview", + "build:wasm_interpreter": "wasm-pack build --dev --target web ../../api/wasm-interpreter -- --features console_error_panic_hook", + "build:wasm_interpreter-release": "wasm-pack build --release --target web ../../api/wasm-interpreter -- --features console_error_panic_hook", "lint": "eslint src", "clean": "rimraf dist dev-dist pkg", "start": "npm run clean && npm run build:wasm_lsp && npm run start:vite", @@ -19,7 +21,7 @@ "test:cypress_open-chromium": "cypress open --browser=chromium --e2e", "test:cypress_run-ff": "cypress run --browser=firefox --e2e", "test:cypress_open-ff": "cypress open --browser=firefox --e2e", - "slintpad:prepublish": "npm run clean && npm run build:wasm_lsp-release", + "slintpad:prepublish": "npm run clean && npm run build:wasm_lsp-release && npm run build:wasm_interpreter-release", "postinstall": "monaco-treemending" }, "keywords": [], @@ -31,6 +33,7 @@ "@types/vscode": "~1.82.0", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", + "cross-env": "^7.0.3", "eslint": "^8.32.0", "monaco-editor": "0.41.0", "monaco-languageclient": "6.4.6", diff --git a/tools/slintpad/src/editor_widget.ts b/tools/slintpad/src/editor_widget.ts index 28d1ca76eb..38db41d5c3 100644 --- a/tools/slintpad/src/editor_widget.ts +++ b/tools/slintpad/src/editor_widget.ts @@ -129,10 +129,14 @@ export class KnownUrlMapper implements UrlMapper { const file_path = file_from_internal_uri(this.#uuid, uri); const mapped_url = this.#map[file_path] || null; - return ( - monaco.Uri.parse(mapped_url ?? "file:///missing_url") ?? - monaco.Uri.parse("file:///broken_url") - ); + if (mapped_url) { + return ( + monaco.Uri.parse(mapped_url) ?? + monaco.Uri.parse("file:///broken_url") + ); + } else { + return uri; + } } } @@ -200,7 +204,7 @@ function tabTitleFromURL(url: monaco.Uri): string { class EditorPaneWidget extends Widget { auto_compile = true; - #current_style = "fluent-light"; + #current_style = ""; #main_uri: monaco.Uri | null = null; #editor_view_states: Map< monaco.Uri, @@ -208,7 +212,6 @@ class EditorPaneWidget extends Widget { >; #editor: monaco.editor.IStandaloneCodeEditor | null = null; #client: MonacoLanguageClient | null = null; - #keystroke_timeout_handle?: number; #url_mapper: UrlMapper | null = null; #edit_era: number; #disposables: monaco.IDisposable[] = []; @@ -223,13 +226,6 @@ class EditorPaneWidget extends Widget { return; }; - #onRenderRequest?: ( - _style: string, - _source: string, - _url: string, - _fetch: (_url: string) => Promise, - ) => Promise; - #onModelRemoved?: (_url: monaco.Uri) => void; #onModelAdded?: (_url: monaco.Uri) => void; #onModelSelected?: (_url: monaco.Uri | null) => void; @@ -279,17 +275,7 @@ class EditorPaneWidget extends Widget { const sw_channel = new MessageChannel(); sw_channel.port1.onmessage = (m) => { if (m.data.type === "MapUrl") { - const reply_port = m.ports[0]; - const internal_uri = monaco.Uri.parse(m.data.url); - const mapped_url = - this.#url_mapper?.from_internal(internal_uri)?.toString() ?? - ""; - const file = file_from_internal_uri( - this.#internal_uuid, - internal_uri, - ); - this.#extra_file_urls[file] = mapped_url; - reply_port.postMessage(mapped_url); + console.log("REMOVE THE SERVICE WORKER AGAIN"); } else { console.error( "Unknown message received from service worker:", @@ -430,16 +416,10 @@ class EditorPaneWidget extends Widget { return this.#extra_file_urls; } - compile() { - this.update_preview(); - } - async set_style(value: string) { this.#current_style = value; const config = '{ "slint.preview.style": "' + value + '" }'; await updateUserConfiguration(config); - - this.update_preview(); } style() { @@ -482,15 +462,11 @@ class EditorPaneWidget extends Widget { private add_model_listener(model: monaco.editor.ITextModel) { const uri = model.uri; - model.onDidChangeContent(() => { - this.maybe_update_preview_automatically(); - }); this.#editor_view_states.set(uri, null); this.#onModelAdded?.(uri); if (monaco.editor.getModels().length === 1) { this.#main_uri = uri; this.set_model(uri); - this.update_preview(); } } @@ -515,51 +491,6 @@ class EditorPaneWidget extends Widget { } } - protected update_preview() { - const model = monaco.editor.getModel( - this.#main_uri ?? new monaco.Uri(), - ); - if (model != null) { - const source = model.getValue(); - const era = this.#edit_era; - - setTimeout(() => { - if (this.#onRenderRequest != null) { - this.#onRenderRequest( - this.#current_style, - source, - this.#main_uri?.toString() ?? "", - (url: string) => { - return this.handle_lsp_url_request(era, url); - }, - ).then((markers: monaco.editor.IMarkerData[]) => { - if (this.#editor != null) { - const model = this.#editor.getModel(); - if (model != null) { - monaco.editor.setModelMarkers( - model, - "slint", - markers, - ); - } - } - }); - } - }, 1); - } - } - - protected maybe_update_preview_automatically() { - if (this.auto_compile) { - if (this.#keystroke_timeout_handle != null) { - clearTimeout(this.#keystroke_timeout_handle); - } - this.#keystroke_timeout_handle = setTimeout(() => { - this.update_preview(); - }, 500); - } - } - private setup_editor( container: HTMLDivElement, lsp: Lsp, @@ -649,17 +580,6 @@ class EditorPaneWidget extends Widget { return lsp.language_client; } - set onRenderRequest( - request: ( - _style: string, - _source: string, - _url: string, - _fetch: (_url: string) => Promise, - ) => Promise, - ) { - this.#onRenderRequest = request; - } - set onModelsCleared(f: () => void) { this.#onModelsCleared = f; } @@ -809,6 +729,7 @@ export class EditorWidget extends Widget { layout.addWidget(this.#tab_bar); this.#editor = new EditorPaneWidget(lsp); + this.set_style("fluent"); layout.addWidget(this.#editor); super.layout = layout; @@ -891,18 +812,6 @@ export class EditorWidget extends Widget { return this.#editor.current_text_document_version; } - compile() { - this.#editor.compile(); - } - - set auto_compile(value: boolean) { - this.#editor.auto_compile = value; - } - - get auto_compile() { - return this.#editor.auto_compile; - } - async set_style(value: string) { await this.#editor.set_style(value); } @@ -968,17 +877,6 @@ export class EditorWidget extends Widget { } } - set onRenderRequest( - request: ( - _style: string, - _source: string, - _url: string, - _fetch: (_url: string) => Promise, - ) => Promise, - ) { - this.#editor.onRenderRequest = request; - } - set onPositionChange(cb: PositionChangeCallback) { this.#editor.onPositionChangeCallback = cb; } diff --git a/tools/slintpad/src/index.ts b/tools/slintpad/src/index.ts index ebaa969852..4ff53f675f 100644 --- a/tools/slintpad/src/index.ts +++ b/tools/slintpad/src/index.ts @@ -101,7 +101,6 @@ function create_settings_menu(): Menu { }); menu.addItem({ command: "slint:store_github_token" }); - menu.addItem({ command: "slint:auto_compile" }); return menu; } @@ -151,8 +150,6 @@ function create_project_menu(editor: EditorWidget): Menu { menu.addItem({ command: "slint:open_url" }); menu.addItem({ type: "submenu", submenu: create_demo_menu(editor) }); menu.addItem({ type: "separator" }); - menu.addItem({ command: "slint:compile" }); - menu.addItem({ type: "separator" }); menu.addItem({ command: "slint:add_file" }); menu.addItem({ type: "submenu", submenu: create_share_menu(editor) }); menu.addItem({ type: "separator" }); @@ -412,65 +409,17 @@ class DockWidgets { } function setup(lsp: Lsp) { - commands.addCommand("slint:compile", { - label: "Compile", - iconClass: "fa fa-magic", - mnemonic: 1, - execute: () => { - editor.compile(); - }, - }); - - commands.addCommand("slint:auto_compile", { - label: "Automatically Compile on Change", - mnemonic: 1, - isToggled: () => { - return editor.auto_compile; - }, - execute: () => { - editor.auto_compile = !editor.auto_compile; - }, - }); - - commands.addKeyBinding({ - keys: ["Accel B"], - selector: "body", - command: "slint:compile", - }); - const editor = new EditorWidget(lsp); const dock = new DockPanel(); - // lsp.previewer.on_highlight_request = ( - // url: string, - // start: { line: number; column: number }, - // _end: { line: number; column: number }, - // ) => { - // if (url === "") { - // return; - // } - // - // editor.goto_position( - // url, - // LspRange.create( - // start.line - 1, - // start.column - 1, - // start.line - 1, // Highlight a position, not the entire range - // start.column - 1, - // ), - // ); - // }; - const dock_widgets = new DockWidgets( dock, [ () => { - const preview = new PreviewWidget( - lsp, - editor.internal_url_prefix, + const preview = new PreviewWidget(lsp, (url: string) => + editor.map_url(url), ); - commands.execute("slint:compile"); return preview; }, {}, diff --git a/tools/slintpad/src/lsp.ts b/tools/slintpad/src/lsp.ts index 3610d2b354..f06f0e9fa0 100644 --- a/tools/slintpad/src/lsp.ts +++ b/tools/slintpad/src/lsp.ts @@ -52,15 +52,10 @@ function createLanguageClient( export type FileReader = (_url: string) => Promise; export class LspWaiter { - #previewer_port: MessagePort; #previewer_promise: Promise | null; #lsp_promise: Promise | null; constructor() { - const lsp_previewer_channel = new MessageChannel(); - const lsp_side = lsp_previewer_channel.port1; - this.#previewer_port = lsp_previewer_channel.port2; - const worker = new Worker( new URL("worker/lsp_worker.ts", import.meta.url), { type: "module" }, @@ -74,7 +69,6 @@ export class LspWaiter { } }; }); - worker.postMessage(lsp_side, [lsp_side]); this.#previewer_promise = slint_init(); } @@ -97,7 +91,6 @@ export class Previewer { #preview_connector: slint_preview.PreviewConnector; constructor(connector: slint_preview.PreviewConnector) { - console.log("LSP/Previewer: Constructor"); this.#preview_connector = connector; } @@ -128,7 +121,6 @@ export class Lsp { const notification = data as NotificationMessage; const params = notification.params; - console.log("Got lsp_to_preview communication:", params); this.#preview_connector?.process_lsp_to_preview_message( params, ); @@ -213,20 +205,25 @@ export class Lsp { return lsp_client; } - async previewer(): Promise { - console.log("LSP: Grabbing Previewer!"); + async previewer( + resource_url_mapper: ResourceUrlMapperFunction, + ): Promise { if (this.#preview_connector === null) { - console.log("LSP: Running event loop!"); try { slint_preview.run_event_loop(); } catch (e) { // this is not an error! } - console.log("LSP: Creating Preview connector"); + this.#preview_connector = - await slint_preview.PreviewConnector.create(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await slint_preview.PreviewConnector.create((data: any) => { + this.language_client.sendNotification( + "slint/preview_to_lsp", + data, + ); + }, resource_url_mapper); } - console.log("LSP: Got preview connector...", this.#preview_connector); return new Previewer(this.#preview_connector); } } diff --git a/tools/slintpad/src/preview.ts b/tools/slintpad/src/preview.ts index 0e6d593a4d..303dd65e6b 100644 --- a/tools/slintpad/src/preview.ts +++ b/tools/slintpad/src/preview.ts @@ -1,7 +1,7 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial -import slint_init, * as slint from "@lsp/slint_lsp_wasm.js"; +import slint_init, * as slint from "@interpreter/slint_wasm_interpreter.js"; (async function () { await slint_init(); @@ -52,7 +52,6 @@ export Demo := Window { source, base_url, style, - async (_url: string) => Promise.resolve(undefined), async (url: string): Promise => { const file_source = loaded_documents.get(url); if (file_source === undefined) { diff --git a/tools/slintpad/src/preview_widget.ts b/tools/slintpad/src/preview_widget.ts index 39c8c76349..7dbbf55f2b 100644 --- a/tools/slintpad/src/preview_widget.ts +++ b/tools/slintpad/src/preview_widget.ts @@ -6,15 +6,10 @@ import { Message } from "@lumino/messaging"; import { Widget } from "@lumino/widgets"; -import { Previewer, Lsp } from "./lsp"; +import { Previewer, Lsp, ResourceUrlMapperFunction } from "./lsp"; export class PreviewWidget extends Widget { - // #canvas: HTMLCanvasElement | null = null; - // #canvas_observer: MutationObserver | null = null; - // #zoom_level = 100; #previewer: Previewer | null = null; - // #picker_mode = false; - // #preview_connector: slint_preview.PreviewConnector; static createNode(): HTMLElement { const node = document.createElement("div"); @@ -33,7 +28,7 @@ export class PreviewWidget extends Widget { return node; } - constructor(lsp: Lsp, _internal_url_prefix: string) { + constructor(lsp: Lsp, resource_url_mapper: ResourceUrlMapperFunction) { super({ node: PreviewWidget.createNode() }); this.setFlag(Widget.Flag.DisallowLayout); @@ -43,251 +38,25 @@ export class PreviewWidget extends Widget { this.title.caption = `Slint Viewer`; this.title.closable = true; - // console.assert(previewer.canvas_id === null); - - console.log("PW: Constructor: Requesting Previewer..."); - lsp.previewer().then((p) => { - console.log("PW: Got my previewer!"); + lsp.previewer(resource_url_mapper).then((p) => { this.#previewer = p; - console.log("CREATING UI"); // Give the UI some time to wire up the canvas so it can be found // when searching the document. this.#previewer.show_ui().then(() => { - console.log("UI should be up!"); + console.info("UI should be up!"); }); }); - - this.setup_canvas(); - - this.populate_menu(); - - // this.#previewer.on_error = (_error_string: string) => { - // const error_area = this.errorNode; - // - // error_area.innerHTML = ""; - // - // if (error_string != "") { - // for (const line of error_string.split("\n")) { - // const text = document.createTextNode( - // line.replaceAll(internal_url_prefix, ""), - // ); - // const p = document.createElement("p"); - // p.className = "error-message"; - // p.appendChild(text); - // error_area.appendChild(p); - // } - // - // error_area.style.display = "block"; - // } else { - // error_area.style.display = "none"; - // } - // }; - } - - private populate_menu() { - // const menu = this.menuNode; - // - // const zoom_in = document.createElement("button"); - // zoom_in.innerHTML = ''; - // - // const zoom_level = document.createElement("input"); - // zoom_level.type = "number"; - // zoom_level.max = "1600"; - // zoom_level.min = "25"; - // zoom_level.value = this.#zoom_level.toString(); - // - // const zoom_out = document.createElement("button"); - // zoom_out.innerHTML = ''; - // - // const set_zoom_level = (level: number) => { - // this.#zoom_level = level; - // const canvas = this.canvasNode; - // if (canvas != null) { - // canvas.style.scale = (level / 100).toString(); - // } - // if (+zoom_level.value != level) { - // zoom_level.value = level.toString(); - // } - // }; - // - // zoom_in.addEventListener("click", () => { - // let next_level = +zoom_level.max; - // const current_level = +zoom_level.value; - // const smallest_level = +zoom_level.min; - // - // while (next_level > smallest_level && next_level >= current_level) { - // next_level = Math.ceil(next_level / 2); - // } - // set_zoom_level(next_level); - // }); - // - // zoom_out.addEventListener("click", () => { - // let next_level = +zoom_level.min; - // const current_level = +zoom_level.value; - // const biggest_level = +zoom_level.max; - // - // while (next_level < biggest_level && next_level <= current_level) { - // next_level = Math.ceil(next_level * 2); - // } - // set_zoom_level(next_level); - // }); - // - // zoom_level.addEventListener("change", () => { - // set_zoom_level(+zoom_level.value); - // }); - // - // const item_picker = document.createElement("button"); - // item_picker.innerHTML = ''; - // - // const toggle_button_state = (state: boolean): boolean => { - // this.setPickerMode(state); - // return state; - // }; - // - // item_picker.addEventListener("click", () => { - // this.#picker_mode = toggle_button_state(!this.#picker_mode); - // }); - // item_picker.style.marginLeft = "20px"; - // - // toggle_button_state(this.#picker_mode); - // - // menu.appendChild(zoom_in); - // menu.appendChild(zoom_level); - // menu.appendChild(zoom_out); - // menu.appendChild(item_picker); - } - - protected setPickerMode(_mode: boolean) { - // this.canvasNode.classList.remove("picker-mode"); - // if (mode) { - // this.canvasNode.classList.add("picker-mode"); - // } - // this.#previewer.picker_mode = mode; } protected onCloseRequest(msg: Message): void { - // this.#previewer.canvas_id = null; super.onCloseRequest(msg); this.dispose(); } - protected update_scroll_size() { - // // I use style.scale to zoom the canvas, which can be GPU accelerated - // // and should be fast. Unfortunately that only scales at render-time, - // // _not_ at layout time. So scrolling breaks as it calculates the scroll - // // area based on the canvas size without scaling applied! - // // - // // So we have a scrollNode as the actual scroll area and watch the canvas - // // for style changes, triggering this function. - // // - // // This resizes the scrollNode to be scale_factor * canvas size + padding - // // and places the canvas into the middle- This makes scrolling work - // // properly: The scroll area size is calculated based on the scrollNode, - // // which has enough room around the canvas for it to be rendered in - // // zoomed state. - // if (this.#canvas == null || this.#zoom_level < 0) { - // return; - // } - // - // const padding = 25; - // const canvas_style = document.defaultView?.getComputedStyle( - // this.#canvas, - // ); - // const parent_style = document.defaultView?.getComputedStyle( - // this.contentNode, - // ); - // - // if (canvas_style == null || parent_style == null) { - // return; - // } - // - // const raw_canvas_scale = - // canvas_style.scale === "none" ? 1 : parseFloat(canvas_style.scale); - // const raw_canvas_width = parseInt(canvas_style.width, 10); - // const raw_canvas_height = parseInt(canvas_style.height, 10); - // const canvas_width = Math.ceil(raw_canvas_width * raw_canvas_scale); - // const canvas_height = Math.ceil(raw_canvas_height * raw_canvas_scale); - // const width = Math.max( - // parseInt(parent_style.width, 10), - // canvas_width + 2 * padding, - // ); - // const height = Math.max( - // parseInt(parent_style.height, 10), - // canvas_height + 3 * padding, - // ); - // const left = Math.ceil((width - raw_canvas_width) / 2) + "px"; - // const top = Math.ceil((height - raw_canvas_height) / 2) + "px"; // have twice the padding on top - // - // const zl = this.#zoom_level; - // this.#zoom_level = -1; - // this.#canvas.style.left = left; - // this.#canvas.style.top = top; - // this.scrollNode.style.width = width + "px"; - // this.scrollNode.style.height = height + "px"; - // this.#zoom_level = zl; - } - - protected setup_canvas() { - // const canvas_id = "canvas"; - // - // this.#canvas = this.#preview_connector.canvas(); - // - // this.#canvas.width = 800; - // this.#canvas.height = 600; - // this.#canvas.id = canvas_id; - // this.#canvas.className = "slint-preview"; - // this.#canvas.style.scale = (this.#zoom_level / 100).toString(); - // this.#canvas.style.padding = "0px"; - // this.#canvas.style.margin = "0px"; - // this.#canvas.style.position = "absolute"; - // this.#canvas.style.imageRendering = "pixelated"; - // - // this.#canvas.dataset.slintAutoResizeToPreferred = "true"; - // - // this.contentNode.appendChild(this.#canvas); - // - // const update_scroll_size = () => { - // this.update_scroll_size(); - // }; - // - // update_scroll_size(); - // - // // Callback function to execute when mutations are observed - // this.#canvas_observer = new MutationObserver((mutationList) => { - // for (const mutation of mutationList) { - // if ( - // mutation.type === "attributes" && - // mutation.attributeName === "style" - // ) { - // update_scroll_size(); - // } - // } - // }); - // this.#canvas_observer.observe(this.#canvas, { attributes: true }); - // - // this.#previewer.canvas_id = canvas_id; - } - protected get contentNode(): HTMLDivElement { return this.node.getElementsByClassName( "preview-container", )[0] as HTMLDivElement; } - - dispose() { - super.dispose(); - // this.#canvas_observer?.disconnect(); - } - - protected onAfterAttach(_msg: Message): void { - // super.onAfterAttach(msg); - // this.#previewer.canvas_id = this.canvasNode.id; - } - - protected onResize(_msg: Message): void { - // if (this.isAttached) { - // this.update_scroll_size(); - // } - } } diff --git a/tools/slintpad/src/shared/lsp_commands.ts b/tools/slintpad/src/shared/lsp_commands.ts index 4e155cb843..1f705c41c8 100644 --- a/tools/slintpad/src/shared/lsp_commands.ts +++ b/tools/slintpad/src/shared/lsp_commands.ts @@ -37,16 +37,6 @@ export async function showPreview( return vscode.commands.executeCommand("slint/showPreview", url, component); } -export async function setDesignMode( - enable: boolean, -): Promise { - return vscode.commands.executeCommand("slint/setDesignMode", enable); -} - -export async function toggleDesignMode(): Promise { - return vscode.commands.executeCommand("slint/toggleDesignMode"); -} - export async function setBinding( doc: OptionalVersionedTextDocumentIdentifier, element_range: LspRange, diff --git a/tools/slintpad/src/worker/lsp_worker.ts b/tools/slintpad/src/worker/lsp_worker.ts index 9f74715622..59caa85845 100644 --- a/tools/slintpad/src/worker/lsp_worker.ts +++ b/tools/slintpad/src/worker/lsp_worker.ts @@ -9,74 +9,75 @@ import { BrowserMessageWriter, } from "vscode-languageserver/browser"; -slint_init() - .then(() => { - const reader = new BrowserMessageReader(self); - const writer = new BrowserMessageWriter(self); +slint_init().then(() => { + const reader = new BrowserMessageReader(self); + const writer = new BrowserMessageWriter(self); - let the_lsp: slint_lsp.SlintServer; + let the_lsp: slint_lsp.SlintServer; - const connection = createConnection(reader, writer); + const connection = createConnection(reader, writer); - function send_notification(method: string, params: unknown): boolean { - connection.sendNotification(method, params); - return true; - } + function send_notification(method: string, params: unknown): boolean { + connection.sendNotification(method, params); + return true; + } - async function send_request( - method: string, - params: unknown, - ): Promise { - return await connection.sendRequest(method, params); - } + async function send_request( + method: string, + params: unknown, + ): Promise { + return await connection.sendRequest(method, params); + } - async function load_file(path: string): Promise { - return await connection.sendRequest("slint/load_file", path); - } + async function load_file(path: string): Promise { + return await connection.sendRequest("slint/load_file", path); + } - connection.onInitialize( - (params: InitializeParams): InitializeResult => { - the_lsp = slint_lsp.create( - params, - send_notification, - send_request, - load_file, - ); - const response = the_lsp.server_initialize_result( - params.capabilities, - ); - response.capabilities.codeLensProvider = null; // CodeLenses are not relevant for Slintpad - return response; - }, + connection.onInitialize((params: InitializeParams): InitializeResult => { + the_lsp = slint_lsp.create( + params, + send_notification, + send_request, + load_file, ); - - connection.onRequest(async (method, params, token) => { - return await the_lsp.handle_request(token, method, params); - }); - - connection.onDidChangeTextDocument(async (param) => { - await the_lsp.reload_document( - param.contentChanges[param.contentChanges.length - 1].text, - param.textDocument.uri, - param.textDocument.version, - ); - }); - - connection.onDidOpenTextDocument(async (param) => { - await the_lsp.reload_document( - param.textDocument.text, - param.textDocument.uri, - param.textDocument.version, - ); - }); - - connection.onDidChangeConfiguration(async (_param) => { - await the_lsp.reload_config(); - }); - - // Listen on the connection - connection.listen(); - - // Now that we listen, the client is ready to send the init message - self.postMessage("OK"); + return the_lsp.server_initialize_result(params.capabilities); }); + + connection.onRequest(async (method, params, token) => { + return await the_lsp.handle_request(token, method, params); + }); + + connection.onNotification( + "slint/preview_to_lsp", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async (params: any) => { + await the_lsp.process_preview_to_lsp_message(params); + }, + ); + + connection.onDidChangeTextDocument(async (param) => { + await the_lsp.reload_document( + param.contentChanges[param.contentChanges.length - 1].text, + param.textDocument.uri, + param.textDocument.version, + ); + }); + + connection.onDidOpenTextDocument(async (param) => { + await the_lsp.reload_document( + param.textDocument.text, + param.textDocument.uri, + param.textDocument.version, + ); + }); + + connection.onDidChangeConfiguration(async (_param) => { + await the_lsp.reload_config(); + }); + + // Listen on the connection + connection.listen(); + + // Now that we listen, the client is ready to send the init message + self.postMessage("OK"); +}); diff --git a/tools/slintpad/tsconfig.default.json b/tools/slintpad/tsconfig.default.json index 5e9f2b6a3b..bd92f16c90 100644 --- a/tools/slintpad/tsconfig.default.json +++ b/tools/slintpad/tsconfig.default.json @@ -10,7 +10,8 @@ "moduleResolution": "node", "outDir": "./dist", "paths": { - "@lsp/*": ["../../lsp/pkg/*"] + "@lsp/*": ["../../lsp/pkg/*"], + "@interpreter/*": ["../../../api/wasm-interpreter/pkg/*"] }, "rootDir": ".", "skipLibCheck": true, diff --git a/tools/slintpad/vite.config.ts b/tools/slintpad/vite.config.ts index 967506371b..32be6eb16b 100644 --- a/tools/slintpad/vite.config.ts +++ b/tools/slintpad/vite.config.ts @@ -29,6 +29,10 @@ export default defineConfig(() => { resolve: { alias: { "@lsp": resolve(__dirname, "../lsp/pkg"), + "@interpreter": resolve( + __dirname, + "../../api/wasm-interpreter/pkg", + ), "~@lumino": "node_modules/@lumino/", // work around strange defaults in @lumino path: "path-browserify", // To make path.sep available to monaco },