mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-27 15:16:53 +09:00
## 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
77 lines
2.0 KiB
TypeScript
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,
|
|
]);
|
|
});
|