@surrealdb/ui 1.2.16 → 1.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/1299e4695ad0bd61.svg +5 -0
- package/dist/assets/408117e904653548.svg +48 -0
- package/dist/assets/b516b020352f6369.svg +1 -0
- package/dist/assets/dfde8de566c3c2b9.svg +11 -0
- package/dist/assets/e9eae7bc863f211b.svg +1 -0
- package/dist/assets/f74ae3707a5d418f.svg +1 -0
- package/dist/ui.d.ts +12 -0
- package/dist/ui.js +659 -647
- package/dist/ui.js.map +1 -1
- package/package.json +8 -2
- package/.claude/launch.json +0 -17
- package/.oxfmtrc.json +0 -27
- package/.oxlintrc.json +0 -23
- package/.picto-fix-png/Cursor.png +0 -0
- package/.picto-fix-png/DocumentGradient.png +0 -0
- package/.picto-fix-png/FileGradient.png +0 -0
- package/.picto-fix-png/HealthBubbleGradient.png +0 -0
- package/.picto-fix-png/LearnGradient.png +0 -0
- package/.picto-fix-png/MediaFileGradient.png +0 -0
- package/.zed/settings.json +0 -36
- package/AGENTS.md +0 -111
- package/CLAUDE.md +0 -111
- package/REVIEW.md +0 -81
- package/doc/code-style.md +0 -264
- package/doc/release.md +0 -102
- package/doc/stacker-plan.md +0 -219
- package/release-notes.md +0 -39
- package/tests/_setup/e2e-helpers.tsx +0 -169
- package/tests/_setup/markdown-classes.ts +0 -3
- package/tests/_setup/portable-stories.ts +0 -33
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-continues-an-ordered-list-when-Enter-is-pressed-at-the-end-of-a-numbered-line-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-diagram-for-the-fenced-mermaid-block-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-horizontal-rule-as-a-separator-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-read-only-table-preview-when-the-table-block-is-inactive-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-shows-blockquote-text-from-the-sample-document-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-shows-fenced-TypeScript-sample-code-in-the-document-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-auto-continues-a-bullet-list-when-Enter-is-pressed-at-the-end-of-a-list-item-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-expands-triple-backtick-into-a-fenced-code-block-with-the-caret-on-the-empty-body-line-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-reflects-typed-characters-in-the-underlying-document-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/heading-fold.test.tsx/MarkdownEditor---heading-folds-viewer--folds-and-unfolds-a-heading-section-via-the-margin-control-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-clicking-a-callout-header-focuses-the-editor-and-parks-the-caret-inside-the-callout--REASONING--4--1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-keeps-list-bullets-consistent-after-hopping-the-caret-across-items-several-times-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-reveals-heading-marks-when-the-caret-enters-the-heading-line-and-hides-them-when-it-leaves-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-shows-the-right-callout-title-when-moving-focus-between-consecutive-callouts-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---preview-widgets-clicking-a-callout-header-focuses-the-editor-with-the-caret-in-that-callout-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---preview-widgets-keeps-list-bullets-consistent-after-hopping-the-caret-across-items-several-times-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---preview-widgets-shows-the-right-callout-title-when-moving-focus-between-consecutive-callouts-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-block-content.test.tsx/MarkdownEditor---block-JSX-components-clicking-the-edit-source-action-selects-the-component-source-range-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-block-content.test.tsx/MarkdownEditor---block-JSX-components-renders-block-components-via-JsxBlockWidget-with-an-edit-source-action-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-block-content.test.tsx/MarkdownEditor---inline-JSX-rendering-block-content-keeps-multiple-inline-JSX-widgets-on-the-same-line-side-by-side-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-highlight.test.tsx/MarkdownEditor---JSX-attribute-highlighting-does-not-let-the-nested-HTML-parser-mis-highlight-attributes-after-an-expression-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-inline-click.test.tsx/MarkdownEditor---inline-JSX-click-reveal-reveals-the-raw-source-when-an-inline-component-badge-is-clicked-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-inline-click.test.tsx/MarkdownEditor---inline-JSX-click-reveal-reveals-the-raw-source-when-an-inline-component-badge-is-clicked-2.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-inline-click.test.tsx/MarkdownEditor---inline-JSX-click-reveal-reveals-the-raw-source-when-an-inline-component-badge-is-clicked-3.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-selection.test.tsx/MarkdownEditor---JSX-selection-reveal-shows-JSX-widget-when-inactive-and-raw-source-when-caret-is-inside-the-tag-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/media-sizing.test.tsx/MarkdownEditor---media-sizing-images-and-videos-do-not-exceed-the-editor-content-width-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/media-sizing.test.tsx/MarkdownEditor---media-sizing-matches-MarkdownViewer-image-width-in-the-split-playground-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/modes.test.tsx/MarkdownEditor---modes-standalone-MarkdownViewer-is-not-a-CodeMirror-surface-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/modes.test.tsx/MarkdownEditor---modes-toggling-the-SegmentedControl-swaps-mode-without-remounting-the-editor-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-repeated-checkbox-toggles-do-not-duplicate-or-drift-task-markers-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-scroll-position-is-preserved-when-the-document-is-edited--chat-92584463--1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-task-checkbox-toggles-between-checked-and-unchecked-in-the-source-document-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-toggle-via-checkbox-preserves-task-marker-widgets-visible-from-the-previous-caret-position-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/slash-commands.test.tsx/MarkdownEditor---slash-commands-dismisses-on-Escape-and-leaves-the-slash-as-literal-text-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/slash-commands.test.tsx/MarkdownEditor---slash-commands-highlights-the-slash-and-shows-an-Enter-command-placeholder-until-text-is-typed-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/slash-commands.test.tsx/MarkdownEditor---slash-commands-opens-a-filtered--keyboard-navigable-menu-and-inserts-boilerplate-on-Enter-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/undo-redo.test.tsx/MarkdownEditor---undo-and-redo-restores-undone-document-edits-via-redo-1.png +0 -0
- package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +0 -153
- package/tests/e2e/MarkdownEditor/edits.test.tsx +0 -112
- package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +0 -45
- package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +0 -193
- package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +0 -73
- package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +0 -243
- package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +0 -69
- package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +0 -60
- package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +0 -54
- package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +0 -44
- package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +0 -68
- package/tests/e2e/MarkdownEditor/media-align.test.tsx +0 -58
- package/tests/e2e/MarkdownEditor/media-edit.test.tsx +0 -64
- package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +0 -124
- package/tests/e2e/MarkdownEditor/modes.test.tsx +0 -94
- package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +0 -50
- package/tests/e2e/MarkdownEditor/regressions.test.tsx +0 -223
- package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +0 -100
- package/tests/e2e/MarkdownEditor/table-click.test.tsx +0 -48
- package/tests/e2e/MarkdownEditor/table-controls.test.tsx +0 -57
- package/tests/e2e/MarkdownEditor/table-format.test.tsx +0 -42
- package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +0 -39
- package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-computed-font-size-on-first-heading-1.png +0 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-counts-for-shared-structural-classes-1.png +0 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-visible-text-between-preview-editor--blurred--and-MarkdownViewer-1.png +0 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/render.test.tsx/MarkdownViewer---render-exercises-shared-preview-class-names-without-mounting-CodeMirror-1.png +0 -0
- package/tests/e2e/MarkdownViewer/parity.test.tsx +0 -191
- package/tests/e2e/MarkdownViewer/render.test.tsx +0 -36
- package/tests/e2e/Stacker/stacker.test.tsx +0 -304
- package/tests/unit/Editor/helpers.test.ts +0 -42
- package/tests/unit/MarkdownConformance/error-isolation.test.tsx +0 -44
- package/tests/unit/MarkdownConformance/indentation.test.tsx +0 -30
- package/tests/unit/MarkdownConformance/inline-code-fuzz.test.tsx +0 -255
- package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +0 -57
- package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +0 -165
- package/tests/unit/MarkdownConformance/links.test.tsx +0 -95
- package/tests/unit/MarkdownConformance/render-helpers.tsx +0 -88
- package/tests/unit/MarkdownEditor/code-info.test.ts +0 -63
- package/tests/unit/MarkdownEditor/decorations.test.ts +0 -490
- package/tests/unit/MarkdownEditor/editor-ready.test.ts +0 -36
- package/tests/unit/MarkdownEditor/html-descriptors.test.ts +0 -94
- package/tests/unit/MarkdownEditor/indented-fence.test.ts +0 -103
- package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +0 -115
- package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +0 -92
- package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +0 -88
- package/tests/unit/MarkdownEditor/list-indent.test.ts +0 -95
- package/tests/unit/MarkdownEditor/slash-commands.test.ts +0 -213
- package/tests/unit/MarkdownEditor/table-format.test.ts +0 -83
- package/tests/unit/MarkdownEditor/table.test.ts +0 -119
- package/tests/unit/MarkdownEditor/triggers.test.ts +0 -244
- package/tests/unit/MarkdownEditor/widget-store.test.ts +0 -105
- package/tests/unit/MarkdownViewer/callout.test.tsx +0 -60
- package/tests/unit/MarkdownViewer/code-highlight.test.tsx +0 -61
- package/tests/unit/MarkdownViewer/code-title.test.tsx +0 -62
- package/tests/unit/MarkdownViewer/features.test.tsx +0 -164
- package/tests/unit/MarkdownViewer/headings.test.tsx +0 -40
- package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +0 -98
- package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +0 -234
- package/tests/unit/MarkdownViewer/jsx.test.tsx +0 -211
- package/tests/unit/MarkdownViewer/list-bullets.test.tsx +0 -49
- package/tests/unit/MarkdownViewer/list-code.test.tsx +0 -65
- package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +0 -143
- package/tests/unit/MarkdownViewer/read-time.test.ts +0 -21
- package/tests/unit/MarkdownViewer/renderers.test.tsx +0 -95
- package/tests/unit/MarkdownViewer/runnable.test.tsx +0 -69
- package/tests/unit/MarkdownViewer/ssr.test.tsx +0 -94
- package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +0 -74
- package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +0 -196
- package/tests/unit/MarkdownViewer/tabs.test.tsx +0 -116
- package/tests/unit/Stacker/drag.test.ts +0 -21
- package/tests/unit/Stacker/helpers.test.ts +0 -105
- package/tests/unit/Stacker/reducer.test.ts +0 -107
- package/tests/unit/Syntax/language.test.ts +0 -63
- package/tools/duplicate-vector-mappings.json +0 -143
- package/tools/glass-export-overrides.json +0 -8
- package/tools/icon-parser.ts +0 -78
- package/tools/integration-gradient-exports.json +0 -57
- package/tools/picto-compare.ts +0 -64
- package/tools/picto-merge-urls.ts +0 -43
- package/tools/picto-sync.ts +0 -630
- package/tools/picto-verify-gradients.ts +0 -86
- package/tools/vector-export-nodes.json +0 -977
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { EditorSelection, EditorState } from "@codemirror/state";
|
|
2
|
-
import { EditorView } from "@codemirror/view";
|
|
3
|
-
import { DEFAULT_SLASH_COMMANDS } from "@src/lib/markdown/editor/slash/commands";
|
|
4
|
-
import { slashDecorationsField } from "@src/lib/markdown/editor/slash/decorations";
|
|
5
|
-
import { getSlashSession, slashSessionField } from "@src/lib/markdown/editor/slash/field";
|
|
6
|
-
import { Callout } from "@src/lib/markdown/grammar/callout";
|
|
7
|
-
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
8
|
-
import { describe, expect, it } from "vitest";
|
|
9
|
-
|
|
10
|
-
function createView(doc: string, cursor = doc.length): EditorView {
|
|
11
|
-
const state = EditorState.create({
|
|
12
|
-
doc,
|
|
13
|
-
extensions: [
|
|
14
|
-
createMarkdownLanguage({ extensions: [Callout] }),
|
|
15
|
-
slashSessionField,
|
|
16
|
-
slashDecorationsField,
|
|
17
|
-
],
|
|
18
|
-
selection: EditorSelection.cursor(cursor),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const parent = document.createElement("div");
|
|
22
|
-
document.body.appendChild(parent);
|
|
23
|
-
|
|
24
|
-
return new EditorView({ state, parent });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Simulate typing `text` one character at a time as `input.type` events. */
|
|
28
|
-
function type(view: EditorView, text: string): void {
|
|
29
|
-
for (const char of text) {
|
|
30
|
-
const pos = view.state.selection.main.head;
|
|
31
|
-
view.dispatch({
|
|
32
|
-
changes: { from: pos, insert: char },
|
|
33
|
-
selection: EditorSelection.cursor(pos + 1),
|
|
34
|
-
userEvent: "input.type",
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function command(id: string) {
|
|
40
|
-
const found = DEFAULT_SLASH_COMMANDS.find((c) => c.id === id);
|
|
41
|
-
if (!found) throw new Error(`unknown command ${id}`);
|
|
42
|
-
return found;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
describe("slashSessionField", () => {
|
|
46
|
-
it("opens a session when typing / at the start of a line", () => {
|
|
47
|
-
const view = createView("", 0);
|
|
48
|
-
type(view, "/");
|
|
49
|
-
|
|
50
|
-
expect(getSlashSession(view.state)).toEqual({ from: 0, query: "" });
|
|
51
|
-
|
|
52
|
-
view.destroy();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("opens a session when typing / after whitespace", () => {
|
|
56
|
-
const view = createView("foo ", 4);
|
|
57
|
-
type(view, "/");
|
|
58
|
-
|
|
59
|
-
expect(getSlashSession(view.state)?.from).toBe(4);
|
|
60
|
-
|
|
61
|
-
view.destroy();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("does not open a session mid-word", () => {
|
|
65
|
-
const view = createView("foo", 3);
|
|
66
|
-
type(view, "/");
|
|
67
|
-
|
|
68
|
-
expect(getSlashSession(view.state)).toBeNull();
|
|
69
|
-
|
|
70
|
-
view.destroy();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("does not open a session inside a fenced code block", () => {
|
|
74
|
-
const view = createView("```\n\n```", 4);
|
|
75
|
-
type(view, "/");
|
|
76
|
-
|
|
77
|
-
expect(getSlashSession(view.state)).toBeNull();
|
|
78
|
-
|
|
79
|
-
view.destroy();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("tracks the query as the user continues typing", () => {
|
|
83
|
-
const view = createView("", 0);
|
|
84
|
-
type(view, "/head");
|
|
85
|
-
|
|
86
|
-
expect(getSlashSession(view.state)?.query).toBe("head");
|
|
87
|
-
|
|
88
|
-
view.destroy();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("keeps the session open across a single space but closes on two", () => {
|
|
92
|
-
const view = createView("", 0);
|
|
93
|
-
type(view, "/a ");
|
|
94
|
-
expect(getSlashSession(view.state)?.query).toBe("a ");
|
|
95
|
-
|
|
96
|
-
type(view, " ");
|
|
97
|
-
expect(getSlashSession(view.state)).toBeNull();
|
|
98
|
-
|
|
99
|
-
view.destroy();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("closes the session when the slash is deleted", () => {
|
|
103
|
-
const view = createView("", 0);
|
|
104
|
-
type(view, "/h");
|
|
105
|
-
const slashPos = getSlashSession(view.state)?.from ?? -1;
|
|
106
|
-
|
|
107
|
-
view.dispatch({
|
|
108
|
-
changes: { from: slashPos, to: slashPos + 1 },
|
|
109
|
-
userEvent: "delete",
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
expect(getSlashSession(view.state)).toBeNull();
|
|
113
|
-
|
|
114
|
-
view.destroy();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("closes the session when the caret moves before the slash", () => {
|
|
118
|
-
const view = createView("", 0);
|
|
119
|
-
type(view, "/h");
|
|
120
|
-
|
|
121
|
-
view.dispatch({ selection: EditorSelection.cursor(0) });
|
|
122
|
-
|
|
123
|
-
expect(getSlashSession(view.state)).toBeNull();
|
|
124
|
-
|
|
125
|
-
view.destroy();
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("DEFAULT_SLASH_COMMANDS", () => {
|
|
130
|
-
it("replaces the /query range with a heading prefix and positions the caret", () => {
|
|
131
|
-
const view = createView("", 0);
|
|
132
|
-
type(view, "/h1");
|
|
133
|
-
const session = getSlashSession(view.state);
|
|
134
|
-
expect(session).not.toBeNull();
|
|
135
|
-
|
|
136
|
-
command("heading-1").run(view, {
|
|
137
|
-
from: session?.from ?? 0,
|
|
138
|
-
to: view.state.selection.main.head,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
expect(view.state.doc.toString()).toBe("# ");
|
|
142
|
-
expect(view.state.selection.main.head).toBe(2);
|
|
143
|
-
// Replacing the slash text closes the session.
|
|
144
|
-
expect(getSlashSession(view.state)).toBeNull();
|
|
145
|
-
|
|
146
|
-
view.destroy();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("inserts a fenced code block with the caret on the empty middle line", () => {
|
|
150
|
-
const view = createView("", 0);
|
|
151
|
-
type(view, "/code");
|
|
152
|
-
|
|
153
|
-
command("code-block").run(view, {
|
|
154
|
-
from: 0,
|
|
155
|
-
to: view.state.selection.main.head,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
expect(view.state.doc.toString()).toBe("```\n\n```");
|
|
159
|
-
expect(view.state.selection.main.head).toBe(4);
|
|
160
|
-
|
|
161
|
-
view.destroy();
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
function collectDecorations(view: EditorView) {
|
|
166
|
-
const set = view.state.field(slashDecorationsField);
|
|
167
|
-
const marks: Array<{ from: number; to: number }> = [];
|
|
168
|
-
let widgets = 0;
|
|
169
|
-
|
|
170
|
-
set.between(0, view.state.doc.length, (from, to, deco) => {
|
|
171
|
-
if (deco.spec.widget) {
|
|
172
|
-
widgets += 1;
|
|
173
|
-
} else {
|
|
174
|
-
marks.push({ from, to });
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
return { marks, widgets };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
describe("slashDecorationsField", () => {
|
|
182
|
-
it("emits no decorations when there is no active session", () => {
|
|
183
|
-
const view = createView("hello", 5);
|
|
184
|
-
|
|
185
|
-
expect(collectDecorations(view)).toEqual({ marks: [], widgets: 0 });
|
|
186
|
-
|
|
187
|
-
view.destroy();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("highlights the lone slash and shows the placeholder when the query is empty", () => {
|
|
191
|
-
const view = createView("", 0);
|
|
192
|
-
type(view, "/");
|
|
193
|
-
|
|
194
|
-
const { marks, widgets } = collectDecorations(view);
|
|
195
|
-
// Mark wraps the slash and the trailing placeholder widget (0..2).
|
|
196
|
-
expect(marks).toEqual([{ from: 0, to: 2 }]);
|
|
197
|
-
expect(widgets).toBe(1);
|
|
198
|
-
|
|
199
|
-
view.destroy();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("extends the highlight over the query and drops the placeholder once text is typed", () => {
|
|
203
|
-
const view = createView("", 0);
|
|
204
|
-
type(view, "/head");
|
|
205
|
-
|
|
206
|
-
const { marks, widgets } = collectDecorations(view);
|
|
207
|
-
// Mark spans the slash + "head" (positions 0..5).
|
|
208
|
-
expect(marks).toEqual([{ from: 0, to: 5 }]);
|
|
209
|
-
expect(widgets).toBe(0);
|
|
210
|
-
|
|
211
|
-
view.destroy();
|
|
212
|
-
});
|
|
213
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { EditorSelection, EditorState } from "@codemirror/state";
|
|
2
|
-
import { markdownTableFormatter, tableFormatSpec } from "@src/lib/markdown/editor/table-format";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
|
|
5
|
-
function stateWith(doc: string, caret: number): EditorState {
|
|
6
|
-
return EditorState.create({
|
|
7
|
-
doc,
|
|
8
|
-
selection: EditorSelection.cursor(caret),
|
|
9
|
-
extensions: [markdownTableFormatter()],
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
describe("tableFormatSpec", () => {
|
|
14
|
-
it("aligns columns and pads cells with trailing spaces", () => {
|
|
15
|
-
const doc = "| a | b |\n| - | - |\n| longvalue | y |";
|
|
16
|
-
const state = stateWith(doc, doc.indexOf("longvalue"));
|
|
17
|
-
const spec = tableFormatSpec(state);
|
|
18
|
-
expect(spec).not.toBeNull();
|
|
19
|
-
const next = state.update(spec ?? {}).state;
|
|
20
|
-
expect(next.doc.toString()).toBe(
|
|
21
|
-
["| a | b |", "| --------- | --- |", "| longvalue | y |"].join("\n"),
|
|
22
|
-
);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("returns null for an already-formatted table", () => {
|
|
26
|
-
const doc = ["| a | b |", "| --------- | --- |", "| longvalue | y |"].join(
|
|
27
|
-
"\n",
|
|
28
|
-
);
|
|
29
|
-
const state = stateWith(doc, doc.indexOf("longvalue"));
|
|
30
|
-
expect(tableFormatSpec(state)).toBeNull();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("returns null when the caret is not in a table", () => {
|
|
34
|
-
const doc = "just a paragraph with a | pipe";
|
|
35
|
-
const state = stateWith(doc, 3);
|
|
36
|
-
expect(tableFormatSpec(state)).toBeNull();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("keeps the caret within the edited cell after formatting", () => {
|
|
40
|
-
const doc = "| a | b |\n| - | - |\n| longvalue | y |";
|
|
41
|
-
// Caret right after "long" inside the body cell.
|
|
42
|
-
const caret = doc.indexOf("longvalue") + 4;
|
|
43
|
-
const state = stateWith(doc, caret);
|
|
44
|
-
const spec = tableFormatSpec(state);
|
|
45
|
-
const next = state.update(spec ?? {}).state;
|
|
46
|
-
const formatted = next.doc.toString();
|
|
47
|
-
const cellStart = formatted.indexOf("longvalue");
|
|
48
|
-
expect(next.selection.main.head).toBe(cellStart + 4);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("markdownTableFormatter", () => {
|
|
53
|
-
it("reformats the table after an edit inside it", () => {
|
|
54
|
-
const doc = "| a | b |\n| - | - |\n| x | y |";
|
|
55
|
-
const state = EditorState.create({
|
|
56
|
-
doc,
|
|
57
|
-
selection: EditorSelection.cursor(doc.indexOf("x") + 1),
|
|
58
|
-
extensions: [markdownTableFormatter()],
|
|
59
|
-
});
|
|
60
|
-
// Type "tra" after "x" → "xtra", which widens column 1.
|
|
61
|
-
const tr = state.update({
|
|
62
|
-
changes: { from: doc.indexOf("x") + 1, insert: "tra" },
|
|
63
|
-
selection: EditorSelection.cursor(doc.indexOf("x") + 4),
|
|
64
|
-
});
|
|
65
|
-
const next = tr.state;
|
|
66
|
-
expect(next.doc.toString()).toContain("| xtra | y |");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("does not loop on its own formatting transaction", () => {
|
|
70
|
-
const doc = "| a | b |\n| - | - |\n| x | y |";
|
|
71
|
-
const state = EditorState.create({
|
|
72
|
-
doc,
|
|
73
|
-
selection: EditorSelection.cursor(doc.indexOf("x") + 1),
|
|
74
|
-
extensions: [markdownTableFormatter()],
|
|
75
|
-
});
|
|
76
|
-
const tr = state.update({
|
|
77
|
-
changes: { from: doc.indexOf("x") + 1, insert: "tra" },
|
|
78
|
-
});
|
|
79
|
-
// A single user edit produces a stable, idempotent result.
|
|
80
|
-
const again = tr.state.update({ selection: EditorSelection.cursor(0) });
|
|
81
|
-
expect(again.state.doc.toString()).toBe(tr.state.doc.toString());
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
gfmTableAddColumn,
|
|
3
|
-
gfmTableAddRow,
|
|
4
|
-
gfmTableRemoveColumn,
|
|
5
|
-
gfmTableRemoveRow,
|
|
6
|
-
locateGfmTableCells,
|
|
7
|
-
parseGfmTable,
|
|
8
|
-
serialiseGfmTable,
|
|
9
|
-
type TableModel,
|
|
10
|
-
} from "@src/lib/markdown/tree/gfm-table";
|
|
11
|
-
import { describe, expect, it } from "vitest";
|
|
12
|
-
|
|
13
|
-
describe("GFM table", () => {
|
|
14
|
-
it("parses simple GFM tables", () => {
|
|
15
|
-
const source = "| a | b |\n| - | - |\n| 1 | 2 |";
|
|
16
|
-
const model = parseGfmTable(source);
|
|
17
|
-
expect(model).not.toBeNull();
|
|
18
|
-
expect(model?.headers).toEqual(["a", "b"]);
|
|
19
|
-
expect(model?.rows).toEqual([["1", "2"]]);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("respects alignment markers", () => {
|
|
23
|
-
const source = "| a | b | c |\n| :- | :-: | -: |\n| 1 | 2 | 3 |";
|
|
24
|
-
const model = parseGfmTable(source);
|
|
25
|
-
expect(model?.alignments).toEqual(["left", "center", "right"]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("round trips a table preserving content", () => {
|
|
29
|
-
const source = "| Name | Score |\n| :--- | ----: |\n| Ari | 12 |\n| Nova | 8 |";
|
|
30
|
-
const model = parseGfmTable(source);
|
|
31
|
-
if (!model) throw new Error("expected parse");
|
|
32
|
-
const serialised = serialiseGfmTable(model);
|
|
33
|
-
|
|
34
|
-
const reparsed = parseGfmTable(serialised);
|
|
35
|
-
expect(reparsed).not.toBeNull();
|
|
36
|
-
expect(reparsed?.headers).toEqual(model.headers);
|
|
37
|
-
expect(reparsed?.alignments).toEqual(model.alignments);
|
|
38
|
-
expect(reparsed?.rows).toEqual(model.rows);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("returns null for non-table input", () => {
|
|
42
|
-
expect(parseGfmTable("just a paragraph")).toBeNull();
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("locateGfmTableCells", () => {
|
|
47
|
-
it("locates the content start offset of each cell", () => {
|
|
48
|
-
const source = "| a | b |\n| - | - |\n| 1 | 2 |";
|
|
49
|
-
const cells = locateGfmTableCells(source);
|
|
50
|
-
expect(cells).not.toBeNull();
|
|
51
|
-
// Header content starts: 'a' at 2, 'b' at 6.
|
|
52
|
-
expect(cells?.headerOffsets).toEqual([2, 6]);
|
|
53
|
-
expect(source[cells?.headerOffsets[0] ?? -1]).toBe("a");
|
|
54
|
-
expect(source[cells?.headerOffsets[1] ?? -1]).toBe("b");
|
|
55
|
-
// Body row content starts: '1' and '2'.
|
|
56
|
-
expect(source[cells?.rowOffsets[0]?.[0] ?? -1]).toBe("1");
|
|
57
|
-
expect(source[cells?.rowOffsets[0]?.[1] ?? -1]).toBe("2");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("handles ragged padding and missing outer pipes", () => {
|
|
61
|
-
const source = "Name | Score\n--- | ---\nAri | 12";
|
|
62
|
-
const cells = locateGfmTableCells(source);
|
|
63
|
-
expect(cells).not.toBeNull();
|
|
64
|
-
expect(source.slice(cells?.headerOffsets[0] ?? 0, (cells?.headerOffsets[0] ?? 0) + 4)).toBe(
|
|
65
|
-
"Name",
|
|
66
|
-
);
|
|
67
|
-
expect(source.slice(cells?.headerOffsets[1] ?? 0, (cells?.headerOffsets[1] ?? 0) + 5)).toBe(
|
|
68
|
-
"Score",
|
|
69
|
-
);
|
|
70
|
-
expect(source[cells?.rowOffsets[0]?.[1] ?? -1]).toBe("1");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("returns null for non-table input", () => {
|
|
74
|
-
expect(locateGfmTableCells("just a paragraph")).toBeNull();
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("GFM table edits", () => {
|
|
79
|
-
const base: TableModel = {
|
|
80
|
-
headers: ["a", "b"],
|
|
81
|
-
alignments: ["default", "default"],
|
|
82
|
-
rows: [["1", "2"]],
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
it("adds an empty trailing column", () => {
|
|
86
|
-
const next = gfmTableAddColumn(base);
|
|
87
|
-
expect(next.headers).toEqual(["a", "b", ""]);
|
|
88
|
-
expect(next.alignments).toHaveLength(3);
|
|
89
|
-
expect(next.rows).toEqual([["1", "2", ""]]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("removes the last column but keeps at least one", () => {
|
|
93
|
-
const next = gfmTableRemoveColumn(base);
|
|
94
|
-
expect(next.headers).toEqual(["a"]);
|
|
95
|
-
expect(next.rows).toEqual([["1"]]);
|
|
96
|
-
const single: TableModel = { headers: ["only"], alignments: ["default"], rows: [["x"]] };
|
|
97
|
-
expect(gfmTableRemoveColumn(single)).toEqual(single);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("adds an empty trailing row sized to the columns", () => {
|
|
101
|
-
const next = gfmTableAddRow(base);
|
|
102
|
-
expect(next.rows).toEqual([
|
|
103
|
-
["1", "2"],
|
|
104
|
-
["", ""],
|
|
105
|
-
]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("removes the last row and is a no-op with no rows", () => {
|
|
109
|
-
expect(gfmTableRemoveRow(base).rows).toEqual([]);
|
|
110
|
-
const empty: TableModel = { headers: ["a"], alignments: ["default"], rows: [] };
|
|
111
|
-
expect(gfmTableRemoveRow(empty)).toEqual(empty);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("round-trips edits through serialise/parse", () => {
|
|
115
|
-
const widened = serialiseGfmTable(gfmTableAddColumn(base));
|
|
116
|
-
const reparsed = parseGfmTable(widened);
|
|
117
|
-
expect(reparsed?.headers).toEqual(["a", "b", ""]);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { EditorSelection, EditorState } from "@codemirror/state";
|
|
2
|
-
import { EditorView, runScopeHandlers } from "@codemirror/view";
|
|
3
|
-
import { markdownPairKeymap, markdownSnippetHandler } from "@src/lib/markdown/editor/snippets";
|
|
4
|
-
import { Callout } from "@src/lib/markdown/grammar/callout";
|
|
5
|
-
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
6
|
-
import { describe, expect, it } from "vitest";
|
|
7
|
-
|
|
8
|
-
function createView(doc: string, cursor?: number | { anchor: number; head: number }): EditorView {
|
|
9
|
-
const state = EditorState.create({
|
|
10
|
-
doc,
|
|
11
|
-
extensions: [
|
|
12
|
-
createMarkdownLanguage({ extensions: [Callout] }),
|
|
13
|
-
markdownSnippetHandler,
|
|
14
|
-
markdownPairKeymap,
|
|
15
|
-
],
|
|
16
|
-
selection:
|
|
17
|
-
typeof cursor === "number"
|
|
18
|
-
? EditorSelection.cursor(cursor)
|
|
19
|
-
: cursor
|
|
20
|
-
? EditorSelection.range(cursor.anchor, cursor.head)
|
|
21
|
-
: undefined,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const parent = document.createElement("div");
|
|
25
|
-
document.body.appendChild(parent);
|
|
26
|
-
|
|
27
|
-
return new EditorView({ state, parent });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Dispatch a Backspace through the registered key handlers. */
|
|
31
|
-
function backspace(view: EditorView): boolean {
|
|
32
|
-
return runScopeHandlers(view, new KeyboardEvent("keydown", { key: "Backspace" }), "editor");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Simulate a single-character user input by walking the same input-handler
|
|
37
|
-
* pipeline the editor uses. Falls back to a default insert when no handler
|
|
38
|
-
* consumes the event so we exercise the trigger but otherwise behave like a
|
|
39
|
-
* normal keystroke.
|
|
40
|
-
*/
|
|
41
|
-
function type(view: EditorView, text: string): void {
|
|
42
|
-
const main = view.state.selection.main;
|
|
43
|
-
const handlers = view.state.facet(EditorView.inputHandler);
|
|
44
|
-
const buildTransaction = () => view.state.update(view.state.replaceSelection(text));
|
|
45
|
-
for (const handler of handlers) {
|
|
46
|
-
if (handler(view, main.from, main.to, text, buildTransaction)) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
view.dispatch(buildTransaction());
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe("markdownSnippetHandler", () => {
|
|
54
|
-
it("expands triple backtick into a fenced code block with caret on the empty content line", () => {
|
|
55
|
-
const view = createView("``", 2);
|
|
56
|
-
type(view, "`");
|
|
57
|
-
|
|
58
|
-
expect(view.state.doc.toString()).toBe("```\n\n```");
|
|
59
|
-
expect(view.state.selection.main.empty).toBe(true);
|
|
60
|
-
// Caret on the empty middle line, after "```\n".
|
|
61
|
-
expect(view.state.selection.main.from).toBe(4);
|
|
62
|
-
|
|
63
|
-
view.destroy();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("preserves leading indent when expanding a fence inside a nested context", () => {
|
|
67
|
-
const view = createView(" ``", 4);
|
|
68
|
-
type(view, "`");
|
|
69
|
-
|
|
70
|
-
expect(view.state.doc.toString()).toBe(" ```\n \n ```");
|
|
71
|
-
// Caret on the indented content line.
|
|
72
|
-
expect(view.state.selection.main.from).toBe(8);
|
|
73
|
-
|
|
74
|
-
view.destroy();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("expands a triple backtick typed one keystroke at a time", () => {
|
|
78
|
-
const view = createView("", 0);
|
|
79
|
-
type(view, "`");
|
|
80
|
-
type(view, "`");
|
|
81
|
-
type(view, "`");
|
|
82
|
-
|
|
83
|
-
expect(view.state.doc.toString()).toBe("```\n\n```");
|
|
84
|
-
expect(view.state.selection.main.from).toBe(4);
|
|
85
|
-
|
|
86
|
-
view.destroy();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("does not expand a fence from a single backtick after text", () => {
|
|
90
|
-
const view = createView("abc", 3);
|
|
91
|
-
type(view, "`");
|
|
92
|
-
|
|
93
|
-
// A lone backtick must auto-pair, never clobber the preceding text.
|
|
94
|
-
expect(view.state.doc.toString()).toBe("abc``");
|
|
95
|
-
expect(view.state.selection.main.from).toBe(4);
|
|
96
|
-
|
|
97
|
-
view.destroy();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("does not expand a fence when the backticks are not at the line start", () => {
|
|
101
|
-
const view = createView("x ``", 4);
|
|
102
|
-
type(view, "`");
|
|
103
|
-
|
|
104
|
-
expect(view.state.doc.toString()).toBe("x ```");
|
|
105
|
-
|
|
106
|
-
view.destroy();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("does not expand a fence when there is content after the cursor on the same line", () => {
|
|
110
|
-
const view = createView("``rest", 2);
|
|
111
|
-
type(view, "`");
|
|
112
|
-
|
|
113
|
-
expect(view.state.doc.toString()).toBe("```rest");
|
|
114
|
-
|
|
115
|
-
view.destroy();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("does not expand a fence when typing inside an existing fenced code block", () => {
|
|
119
|
-
const view = createView("```js\nlet x``\n```", 13);
|
|
120
|
-
type(view, "`");
|
|
121
|
-
|
|
122
|
-
expect(view.state.doc.toString()).toBe("```js\nlet x```\n```");
|
|
123
|
-
|
|
124
|
-
view.destroy();
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("wraps a non-empty selection with backticks", () => {
|
|
128
|
-
const view = createView("hello world", { anchor: 0, head: 5 });
|
|
129
|
-
type(view, "`");
|
|
130
|
-
|
|
131
|
-
expect(view.state.doc.toString()).toBe("`hello` world");
|
|
132
|
-
expect(view.state.selection.main.from).toBe(1);
|
|
133
|
-
expect(view.state.selection.main.to).toBe(6);
|
|
134
|
-
|
|
135
|
-
view.destroy();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("wraps a selection with asterisks for italic and again for bold", () => {
|
|
139
|
-
const view = createView("hello world", { anchor: 0, head: 5 });
|
|
140
|
-
type(view, "*");
|
|
141
|
-
expect(view.state.doc.toString()).toBe("*hello* world");
|
|
142
|
-
|
|
143
|
-
type(view, "*");
|
|
144
|
-
expect(view.state.doc.toString()).toBe("**hello** world");
|
|
145
|
-
|
|
146
|
-
view.destroy();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("wraps a selection with tildes for strikethrough", () => {
|
|
150
|
-
const view = createView("hello world", { anchor: 0, head: 5 });
|
|
151
|
-
type(view, "~");
|
|
152
|
-
type(view, "~");
|
|
153
|
-
|
|
154
|
-
expect(view.state.doc.toString()).toBe("~~hello~~ world");
|
|
155
|
-
|
|
156
|
-
view.destroy();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("wraps a selection as a markdown link with caret between the parens", () => {
|
|
160
|
-
const view = createView("hello world", { anchor: 0, head: 5 });
|
|
161
|
-
type(view, "[");
|
|
162
|
-
|
|
163
|
-
expect(view.state.doc.toString()).toBe("[hello]() world");
|
|
164
|
-
expect(view.state.selection.main.empty).toBe(true);
|
|
165
|
-
expect(view.state.selection.main.from).toBe(8);
|
|
166
|
-
|
|
167
|
-
view.destroy();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("does not wrap when the selection is inside a fenced code block", () => {
|
|
171
|
-
const view = createView("```js\nlet x = 1\n```", { anchor: 6, head: 11 });
|
|
172
|
-
type(view, "*");
|
|
173
|
-
|
|
174
|
-
const doc = view.state.doc.toString();
|
|
175
|
-
expect(doc).not.toContain("*let x *");
|
|
176
|
-
expect(doc).not.toContain("*let x ");
|
|
177
|
-
|
|
178
|
-
view.destroy();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("auto-pairs a lone backtick for inline code", () => {
|
|
182
|
-
const view = createView("run ", 4);
|
|
183
|
-
type(view, "`");
|
|
184
|
-
|
|
185
|
-
expect(view.state.doc.toString()).toBe("run ``");
|
|
186
|
-
expect(view.state.selection.main.from).toBe(5);
|
|
187
|
-
|
|
188
|
-
view.destroy();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("auto-pairs and steps over brackets", () => {
|
|
192
|
-
const view = createView("", 0);
|
|
193
|
-
type(view, "(");
|
|
194
|
-
expect(view.state.doc.toString()).toBe("()");
|
|
195
|
-
expect(view.state.selection.main.from).toBe(1);
|
|
196
|
-
|
|
197
|
-
type(view, ")");
|
|
198
|
-
expect(view.state.doc.toString()).toBe("()");
|
|
199
|
-
expect(view.state.selection.main.from).toBe(2);
|
|
200
|
-
|
|
201
|
-
view.destroy();
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("does not double a closing bracket that was not auto-paired", () => {
|
|
205
|
-
const view = createView("foo", 3);
|
|
206
|
-
type(view, ")");
|
|
207
|
-
|
|
208
|
-
expect(view.state.doc.toString()).toBe("foo)");
|
|
209
|
-
|
|
210
|
-
view.destroy();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("suppresses emphasis auto-pairing directly in front of a word", () => {
|
|
214
|
-
const view = createView("word", 0);
|
|
215
|
-
type(view, "*");
|
|
216
|
-
|
|
217
|
-
expect(view.state.doc.toString()).toBe("*word");
|
|
218
|
-
expect(view.state.selection.main.from).toBe(1);
|
|
219
|
-
|
|
220
|
-
view.destroy();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("deletes an empty auto-inserted pair with a single backspace", () => {
|
|
224
|
-
const view = createView("", 0);
|
|
225
|
-
type(view, "(");
|
|
226
|
-
expect(view.state.doc.toString()).toBe("()");
|
|
227
|
-
|
|
228
|
-
backspace(view);
|
|
229
|
-
expect(view.state.doc.toString()).toBe("");
|
|
230
|
-
expect(view.state.selection.main.from).toBe(0);
|
|
231
|
-
|
|
232
|
-
view.destroy();
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("leaves a non-empty pair to the default backspace", () => {
|
|
236
|
-
const view = createView("(x)", 2);
|
|
237
|
-
// The pair handler declines (no empty pair around the caret), so the
|
|
238
|
-
// default backspace binding is left to run.
|
|
239
|
-
expect(backspace(view)).toBe(false);
|
|
240
|
-
expect(view.state.doc.toString()).toBe("(x)");
|
|
241
|
-
|
|
242
|
-
view.destroy();
|
|
243
|
-
});
|
|
244
|
-
});
|