Files
webstudio/apps/builder/app/shared/code-editor-base.test.ts
Oleg Isonen cfba520e6d feat: Open text assets in code editor (#5918)
## What changed

- open every textual asset type from the upload registry in an editable
CodeMirror dialog
- select installed CodeMirror language support by MIME type and use
plain text editing as the fallback
- support `txt`, `csv`, `md`, `js`, `css`, `json`, `html`, `xml`, and
`svg` without adding asset-specific language branches to the shared
editor
- keep the canvas mounted and interactive behind the dialog
- persist edits on blur, `Cmd/Ctrl+S`, or `Cmd/Ctrl+Enter` through the
asset replacement pipeline
- preserve folder and description metadata and repoint supported asset
references
- clean up failed replacement uploads instead of leaving orphan assets

## Impact

Users can open, edit, and persist all text-based upload formats directly
in the builder without replacing or reloading the canvas. New `text/*`
upload types will automatically become editable as plain text; installed
syntax support is selected by MIME type.

## Validation

- `pnpm --filter @webstudio-is/builder test --
app/builder/features/text-file-editor/text-file-utils.test.ts
app/builder/shared/assets/replace-asset.test.ts
app/builder/shared/asset-manager/asset-manager.test.tsx`
- `pnpm --filter @webstudio-is/builder typecheck`
- `pnpm exec oxlint --deny-warnings <changed files>`
- `pnpm exec oxfmt --check <changed files>`
- `git diff --check`
- verified the running dev server applied the changes through Vite HMR

Ref #5824
2026-07-20 13:42:06 +02:00

77 lines
2.0 KiB
TypeScript

import { EditorSelection } from "@codemirror/state";
import { expect, test } from "vitest";
import {
clampEditorSelection,
getTemplateInsertion,
normalizeEditorValue,
} from "./code-editor-base";
test("clampEditorSelection preserves selection within document length", () => {
const selection = EditorSelection.create([
EditorSelection.cursor(3),
EditorSelection.range(6, 8),
]);
const clampedSelection = clampEditorSelection(selection, 10);
expect(
clampedSelection.ranges.map((range) => [range.anchor, range.head])
).toEqual([
[3, 3],
[6, 8],
]);
expect(clampedSelection.mainIndex).toBe(selection.mainIndex);
});
test("clampEditorSelection clamps selection to document length", () => {
const selection = EditorSelection.create(
[EditorSelection.range(1, 3), EditorSelection.range(8, 12)],
1
);
const clampedSelection = clampEditorSelection(selection, 5);
expect(
clampedSelection.ranges.map((range) => [range.anchor, range.head])
).toEqual([
[1, 3],
[5, 5],
]);
expect(clampedSelection.mainIndex).toBe(selection.mainIndex);
});
test("normalizeEditorValue defaults undefined to empty string", () => {
expect(normalizeEditorValue(undefined)).toBe("");
expect(normalizeEditorValue("value")).toBe("value");
});
test("getTemplateInsertion wraps and selects existing text", () => {
const insertion = getTemplateInsertion({
from: 4,
selectedText: "selected",
prefix: "**",
suffix: "**",
placeholder: "bold text",
});
expect(insertion.text).toBe("**selected**");
expect([insertion.selection.anchor, insertion.selection.head]).toEqual([
6, 14,
]);
});
test("getTemplateInsertion inserts and selects a placeholder", () => {
const insertion = getTemplateInsertion({
from: 3,
selectedText: "",
prefix: "[",
suffix: "](https://)",
placeholder: "link text",
});
expect(insertion.text).toBe("[link text](https://)");
expect([insertion.selection.anchor, insertion.selection.head]).toEqual([
4, 13,
]);
});