lsp: Get signalled by the preview

This commit is contained in:
Tobias Hunger 2023-09-11 15:00:32 +02:00 committed by Olivier Goffart
parent 4310969b2a
commit d53cebd3e1
25 changed files with 776 additions and 1420 deletions

View file

@ -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",

View file

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

View file

@ -1,6 +1,8 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
// cSpell: ignore 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];
}

View file

@ -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<boolean>("preview.providedByEditor")
) {
wasm_preview.showPreview(
context,
vscode.Uri.parse(args[0], true),
args[1],
);
return true;
}
return false;
},
(_) => {
if (
vscode.workspace
.getConfiguration("slint")
.get<boolean>("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),
);

View file

@ -1,201 +1,92 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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<string, string>();
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<string> {
// 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 `<!DOCTYPE html>
const result = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
@ -203,181 +94,72 @@ function getPreviewHtml(slint_wasm_preview_url: Uri): string {
<title>Slint Preview</title>
<script type="module">
"use strict";
import * as slint from '${slint_wasm_preview_url}';
await slint.default();
import * as slint_preview from '${slint_wasm_preview_url}';
await slint_preview.default();
const vscode = acquireVsCodeApi();
let promises = {};
let current_instance = null;
let design_mode = false;
async function load_file(url) {
let promise = new Promise(resolve => {
promises[url] = resolve;
});
vscode.postMessage({ command: 'load_file', url: url });
let from_editor = await promise;
return from_editor || await (await fetch(url)).text();
try {
slint_preview.run_event_loop();
} catch (_) {
// This is actually not an error:-/
}
async function element_selected(url, sl, sc, el, ec) {
vscode.postMessage({ command: 'element_selected', data: { start: { line: sl, column: sc }, end: { line: el, column: ec }, url: url }});
}
let preview_connector = await slint_preview.PreviewConnector.create(
(data) => { vscode.postMessage({ command: "slint/preview_to_lsp", params: data }); }
);
async function render(source, base_url, style) {
let { component, error_string } =
style ? await slint.compile_from_string_with_style(source, base_url, style, async(url) => Promise.resolve(undefined), async(url) => await load_file(url))
: await slint.compile_from_string(source, base_url, async(url) => Promise.resolve(undefined), async(url) => await load_file(url));
if (error_string != "") {
var text = document.createTextNode(error_string);
var p = document.createElement('pre');
p.appendChild(text);
document.getElementById("slint_error_div").innerHTML = "<pre style='color: red; background-color:#fee; margin:0'>" + p.innerHTML + "</pre>";
}
vscode.postMessage({ command: 'preview_ready' });
if (component !== undefined) {
document.getElementById("slint_error_div").innerHTML = "";
if (current_instance !== null) {
current_instance = component.create_with_existing_window(await current_instance);
} else {
try {
slint.run_event_loop();
} catch (e) {
// ignore event loop exception
}
current_instance = (async () => {
let new_instance = await component.create("slint_canvas");
await new_instance.show();
return new_instance;
})();
}
if (current_instance !== null) {
(await current_instance).set_design_mode(design_mode);
(await current_instance).on_element_selected(element_selected);
}
}
}
window.addEventListener('message', async message => {
if (message.data.command === "slint/lsp_to_preview") {
preview_connector.process_lsp_to_preview_message(
message.data.params,
);
window.addEventListener('message', async event => {
if (event.data.command === "preview") {
design_mode = !!event.data.design_mode;
vscode.setState({base_url: event.data.base_url, component: event.data.component});
await render(event.data.content, event.data.webview_uri, event.data.style);
} else if (event.data.command === "file_loaded") {
let resolve = promises[event.data.url];
if (resolve) {
delete promises[event.data.url];
resolve(event.data.content);
}
} else if (event.data.command === "highlight") {
if (current_instance) {
(await current_instance).highlight(event.data.data.path, event.data.data.offset);
}
} else if (event.data.command === "toggle_design_mode") {
design_mode = !design_mode;
if (current_instance != null) {
(await current_instance).set_design_mode(design_mode);
(await current_instance).on_element_selected(element_selected);
}
return true;
}
});
vscode.postMessage({ command: 'preview_ready' });
preview_connector.show_ui().then(() => vscode.postMessage({ command: 'preview_ready' }));
</script>
</head>
<body>
<div id="slint_error_div"></div>
<canvas style="margin-top: 10px;" id="slint_canvas"></canvas>
<canvas style="margin-top: 10px; width: 100%; height:100%" id="canvas"></canvas>
</body>
</html>`;
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;
}

View file

@ -647,6 +647,13 @@ impl TypeLoader {
pub fn all_documents(&self) -> impl Iterator<Item = &object_tree::Document> + '_ {
self.all_documents.docs.values()
}
/// Returns an iterator over all the loaded documents
pub fn all_file_documents(
&self,
) -> impl Iterator<Item = (&PathBuf, &object_tree::Document)> + '_ {
self.all_documents.docs.iter()
}
}
fn get_native_style(all_loaded_files: &mut Vec<PathBuf>) -> String {

View file

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

View file

@ -6,6 +6,7 @@
use std::{
collections::HashMap,
path::{Path, PathBuf},
rc::Rc,
};
pub type Error = Box<dyn std::error::Error>;
@ -14,6 +15,8 @@ pub type Result<T> = std::result::Result<T, Error>;
/// 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<crate::language::Context>);
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<String>,
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<lsp_types::Diagnostic>,
},
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!
}

View file

@ -58,7 +58,7 @@ fn command_list() -> Vec<String> {
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::<ExecuteCommand, _>(|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(&params.arguments, &ctx)?;
return Ok(None::<serde_json::Value>);
}
@ -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<Context>) -> 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<Vec<CodeLens>> {
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)?;

View file

@ -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<bool>,
to_show: RefCell<Option<common::PreviewComponent>>,
}
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<crate::language::Context>) {
#[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<String, PathBuf>,
) {
#[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<std::path::PathBuf>, _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::<Pin<Box<dyn Future<Output = Result<()>>>>>::new();
@ -312,7 +432,7 @@ async fn handle_notification(req: lsp_server::Notification, ctx: &Rc<Context>) -
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<Context>) -
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<Context>) -
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);
}
}
}
_ => (),
}

View file

@ -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<String> {
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<PathBuf>, offset: u32) {
pub fn highlight(path: &Option<PathBuf>, 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<PathBuf>, 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<lsp_types::ShowDocumentParams> {
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<lsp_types::Url, Vec<lsp_types::Diagnostic>> {
let mut result: HashMap<lsp_types::Url, Vec<lsp_types::Diagnostic>> = Default::default();
for d in diagnostics {
if d.source_file().map_or(true, |f| f.is_relative()) {
continue;
}
let uri = lsp_types::Url::from_file_path(d.source_file().unwrap()).unwrap();
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<lsp_types::Diagnostic>,
) -> 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::<lsp_types::request::ShowDocument>(params) else {
return;
};
i_slint_core::future::spawn_local(fut).unwrap();
}

View file

@ -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<PreviewState> = 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<lsp_types::ShowDocumentParams> {
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<lsp_types::Url, Vec<lsp_types::Diagnostic>> = Default::default();
for d in diagnostics {
if d.source_file().map_or(true, |f| f.is_relative()) {
continue;
}
let uri = lsp_types::Url::from_file_path(d.source_file().unwrap()).unwrap();
lsp_diags.entry(uri).or_default().push(crate::util::to_lsp_diag(d));
}
for (uri, diagnostics) in lsp_diags {
sender
.send_notification(
"textDocument/publishDiagnostics".into(),
lsp_types::PublishDiagnosticsParams { uri, diagnostics, version: None },
)
.ok()?;
}
Some(())
}
/// 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();

View file

@ -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<PreviewUi, PlatformError> {
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)
}

View file

@ -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<WrappedCompiledComp>,
diagnostics: js_sys::Array,
error_string: String,
}
#[wasm_bindgen]
impl CompilationResult {
#[wasm_bindgen(getter)]
pub fn component(&self) -> Option<WrappedCompiledComp> {
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<string | undefined>;
type ImportCallbackFunction = (url: string) => Promise<string>;
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<WrappedInstance>")]
pub type InstancePromise;
#[wasm_bindgen(typescript_type = "Promise<PreviewConnector>")]
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<Rc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = Option<String>>>>>> {
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<ResourceUrlMapperFunction>,
optional_import_callback: Option<ImportCallbackFunction>,
) -> Result<CompilationResult, JsValue> {
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<ResourceUrlMapperFunction>,
optional_import_callback: Option<ImportCallbackFunction>,
) -> Result<CompilationResult, JsValue> {
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<dyn core::future::Future<Output = Option<std::io::Result<String>>>>,
> {
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 <canvas> 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 <canvas> 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<InstancePromise, JsValue> {
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::<InstancePromise>())
}
/// 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<InstancePromise, JsValue> {
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::<InstancePromise>())
}
}
#[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<js_sys::Promise, JsValue> {
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<js_sys::Promise, JsValue> {
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<js_sys::Promise, JsValue> {
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<super::ui::PreviewUi>,
handle: Rc<RefCell<Option<slint_interpreter::ComponentInstance>>>,
lsp_notifier: Option<SignalLspFunction>,
resource_url_mapper: Option<ResourceUrlMapperFunction>,
}
thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();}
#[wasm_bindgen]
pub struct PreviewConnector {
current_previewed_component: RefCell<Option<PreviewComponent>>,
}
pub struct PreviewConnector {}
#[wasm_bindgen]
impl PreviewConnector {
#[wasm_bindgen]
pub fn create() -> Result<PreviewConnectorPromise, JsValue> {
pub fn create(
lsp_notifier: SignalLspFunction,
resource_url_mapper: ResourceUrlMapperFunction,
) -> Result<PreviewConnectorPromise, JsValue> {
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<PathBuf> = include_paths.iter().map(|p| PathBuf::from(p)).collect();
let ip: Vec<PathBuf> = include_paths.iter().map(PathBuf::from).collect();
let lp: HashMap<String, PathBuf> =
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<Rc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = Option<String>>>>>> {
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);
}
})
})

View file

@ -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<crate::language::Context>) {
#[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<String, PathBuf>,
) {
#[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<std::path::PathBuf>, 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::<M>(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<JsValue> {
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();

View file

@ -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",

View file

@ -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<string>,
) => Promise<monaco.editor.IMarkerData[]>;
#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<string>,
) => Promise<monaco.editor.IMarkerData[]>,
) {
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<string>,
) => Promise<monaco.editor.IMarkerData[]>,
) {
this.#editor.onRenderRequest = request;
}
set onPositionChange(cb: PositionChangeCallback) {
this.#editor.onPositionChangeCallback = cb;
}

View file

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

View file

@ -52,15 +52,10 @@ function createLanguageClient(
export type FileReader = (_url: string) => Promise<string>;
export class LspWaiter {
#previewer_port: MessagePort;
#previewer_promise: Promise<slint_preview.InitOutput> | null;
#lsp_promise: Promise<Worker> | 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<Previewer> {
console.log("LSP: Grabbing Previewer!");
async previewer(
resource_url_mapper: ResourceUrlMapperFunction,
): Promise<Previewer> {
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);
}
}

View file

@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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<string> => {
const file_source = loaded_documents.get(url);
if (file_source === undefined) {

View file

@ -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 = '<i class="fa fa-search-minus"></i>';
//
// 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 = '<i class="fa fa-search-plus"></i>';
//
// 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 = '<i class="fa fa-eyedropper"></i>';
//
// 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();
// }
}
}

View file

@ -37,16 +37,6 @@ export async function showPreview(
return vscode.commands.executeCommand("slint/showPreview", url, component);
}
export async function setDesignMode(
enable: boolean,
): Promise<SetBindingResponse> {
return vscode.commands.executeCommand("slint/setDesignMode", enable);
}
export async function toggleDesignMode(): Promise<SetBindingResponse> {
return vscode.commands.executeCommand("slint/toggleDesignMode");
}
export async function setBinding(
doc: OptionalVersionedTextDocumentIdentifier,
element_range: LspRange,

View file

@ -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<unknown> {
return await connection.sendRequest(method, params);
}
async function send_request(
method: string,
params: unknown,
): Promise<unknown> {
return await connection.sendRequest(method, params);
}
async function load_file(path: string): Promise<string> {
return await connection.sendRequest("slint/load_file", path);
}
async function load_file(path: string): Promise<string> {
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");
});

View file

@ -10,7 +10,8 @@
"moduleResolution": "node",
"outDir": "./dist",
"paths": {
"@lsp/*": ["../../lsp/pkg/*"]
"@lsp/*": ["../../lsp/pkg/*"],
"@interpreter/*": ["../../../api/wasm-interpreter/pkg/*"]
},
"rootDir": ".",
"skipLibCheck": true,

View file

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