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
20 changes: 16 additions & 4 deletions test/webview/cm-code-ref-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { codeRefReveal } from "../../src/webview/cm/code-ref/code-ref-reveal.js";
import { fullTree } from "./helpers/full-tree.js";
import { settledState } from "./helpers/settled-state.js";
import { settledMount } from "./helpers/settled-view.js";

function stateFor(doc: string, sel?: number) {
// `tryOpenCodeRefAt` reads `syntaxTree(state)` (the STATE's own field
Expand Down Expand Up @@ -228,7 +229,9 @@ describe("handleCodeRefClick", () => {
visibleRanges: [{ from: 0, to: doc.length }],
tree: fullTree(base),
});
return new EditorView({
// settledMount: handleCodeRefClick's tryOpenCodeRefAt reads syntaxTree(state) —
// an unsettled mount leaves the field on the init-viewport fragment.
return settledMount({
state: EditorState.create({
doc,
extensions: [markdown(), EditorView.decorations.of(revealSet)],
Expand Down Expand Up @@ -304,7 +307,10 @@ describe("quollCodeRefKeymap precedence", () => {
function precedenceView(doc: string, caret: number, host: unknown): EditorView {
const parent = document.createElement("div");
document.body.appendChild(parent);
return new EditorView({
// settledMount: openCodeRefAtCaretCommand's tryOpenCodeRefAt reads
// syntaxTree(state) — an unsettled mount leaves the field on the
// init-viewport fragment.
return settledMount({
state: EditorState.create({
doc,
selection: EditorSelection.cursor(caret),
Expand Down Expand Up @@ -430,7 +436,10 @@ describe("quollCodeRefClickHandler", () => {
visibleRanges: [{ from: 0, to: DOC.length }],
tree: fullTree(base),
});
const view = new EditorView({
// settledMount: tryOpenCodeRefAt (via quollCodeRefClickHandler) reads
// syntaxTree(state) — an unsettled mount leaves the field on the
// init-viewport fragment.
const view = settledMount({
state: EditorState.create({
doc: DOC,
extensions: [
Expand Down Expand Up @@ -518,7 +527,10 @@ describe("openCodeRefAtCaretCommand", () => {
function viewWithCaret(doc: string, caret: number): EditorView {
const parent = document.createElement("div");
document.body.appendChild(parent);
return new EditorView({
// settledMount: openCodeRefAtCaretCommand's tryOpenCodeRefAt reads
// syntaxTree(state) — an unsettled mount leaves the field on the
// init-viewport fragment.
return settledMount({
state: EditorState.create({
doc,
selection: EditorSelection.single(caret),
Expand Down
7 changes: 4 additions & 3 deletions test/webview/decorations/cm-decoration-block-style.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// @vitest-environment happy-dom
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
import { EditorSelection, EditorState } from "@codemirror/state";
import type { DecorationSet } from "@codemirror/view";
import { EditorView } from "@codemirror/view";
import type { DecorationSet, EditorView } from "@codemirror/view";
import { type Tag, tags as t } from "@lezer/highlight";
import { describe, expect, it } from "vitest";
import {
Expand All @@ -23,6 +22,7 @@ import {
import type { BuildContext } from "../../../src/webview/cm/decorations/types.js";
import { blockStyleThemeSpec, quollHighlightSpec } from "../../../src/webview/cm/theme.js";
import { fullTree } from "../helpers/full-tree.js";
import { settledMount } from "../helpers/settled-view.js";

describe("theme.ts — quollHighlightSpec navy+green token contract (palette refresh)", () => {
// A spec entry's `tag` is a Tag OR a readonly Tag[] (the monospace entry uses
Expand Down Expand Up @@ -1118,7 +1118,8 @@ describe("block-style — plugin rebuild triggers (caret move)", () => {
// resolve the individual instances (Codex 90).
extensions: [markdown({ base: markdownLanguage }), blockStyle],
});
return new EditorView({ state, parent });
// Settle: blockquoteRule / fencedCodePanel read syntaxTree(view.state) via toCtx(view).
return settledMount({ state, parent });
}

it("a selection-only transaction rebuilds the fenced panel but NOT the blockquote rule", () => {
Expand Down
7 changes: 5 additions & 2 deletions test/webview/decorations/cm-decoration-orchestrator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import type {
BuildContext,
DecorationProvider,
} from "../../../src/webview/cm/decorations/types.js";
import { settledView } from "../helpers/settled-view.js";
import { settledMount, settledView } from "../helpers/settled-view.js";

function tagsOf(set: DecorationSet): string[] {
const out: string[] = [];
Expand Down Expand Up @@ -458,7 +458,10 @@ describe("multi-cursor arbitration regression", () => {
quollSyntaxReveal(),
],
});
const view = new EditorView({ state, parent });
// Settle: orchestrator.ts reads syntaxTree(view.state) into ctx.tree.
// settledMount, not settledView(new EditorView(…)): the latter strands the
// view if the settle throws, since nothing owns it until the assignment.
const view = settledMount({ state, parent });
try {
// Read the merged decoration set via the EditorView.decorations facet.
const sources = view.state.facet(EditorView.decorations);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import process from "node:process";
import { EditorSelection, EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import type { EditorView } from "@codemirror/view";
import { describe, expect, it, vi } from "vitest";
import { quollSyntaxReveal } from "../../../src/webview/cm/decorations/index.js";
import { quollMarkdownLanguage } from "../../../src/webview/cm/markdown.js";
import { quollHighlighting, quollTokenMarkers } from "../../../src/webview/cm/theme.js";
import { settledMount } from "../helpers/settled-view.js";

vi.mock("../../../src/webview/host.js", () => ({
getHost: () => ({ postMessage: vi.fn() }),
Expand All @@ -33,7 +34,8 @@ const BODY_PX = 14;
function mount(doc: string): EditorView {
const parent = document.createElement("div");
document.body.appendChild(parent);
return new EditorView({
// Settle: quollSyntaxReveal() + quollHighlighting read syntaxTree(view.state).
return settledMount({
parent,
state: EditorState.create({
doc,
Expand Down
6 changes: 4 additions & 2 deletions test/webview/decorations/cm-decoration-thematic-break.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// @vitest-environment happy-dom
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
import { EditorSelection, EditorState } from "@codemirror/state";
import { type DecorationSet, EditorView } from "@codemirror/view";
import type { DecorationSet, EditorView } from "@codemirror/view";
import { describe, expect, it, vi } from "vitest";
import { quollSyntaxReveal } from "../../../src/webview/cm/decorations/index.js";
import { thematicBreakReveal } from "../../../src/webview/cm/decorations/thematic-break-reveal.js";
import { ThematicBreakWidget } from "../../../src/webview/cm/decorations/thematic-break-widget.js";
import type { BuildContext } from "../../../src/webview/cm/decorations/types.js";
import { frontmatterBlockField } from "../../../src/webview/cm/frontmatter/frontmatter-field.js";
import { fullTree } from "../helpers/full-tree.js";
import { settledMount } from "../helpers/settled-view.js";

vi.mock("../../../src/webview/host.js", () => ({
getHost: () => ({ postMessage: vi.fn() }),
Expand Down Expand Up @@ -300,7 +301,8 @@ function mountWithFrontmatter(doc: string): EditorView {
selection: EditorSelection.single(doc.length), // caret at very end, off every HR line
extensions: [markdown({ base: markdownLanguage }), quollSyntaxReveal(), frontmatterBlockField],
});
return new EditorView({ state, parent });
// Settle: thematicBreakReveal classifies HorizontalRule off syntaxTree(view.state).
return settledMount({ state, parent });
}

describe("thematic break — orchestrator integration", () => {
Expand Down
28 changes: 16 additions & 12 deletions test/webview/fenced-code/cm-fenced-code-collapse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,18 +367,22 @@ describe("buildFencedCollapse", () => {
it("DD4: auto-expands when a SECONDARY selection head is inside the concealed region", () => {
const doc = fencedDoc(11);
const insidePos = EditorState.create({ doc }).doc.line(12).from + 1; // body line 11
const state = EditorState.create({
doc,
// main range index 0 = the OUTSIDE caret; a secondary caret sits inside.
selection: EditorSelection.create(
[EditorSelection.cursor(0), EditorSelection.cursor(insidePos)],
0
),
extensions: [
markdown({ base: markdownLanguage }),
EditorState.allowMultipleSelections.of(true),
],
});
// State-only (never mounted) — settle so buildFencedRange's syntaxTree(state)
// read sees the full FencedCode tree, not the truncated init-viewport snapshot.
const state = settledState(
EditorState.create({
doc,
// main range index 0 = the OUTSIDE caret; a secondary caret sits inside.
selection: EditorSelection.create(
[EditorSelection.cursor(0), EditorSelection.cursor(insidePos)],
0
),
extensions: [
markdown({ base: markdownLanguage }),
EditorState.allowMultipleSelections.of(true),
],
})
);
const { decorations, liveExpanded } = buildFencedCollapse(state, new Set());
expect(dump(decorations)[0].isReplace).toBe(false); // expanded via the secondary head
expect([...liveExpanded]).toEqual([0]);
Expand Down
32 changes: 22 additions & 10 deletions test/webview/table/cm-table-field.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { splitToCmText } from "../../../src/webview/cm/seed.js";
import { tableBlockField } from "../../../src/webview/cm/table/index.js";
import type { TableBlockWidget } from "../../../src/webview/cm/table/table-widget.js";
import { fullTree } from "../helpers/full-tree.js";
import { settledMount } from "../helpers/settled-view.js";

function rangesOf(set: DecorationSet): Array<{ from: number; to: number }> {
const out: Array<{ from: number; to: number }> = [];
Expand Down Expand Up @@ -63,7 +64,11 @@ function mount(
...(extraExtensions as never[]),
],
});
return new EditorView({ state, parent });
// Settle: these extensions register tableBlockField WITHOUT tableSkeletonField,
// so resolveModels() takes its fallback arm into tableModels(state) — the
// syntaxTree(state) read (table-skeleton.ts). A mounted view never self-heals
// under happy-dom, so force the full tree at mount time.
return settledMount({ state, parent });
}

const TABLE = "| H1 | H2 |\n| -- | -- |\n| a1 | a2 |";
Expand Down Expand Up @@ -441,7 +446,8 @@ describe("tableBlockField — reveal and offset pins", () => {
const lfText = splitToCmText(raw);
const parent = document.createElement("div");
document.body.appendChild(parent);
const view = new EditorView({
// Settle: same tableModels(state)/syntaxTree(state) fallback read as mount() above.
const view = settledMount({
parent,
state: EditorState.create({
doc: lfText,
Expand Down Expand Up @@ -482,19 +488,25 @@ describe("tableBlockField — reveal and offset pins", () => {
it("drops the widget for a NON-EMPTY selection inside the table (drag reveal)", () => {
const doc = "before\n\n| Name | Role |\n| - | - |\n| alpha | admin |\n\nafter";
const from = doc.indexOf("alpha");
// The caret is the click path; the range is the drag path. Both must reveal.
const caretView = mount(doc, EditorSelection.single(from));
const rangeView = mount(doc, EditorSelection.single(from, from + 5));
// Control: a selection entirely outside the table keeps the widget.
const outsideView = mount(doc, EditorSelection.single(0, 3));
// Declared outside the try so `finally` can reach whichever mounts
// succeeded; each mount() call below already settles, so a throw on the
// 2nd/3rd construction must not strand the ones built before it.
let caretView: EditorView | undefined;
let rangeView: EditorView | undefined;
let outsideView: EditorView | undefined;
try {
// The caret is the click path; the range is the drag path. Both must reveal.
caretView = mount(doc, EditorSelection.single(from));
rangeView = mount(doc, EditorSelection.single(from, from + 5));
// Control: a selection entirely outside the table keeps the widget.
outsideView = mount(doc, EditorSelection.single(0, 3));
expect(rangesOf(caretView.state.field(tableBlockField))).toHaveLength(0);
expect(rangesOf(rangeView.state.field(tableBlockField))).toHaveLength(0);
expect(rangesOf(outsideView.state.field(tableBlockField))).toHaveLength(1);
} finally {
caretView.destroy();
rangeView.destroy();
outsideView.destroy();
caretView?.destroy();
rangeView?.destroy();
outsideView?.destroy();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import type { EditorView } from "@codemirror/view";
import { describe, expect, it } from "vitest";

import { toggleTaskCheckbox } from "../../../src/webview/cm/task-checkbox/task-checkbox-command.js";
import { CheckboxWidget } from "../../../src/webview/cm/task-checkbox/task-checkbox-widget.js";
import { settledMount } from "../helpers/settled-view.js";

function mountView(doc: string): EditorView {
const parent = document.createElement("div");
Expand All @@ -15,7 +16,9 @@ function mountView(doc: string): EditorView {
doc,
extensions: [markdown({ base: markdownLanguage })],
});
return new EditorView({ state, parent });
// toggleTaskCheckbox reads syntaxTree(view.state) (task-checkbox-command.ts) —
// settle so the mounted view's tree isn't stuck on the init-viewport fragment.
return settledMount({ state, parent });
}

describe("CheckboxWidget — DOM + a11y", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import type { EditorView } from "@codemirror/view";
import { describe, expect, it } from "vitest";

import { createSyntaxReveal } from "../../../src/webview/cm/decorations/orchestrator.js";
import { taskCheckboxReveal } from "../../../src/webview/cm/task-checkbox/task-checkbox-reveal.js";
import { settledMount } from "../helpers/settled-view.js";

describe("CheckboxWidget — CM reconcile invocation contract (updateDOM reuse)", () => {
// The direct-invocation tests in cm-task-checkbox-widget-toggle.test.ts call
Expand Down Expand Up @@ -37,7 +38,9 @@ describe("CheckboxWidget — CM reconcile invocation contract (updateDOM reuse)"
selection: { anchor },
extensions: [markdown({ base: markdownLanguage }), createSyntaxReveal([taskCheckboxReveal])],
});
return new EditorView({ state, parent });
// orchestrator.ts reads syntaxTree(view.state) to build the reveal — settle
// at mount only; a settle after dispatch would mask a broken updateDOM.
return settledMount({ state, parent });
}

it("an insert above a revealed checkbox reuses the SAME span node, re-stamped to the new offset", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import { history } from "@codemirror/commands";
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import type { EditorView } from "@codemirror/view";
import { describe, expect, it, vi } from "vitest";

import { CheckboxWidget } from "../../../src/webview/cm/task-checkbox/task-checkbox-widget.js";
import { settledMount } from "../helpers/settled-view.js";

describe("CheckboxWidget — toggle dispatch (focus + toggle-target)", () => {
function mountWithDoc(doc: string): EditorView {
Expand All @@ -16,7 +17,8 @@ describe("CheckboxWidget — toggle dispatch (focus + toggle-target)", () => {
doc,
extensions: [markdown({ base: markdownLanguage }), history()],
});
return new EditorView({ state, parent });
// toggleTaskCheckbox's Lezer cross-check reads syntaxTree(view.state).
return settledMount({ state, parent });
}

it("Space/Enter on the focused widget returns focus to the editor after toggle (round-3 #23 — keyboard UX)", () => {
Expand Down Expand Up @@ -84,7 +86,10 @@ describe("CheckboxWidget — toggle dispatch (focus + toggle-target)", () => {
doc: "- [ ] alpha",
extensions: [markdown({ base: markdownLanguage }), history(), EditorState.readOnly.of(true)],
});
const view = new EditorView({ state, parent });
// Settling is inert here — the readOnly guard aborts before the tree
// read — but applying it uniformly avoids a silent dependence on the
// guard's internal ORDER.
const view = settledMount({ state, parent });
try {
const w = new CheckboxWidget(false, 2, "alpha");
const el = w.toDOM(view);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import { history, undo } from "@codemirror/commands";
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import type { EditorView } from "@codemirror/view";
import { describe, expect, it } from "vitest";

import { CheckboxWidget } from "../../../src/webview/cm/task-checkbox/task-checkbox-widget.js";
import { settledMount } from "../helpers/settled-view.js";

describe("CheckboxWidget — toggle dispatch", () => {
function mountWithDoc(doc: string): EditorView {
Expand All @@ -16,7 +17,8 @@ describe("CheckboxWidget — toggle dispatch", () => {
doc,
extensions: [markdown({ base: markdownLanguage }), history()],
});
return new EditorView({ state, parent });
// toggleTaskCheckbox's Lezer cross-check reads syntaxTree(view.state).
return settledMount({ state, parent });
}

it("mousedown on the widget dispatches a single 3-char-position replace at from+1", () => {
Expand Down Expand Up @@ -210,7 +212,10 @@ describe("CheckboxWidget — toggle dispatch", () => {
doc: "- [ ] alpha",
extensions: [markdown({ base: markdownLanguage }), history(), EditorState.readOnly.of(true)],
});
const view = new EditorView({ state, parent });
// Settling is inert here — the readOnly guard aborts before the tree
// read — but applying it uniformly avoids a silent dependence on the
// guard's internal ORDER.
const view = settledMount({ state, parent });
try {
const w = new CheckboxWidget(false, 2, "alpha");
const el = w.toDOM(view);
Expand Down
Loading