mirror of
https://github.com/slint-ui/slint.git
synced 2025-12-23 09:19:32 +00:00
lsp: Get signalled by the preview
This commit is contained in:
parent
4310969b2a
commit
d53cebd3e1
25 changed files with 776 additions and 1420 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(¶ms.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)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
{},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"@lsp/*": ["../../lsp/pkg/*"]
|
||||
"@lsp/*": ["../../lsp/pkg/*"],
|
||||
"@interpreter/*": ["../../../api/wasm-interpreter/pkg/*"]
|
||||
},
|
||||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue