Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@

type-complexity-threshold = 2500
too-many-arguments-threshold = 10

# `disallowed-methods` is the configuration for the `clippy::disallowed_methods`
# lint, which is allow-by-default. Listing entries here therefore has no effect
# in crates that don't opt into the lint. We currently enable it only in the
# slint-lsp crate (see `#![deny(clippy::disallowed_methods)]` in `tools/lsp/main.rs`
# and `tools/lsp/wasm_main.rs`), so the entries below are effectively scoped to
# slint-lsp. Putting them here (rather than in `tools/lsp/clippy.toml`) avoids
# shadowing the workspace-wide thresholds above — clippy reads exactly one
# clippy.toml per crate, with no merging.
disallowed-methods = [
{ path = "tokio::task::spawn_local", reason = "Use `slint_lsp::common::spawn_local` — it routes to `wasm_bindgen_futures::spawn_local` on wasm." },
]
8 changes: 8 additions & 0 deletions .github/workflows/wasm_editor_and_interpreter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ jobs:
if: ${{ always() }}
working-directory: tools/slintpad
run: npx github-actions-ctrf playwright-report/ctrf-report.json
- name: "Upload Playwright Report"
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: slintpad-playwright-report
path: |
tools/slintpad/playwright-report/
tools/slintpad/test-results/
- name: "Upload slintpad Artifacts"
uses: actions/upload-artifact@v7
with:
Expand Down
14 changes: 14 additions & 0 deletions tools/lsp/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ pub type Result<T> = std::result::Result<T, Error>;
#[cfg(target_arch = "wasm32")]
use crate::wasm_prelude::*;

#[allow(clippy::disallowed_methods)]
pub fn spawn_local<F>(future: F)
where
F: std::future::Future + 'static,
F::Output: 'static,
{
#[cfg(target_arch = "wasm32")]
wasm_bindgen_futures::spawn_local(async move {
let _ = future.await;
});
#[cfg(not(target_arch = "wasm32"))]
tokio::task::spawn_local(future);
}

/// Use this in nodes you want the language server and preview to
/// ignore a node for code analysis purposes.
pub const NODE_IGNORE_COMMENT: &str = "@lsp:ignore-node";
Expand Down
2 changes: 1 addition & 1 deletion tools/lsp/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ pub fn register_request_handlers(rh: &mut RequestHandler) {
return Ok(None::<serde_json::Value>);
}
if params.command.as_str() == POPULATE_COMMAND {
tokio::task::spawn_local(populate_command(&params.arguments, ctx)?);
common::spawn_local(populate_command(&params.arguments, ctx)?);
return Ok(None::<serde_json::Value>);
}
Ok(None::<serde_json::Value>)
Expand Down
2 changes: 1 addition & 1 deletion tools/lsp/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

#![cfg(not(target_arch = "wasm32"))]
#![allow(clippy::await_holding_refcell_ref)]
#![deny(clippy::print_stderr, clippy::print_stdout)]
#![deny(clippy::print_stderr, clippy::print_stdout, clippy::disallowed_methods)]

#[cfg(all(feature = "preview-engine", not(feature = "preview-builtin")))]
compile_error!(
Expand Down
2 changes: 1 addition & 1 deletion tools/lsp/wasm_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

#![cfg(target_arch = "wasm32")]
#![deny(clippy::print_stderr, clippy::print_stdout)]
#![deny(clippy::print_stderr, clippy::print_stdout, clippy::disallowed_methods)]

pub mod common;
mod fmt;
Expand Down
48 changes: 48 additions & 0 deletions tools/slintpad/tests/populate-command.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

// Regression test for https://github.com/slint-ui/slint/issues/11416:
// clicking the "Start with Hello World!" code lens on an empty buffer used to
// panic the wasm LSP because the command handler called tokio::task::spawn_local
// outside of a LocalSet.
import { test, expect } from "@playwright/test";

test("'Start with Hello World!' code lens populates the editor without panicking", async ({
page,
browserName,
}) => {
// Headless Firefox on CI has no WebGL, so opening the SlintPad preview
// panics in internal/renderers/femtovg/opengl.rs:134 with
// "Cannot proceed without WebGL - aborting". That panic shows a modal
// dialog that intercepts our code-lens click. Skip until the preview
// either gets a software-WebGL fallback or stops panicking on init.
test.skip(browserName === "firefox", "preview panics without WebGL");

// A single-whitespace `snippet` makes SlintPad open a main.slint whose content
// has no non-whitespace tokens, which is the condition under which the LSP
// emits the "Start with Hello World!" code lens (see tools/lsp/language.rs).
await page.goto("http://localhost:3000/?snippet=%20");
await expect(page.locator("#tab-key-1-0")).toContainText("main.slint");

// Wait for the LSP-provided code lens to show up and click it. This sends
// `workspace/executeCommand slint/populate`, which is the path that used
// to panic in the wasm LSP.
const code_lens = page.getByRole("button", {
name: "Start with Hello World!",
});
await expect(code_lens).toBeVisible({ timeout: 20_000 });
await code_lens.click();

// Wait until the LSP has responded — either the populate edit landed, or
// a panic dialog popped up.
const editor = page.locator(".monaco-editor").first();
const panic_dialog = page.locator("dialog.panic_dialog");
const main_window = editor.getByText("MainWindow");
await expect(panic_dialog.or(main_window)).toBeVisible({ timeout: 15_000 });

// The critical assertion: no panic dialog. On failure the page snapshot
// in the error context will show the panic message from the LSP.
await expect(panic_dialog).toHaveCount(0);
// And the populate edit actually landed.
await expect(editor).toContainText('"Hello World!"');
});
Loading