Show full documents in Editor
Remove heading-based Markdown pagination so source and preview operate on the complete file. Add support for keyboard undo/redo ops. Give plain text documents the same preview, formatting, save, and file-action toolbar as Markdown, with regression coverage for both behaviors.
Alessandro committed
Jul 17, 2026 at 03:42 UTC
dbdb77b05760122f36c5d7cd05446a54b7bbded9
5 files changed
+132
-167
plugins/_editor/AGENTS.md
+1
@@ -20,6 +20,7 @@
20
- Keep Editor Open wired through the File Browser text picker so users can open one or more Markdown or plain text files with an obvious confirmation action.
21
- Keep Download in the Editor file-actions menu and save dirty text before downloading it.
22
- Keep Save As distinct from Rename: Save As writes the current editor text to a chosen `.md` or `.txt` path and retargets the active session without removing the original file.
23
+- Keep Markdown and plain text on the same toolbar, with full-document source and preview modes plus shared Undo/Redo buttons and keyboard shortcuts.
24
- Preserve source chat context ids when opening Markdown files from tool-result canvas handoffs.
25
26
## Work Guidance
plugins/_editor/webui/editor-panel.html
+20
-25
@@ -8,7 +8,7 @@
8
<div x-data>
9
<template x-if="$store.editor">
10
<div class="editor-panel" x-create="$store.editor.onMount($el, xAttrs($el) || {})" x-destroy="$store.editor.cleanup()">
11
- <div class="editor-shell" @keydown="$store.editor.handleEditorKeydown($event)">
11
+ <div class="editor-shell" @keydown.capture="$store.editor.handleEditorKeydown($event)">
12
<div class="editor-tabs" x-show="$store.editor.visibleTabs().length > 0" style="display: none;" role="tablist" aria-label="Open text files">
13
<template x-for="tab in $store.editor.visibleTabs()" :key="tab.tab_id">
14
<div
@@ -82,7 +82,7 @@
82
<button
83
type="button"
84
class="editor-icon-button editor-mode-toggle"
85
- x-show="$store.editor.isMarkdown()"
85
+ x-show="$store.editor.isTextDocument()"
86
style="display: none;"
87
:title="$store.editor.viewModeTitle()"
88
:aria-label="$store.editor.viewModeTitle()"
@@ -91,44 +91,40 @@
91
<span class="material-symbols-outlined" aria-hidden="true" x-text="$store.editor.viewModeIcon()"></span>
92
</button>
93
94
- <div class="editor-tool-group editor-source-tools" x-show="$store.editor.isTextDocument() && $store.editor.isSourceMode()" style="display: none;">
95
- <button type="button" class="editor-icon-button" title="Undo" aria-label="Undo" :disabled="!$store.editor.canUndo()" @click="$store.editor.undo()">
94
+ <div class="editor-tool-group editor-history-tools" x-show="$store.editor.isTextDocument()" style="display: none;">
95
+ <button type="button" class="editor-icon-button" title="Undo" aria-label="Undo" :disabled="$store.editor.previewEditing || !$store.editor.canUndo()" @click="$store.editor.undo()">
96
<span class="material-symbols-outlined">undo</span>
97
</button>
98
- <button type="button" class="editor-icon-button" title="Redo" aria-label="Redo" :disabled="!$store.editor.canRedo()" @click="$store.editor.redo()">
98
+ <button type="button" class="editor-icon-button" title="Redo" aria-label="Redo" :disabled="$store.editor.previewEditing || !$store.editor.canRedo()" @click="$store.editor.redo()">
99
<span class="material-symbols-outlined">redo</span>
100
</button>
101
- <button type="button" class="editor-icon-button" title="Bold" aria-label="Bold" x-show="$store.editor.isMarkdown()" @click="$store.editor.format('bold')">
101
+ </div>
102
+
103
+ <div class="editor-tool-group editor-source-tools" x-show="$store.editor.isTextDocument() && $store.editor.isSourceMode()" style="display: none;">
104
+ <button type="button" class="editor-icon-button" title="Bold" aria-label="Bold" @click="$store.editor.format('bold')">
105
<span class="material-symbols-outlined">format_bold</span>
106
</button>
104
- <button type="button" class="editor-icon-button" title="Italic" aria-label="Italic" x-show="$store.editor.isMarkdown()" @click="$store.editor.format('italic')">
107
+ <button type="button" class="editor-icon-button" title="Italic" aria-label="Italic" @click="$store.editor.format('italic')">
108
<span class="material-symbols-outlined">format_italic</span>
109
</button>
107
- <button type="button" class="editor-icon-button" title="List" aria-label="List" x-show="$store.editor.isMarkdown()" @click="$store.editor.format('list')">
110
+ <button type="button" class="editor-icon-button" title="List" aria-label="List" @click="$store.editor.format('list')">
111
<span class="material-symbols-outlined">format_list_bulleted</span>
112
</button>
110
- <button type="button" class="editor-icon-button" title="Numbered list" aria-label="Numbered list" x-show="$store.editor.isMarkdown()" @click="$store.editor.format('numbered')">
113
+ <button type="button" class="editor-icon-button" title="Numbered list" aria-label="Numbered list" @click="$store.editor.format('numbered')">
114
<span class="material-symbols-outlined">format_list_numbered</span>
115
</button>
113
- <button type="button" class="editor-icon-button" title="Table" aria-label="Table" x-show="$store.editor.isMarkdown()" @click="$store.editor.format('table')">
116
+ <button type="button" class="editor-icon-button" title="Table" aria-label="Table" @click="$store.editor.format('table')">
117
<span class="material-symbols-outlined">table</span>
118
</button>
119
</div>
117
- <div class="editor-tool-group editor-preview-tools" x-show="$store.editor.isMarkdown() && $store.editor.isPreviewMode()" style="display: none;">
118
- <button type="button" class="editor-icon-button" title="Previous page" aria-label="Previous page" :disabled="$store.editor.previewEditing || $store.editor.activePageIndex <= 0" @click="$store.editor.previousPage()">
119
- <span class="material-symbols-outlined">chevron_left</span>
120
- </button>
121
- <span class="editor-page-count" x-text="$store.editor.pagePositionLabel()"></span>
122
- <button type="button" class="editor-icon-button" title="Next page" aria-label="Next page" :disabled="$store.editor.previewEditing || $store.editor.activePageIndex >= $store.editor.pages().length - 1" @click="$store.editor.nextPage()">
123
- <span class="material-symbols-outlined">chevron_right</span>
124
- </button>
125
- <button type="button" class="editor-icon-button" title="Edit page" aria-label="Edit page" x-show="!$store.editor.previewEditing" @click="$store.editor.startPreviewEdit()">
120
+ <div class="editor-tool-group editor-preview-tools" x-show="$store.editor.isTextDocument() && $store.editor.isPreviewMode()" style="display: none;">
121
+ <button type="button" class="editor-icon-button" title="Edit document" aria-label="Edit document" x-show="!$store.editor.previewEditing" @click="$store.editor.startPreviewEdit()">
122
<span class="material-symbols-outlined">edit_note</span>
123
</button>
128
- <button type="button" class="editor-icon-button is-primary" title="Apply page edit" aria-label="Apply page edit" x-show="$store.editor.previewEditing" @click="$store.editor.applyPreviewEdit()">
124
+ <button type="button" class="editor-icon-button is-primary" title="Apply document edit" aria-label="Apply document edit" x-show="$store.editor.previewEditing" @click="$store.editor.applyPreviewEdit()">
125
<span class="material-symbols-outlined">check</span>
126
</button>
131
- <button type="button" class="editor-icon-button" title="Cancel page edit" aria-label="Cancel page edit" x-show="$store.editor.previewEditing" @click="$store.editor.cancelPreviewEdit()">
127
+ <button type="button" class="editor-icon-button" title="Cancel document edit" aria-label="Cancel document edit" x-show="$store.editor.previewEditing" @click="$store.editor.cancelPreviewEdit()">
128
<span class="material-symbols-outlined">close</span>
129
</button>
130
<button type="button" class="editor-icon-button" title="Search" aria-label="Search" :disabled="$store.editor.previewEditing" @click="$store.editor.openSearch()">
@@ -241,7 +237,7 @@
237
</div>
238
</div>
239
244
- <div class="editor-preview-shell" x-show="$store.editor.session && $store.editor.isMarkdown() && $store.editor.isPreviewMode()" style="display: none;">
240
+ <div class="editor-preview-shell" x-show="$store.editor.session && $store.editor.isTextDocument() && $store.editor.isPreviewMode()" style="display: none;">
241
<div class="editor-preview-title">
242
<h1 x-text="$store.editor.pageTitle()"></h1>
243
</div>
@@ -249,7 +245,7 @@
245
<textarea
246
class="editor-preview-page-editor"
247
data-editor-preview-source
252
- aria-label="Markdown page source"
248
+ aria-label="Document source"
249
x-model="$store.editor.previewEditText"
250
@input="$store.editor.onPreviewEditInput()"
251
@keydown.meta.enter.prevent="$store.editor.applyPreviewEdit()"
@@ -263,7 +259,7 @@
259
data-editor-preview
260
x-show="!$store.editor.previewEditing"
261
x-html="$store.editor.previewHtml()"
266
- x-effect="$store.editor.activePageIndex; $store.editor.editorText; $store.editor.searchQuery; $store.editor.searchIndex; $store.editor.schedulePreviewEnhance()"
262
+ x-effect="$store.editor.editorText; $store.editor.searchQuery; $store.editor.searchIndex; $store.editor.schedulePreviewEnhance()"
263
@click="$store.editor.handlePreviewClick($event)"
264
></div>
265
</div>
@@ -597,7 +593,6 @@
593
min-width: 8px;
594
}
595
600
- .editor-page-count,
596
.editor-search-count {
597
min-width: 54px;
598
color: var(--color-text-secondary);
plugins/_editor/webui/editor-preview.js
+9
-85
@@ -1,6 +1,5 @@
1
import { renderSafeMarkdown } from "/js/safe-markdown.js";
2
3
-const PAGE_HEADING_RE = /^(#{1,2})\s+(.+?)\s*#*\s*$/;
3
const FOOTNOTE_DEF_RE = /^\[\^([^\]]+)\]:\s*(.*)$/;
4
5
export function renderEditorPreviewMarkdown(markdown = "", fullMarkdown = markdown) {
@@ -13,68 +12,15 @@ export function renderEditorPreviewMarkdown(markdown = "", fullMarkdown = markdo
12
13
export function buildMarkdownPages(markdown = "", fallbackTitle = "Markdown") {
14
const source = String(markdown || "");
16
- const lines = source.split("\n");
17
- const pages = [];
18
- let current = null;
19
- let intro = [];
20
- let introStart = 0;
21
- let fenced = false;
22
- let offset = 0;
23
-
24
- const startPage = (title, level, line, start) => {
25
- if (current) {
26
- current.end = start;
27
- pages.push(finalizePage(current, pages.length, source));
28
- }
29
- current = {
30
- title: cleanHeadingText(title) || fallbackTitle,
31
- level,
32
- lines: [line],
33
- start,
34
- end: source.length,
35
- };
36
- };
37
-
38
- for (const [index, line] of lines.entries()) {
39
- const lineStart = offset;
40
- const lineEnd = lineStart + line.length + (index < lines.length - 1 ? 1 : 0);
41
- if (/^\s*```/.test(line)) fenced = !fenced;
42
- const match = !fenced ? line.match(PAGE_HEADING_RE) : null;
43
- if (match) {
44
- if (!current && intro.join("\n").trim()) {
45
- pages.push(finalizePage({
46
- title: fallbackTitle,
47
- level: 0,
48
- lines: intro,
49
- start: introStart,
50
- end: lineStart,
51
- }, pages.length, source));
52
- intro = [];
53
- }
54
- startPage(match[2], match[1].length, line, lineStart);
55
- offset = lineEnd;
56
- continue;
57
- }
58
- if (current) current.lines.push(line);
59
- else intro.push(line);
60
- offset = lineEnd;
61
- }
62
-
63
- if (current) {
64
- current.end = source.length;
65
- pages.push(finalizePage(current, pages.length, source));
66
- }
67
- else if (intro.join("\n").trim() || !pages.length) {
68
- pages.push(finalizePage({
69
- title: fallbackTitle,
70
- level: 0,
71
- lines: intro,
72
- start: introStart,
73
- end: source.length,
74
- }, pages.length, source));
75
- }
76
-
77
- return pages;
15
+ return [{
16
+ index: 0,
17
+ title: fallbackTitle,
18
+ level: 0,
19
+ anchor: slugifyHeading(fallbackTitle),
20
+ start: 0,
21
+ end: source.length,
22
+ markdown: source,
23
+ }];
24
}
25
26
export function slugifyHeading(text = "", used = new Map()) {
@@ -119,28 +65,6 @@ export function isMarkdownPath(path = "") {
65
return /\.md(?:own)?$/i.test(String(path || "").split(/[?#]/, 1)[0]);
66
}
67
122
-function finalizePage(page, index, source = "") {
123
- const start = Math.max(0, Number(page.start || 0));
124
- const end = Math.max(start, Number(page.end ?? String(source || "").length));
125
- const markdown = String(source || "").slice(start, end) || page.lines.join("\n");
126
- return {
127
- index,
128
- title: page.title,
129
- level: page.level,
130
- anchor: slugifyHeading(page.title),
131
- start,
132
- end,
133
- markdown,
134
- };
135
-}
136
-
137
-function cleanHeadingText(text = "") {
138
- return String(text || "")
139
- .replace(/\\([\\`*_[\]{}()#+.!-])/g, "$1")
140
- .replace(/[*_`~]/g, "")
141
- .trim();
142
-}
143
-
68
function prepareFootnotes(markdown = "", fullMarkdown = markdown) {
69
const definitions = [];
70
const body = [];
plugins/_editor/webui/editor-store.js
+42
-54
@@ -251,6 +251,7 @@ const model = {
251
_inputTimer: null,
252
_history: [],
253
_historyIndex: -1,
254
+ _historyPushedAt: 0,
255
_pendingFocus: false,
256
_pendingFocusEnd: true,
257
_focusAttempts: 0,
@@ -336,7 +337,7 @@ const model = {
337
},
338
339
async setViewMode(mode) {
339
- const next = mode === PREVIEW_MODE && this.isMarkdown() ? PREVIEW_MODE : SOURCE_MODE;
340
+ const next = mode === PREVIEW_MODE && this.isTextDocument() ? PREVIEW_MODE : SOURCE_MODE;
341
if (this.viewMode === next) return;
342
this.applyPreviewEdit({ silent: true });
343
this.syncEditorText();
@@ -354,7 +355,7 @@ const model = {
355
},
356
357
async toggleViewMode() {
357
- if (!this.isMarkdown()) return;
358
+ if (!this.isTextDocument()) return;
359
await this.setViewMode(this.isPreviewMode() ? SOURCE_MODE : PREVIEW_MODE);
360
},
361
@@ -367,7 +368,7 @@ const model = {
368
},
369
370
pages() {
370
- if (!this.isMarkdown()) return [];
371
+ if (!this.isTextDocument()) return [];
372
return buildMarkdownPages(this.editorText, this.tabTitle(this.session || {}));
373
},
374
@@ -381,35 +382,13 @@ const model = {
382
return this.currentPage().title || this.tabTitle(this.session || {});
383
},
384
384
- pagePositionLabel() {
385
- const pages = this.pages();
386
- if (!pages.length) return "";
387
- return `${Math.min(this.activePageIndex + 1, pages.length)} of ${pages.length}`;
388
- },
389
-
385
previewHtml() {
391
- if (!this.isMarkdown()) return "";
386
+ if (!this.isTextDocument()) return "";
387
return renderEditorPreviewMarkdown(this.currentPage().markdown || "", this.editorText);
388
},
389
395
- selectPage(index) {
396
- if (this.previewEditing) return;
397
- const pages = this.pages();
398
- if (!pages.length) return;
399
- this.activePageIndex = Math.max(0, Math.min(Number(index) || 0, pages.length - 1));
400
- this.schedulePreviewEnhance();
401
- },
402
-
403
- nextPage() {
404
- this.selectPage(this.activePageIndex + 1);
405
- },
406
-
407
- previousPage() {
408
- this.selectPage(this.activePageIndex - 1);
409
- },
410
-
390
startPreviewEdit() {
412
- if (!this.session || !this.isMarkdown() || !this.isPreviewMode()) return;
391
+ if (!this.session || !this.isTextDocument() || !this.isPreviewMode()) return;
392
const page = this.currentPage();
393
this.previewEditing = true;
394
this.previewEditDirty = false;
@@ -457,7 +436,7 @@ const model = {
436
this.previewEditPageIndex = -1;
437
438
return this.replacePageMarkdown(page, replacement, {
460
- message: "Page updated",
439
+ message: "Document updated",
440
silent: options.silent,
441
});
442
},
@@ -493,7 +472,7 @@ const model = {
472
},
473
474
togglePreviewTask(taskIndex, checked) {
496
- if (!this.session || !this.isMarkdown() || !this.isPreviewMode() || this.previewEditing) return false;
475
+ if (!this.session || !this.isTextDocument() || !this.isPreviewMode() || this.previewEditing) return false;
476
const page = this.currentPage();
477
const lines = String(page.markdown || "").split("\n");
478
const indexes = taskLineIndexes(page.markdown || "");
@@ -514,7 +493,7 @@ const model = {
493
},
494
495
schedulePreviewEnhance() {
517
- if (!this.isMarkdown() || !this.isPreviewMode()) return;
496
+ if (!this.isTextDocument() || !this.isPreviewMode()) return;
497
if (this._previewEnhanceTimer) globalThis.clearTimeout(this._previewEnhanceTimer);
498
this._previewEnhanceTimer = globalThis.setTimeout(() => {
499
this._previewEnhanceTimer = null;
@@ -707,7 +686,7 @@ const model = {
686
},
687
688
openSearch() {
710
- if (!this.isMarkdown()) return;
689
+ if (!this.isTextDocument()) return;
690
if (!this.isPreviewMode()) {
691
this.setViewMode(PREVIEW_MODE);
692
}
@@ -870,8 +849,19 @@ const model = {
849
},
850
851
handleEditorKeydown(event) {
873
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "f") {
874
- if (!this.isMarkdown()) return;
852
+ if (!(event.metaKey || event.ctrlKey) || !this.isTextDocument()) return;
853
+ const key = event.key.toLowerCase();
854
+ const historyAction = key === "y" || (key === "z" && event.shiftKey) ? "redo" : key === "z" ? "undo" : "";
855
+ const nativeEditing = event.target?.matches?.("input, textarea, [contenteditable='true']")
856
+ && !event.target.closest?.("[data-editor-ace], [data-editor-source]");
857
+ if (nativeEditing) return;
858
+ if (historyAction && !this.previewEditing) {
859
+ event.preventDefault();
860
+ event.stopPropagation();
861
+ this[historyAction]();
862
+ return;
863
+ }
864
+ if (key === "f") {
865
event.preventDefault();
866
this.openSearch();
867
}
@@ -982,11 +972,6 @@ const model = {
972
this.activeTabId = tab?.tab_id || "";
973
this.editorText = String(tab?.text || "");
974
this.dirty = Boolean(tab?.dirty);
985
- if (!this.isMarkdown(tab) && this.isPreviewMode()) {
986
- this.viewMode = SOURCE_MODE;
987
- this.searchOpen = false;
988
- this.searchQuery = "";
989
- }
975
if (this.previewEditing) this.cancelPreviewEdit();
976
if (!options.preservePage) {
977
this.activePageIndex = 0;
@@ -1500,7 +1485,6 @@ const model = {
1485
const wasActive = this.activeTabId === (previous?.tab_id || next.tab_id);
1486
if (wasActive) {
1487
this.session = next;
1503
- if (!this.isMarkdown(next) && this.isPreviewMode()) this.viewMode = SOURCE_MODE;
1488
this.updateSourceEditorMode(next);
1489
}
1490
const index = this.tabs.findIndex((tab) => tab.tab_id === (previous?.tab_id || next.tab_id));
@@ -1519,37 +1503,41 @@ const model = {
1503
resetHistory(text) {
1504
this._history = [String(text || "")];
1505
this._historyIndex = 0;
1506
+ this._historyPushedAt = 0;
1507
},
1508
1524
- pushHistory(text) {
1509
+ pushHistory(text, coalesce = false) {
1510
const value = String(text || "");
1511
if (this._history[this._historyIndex] === value) return;
1512
+ const now = Date.now();
1513
+ if (
1514
+ coalesce
1515
+ && this._historyPushedAt
1516
+ && now - this._historyPushedAt <= INPUT_PUSH_DELAY_MS
1517
+ && this._historyIndex === this._history.length - 1
1518
+ && this._historyIndex > 0
1519
+ ) {
1520
+ this._history[this._historyIndex] = value;
1521
+ this._historyPushedAt = now;
1522
+ return;
1523
+ }
1524
this._history = this._history.slice(0, this._historyIndex + 1);
1525
this._history.push(value);
1526
if (this._history.length > MAX_HISTORY) this._history.shift();
1527
this._historyIndex = this._history.length - 1;
1528
+ this._historyPushedAt = coalesce ? now : 0;
1529
},
1530
1531
undo() {
1534
- if (this.sourceEditor && this.isSourceMode()) {
1535
- this.sourceEditor.undo();
1536
- this.editorText = this.sourceEditor.getValue();
1537
- this.syncEditorText();
1538
- return;
1539
- }
1532
if (this._historyIndex <= 0) return;
1533
+ this._historyPushedAt = 0;
1534
this._historyIndex -= 1;
1535
this.applyEditorText(this._history[this._historyIndex], true);
1536
},
1537
1538
redo() {
1546
- if (this.sourceEditor && this.isSourceMode()) {
1547
- this.sourceEditor.redo();
1548
- this.editorText = this.sourceEditor.getValue();
1549
- this.syncEditorText();
1550
- return;
1551
- }
1539
if (this._historyIndex >= this._history.length - 1) return;
1540
+ this._historyPushedAt = 0;
1541
this._historyIndex += 1;
1542
this.applyEditorText(this._history[this._historyIndex], true);
1543
},
@@ -1580,7 +1568,7 @@ const model = {
1568
1569
onSourceInput() {
1570
this.markDirty();
1583
- this.pushHistory(this.editorText);
1571
+ this.pushHistory(this.editorText, true);
1572
this.scheduleInputPush();
1573
},
1574
@@ -1613,7 +1601,7 @@ const model = {
1601
},
1602
1603
format(command) {
1616
- if (!this.session || !this.isMarkdown()) return;
1604
+ if (!this.session || !this.isTextDocument()) return;
1605
if (this.sourceEditor && this.isSourceMode()) {
1606
const selected = this.sourceEditor.getSelectedText();
1607
const replacement = this.formatReplacement(command, selected);
tests/test_office_canvas_setup.py
+60
-3
@@ -156,6 +156,8 @@ def test_right_canvas_uses_desktop_surface_id_and_migrates_legacy_office_state()
156
assert 'input[type="checkbox"]' in editor_store
157
assert "renderEditorPreviewMarkdown" in editor_store
158
assert "buildMarkdownPages" in editor_store
159
+ assert "PAGE_HEADING_RE" not in editor_preview
160
+ assert "markdown: source" in editor_preview
161
assert "hydrateActiveSession" in editor_store
162
assert "refreshSourceEditorLayout" in editor_store
163
assert "editor.resize?.(true)" in editor_store
@@ -170,7 +172,6 @@ def test_right_canvas_uses_desktop_surface_id_and_migrates_legacy_office_state()
172
assert "renderSafeMarkdown" in editor_preview
173
assert "prepareFootnotes" in editor_preview
174
assert "resolveDocumentRelativePath" in editor_preview
173
- assert "slice(start, end)" in editor_preview
175
assert "allowDataImages: true" in editor_preview
176
assert "allowLatex: true" in editor_preview
177
assert "html = sanitizeHtml(html, options);" in safe_markdown
@@ -564,6 +565,7 @@ def test_editor_toolbar_places_preview_toggle_left_and_save_on_right():
565
toolbar = editor_panel[toolbar_start:toolbar_end]
566
567
mode_toggle = toolbar.index("editor-mode-toggle")
568
+ history_tools = toolbar.index("editor-history-tools")
569
source_tools = toolbar.index("editor-source-tools")
570
preview_tools = toolbar.index("editor-preview-tools")
571
spacer = toolbar.index("editor-toolbar-spacer")
@@ -572,8 +574,8 @@ def test_editor_toolbar_places_preview_toggle_left_and_save_on_right():
574
file_actions = toolbar.index("editor-file-actions")
575
file_menu = toolbar.index("editor-file-menu")
576
575
- assert mode_toggle < source_tools
576
- assert mode_toggle < preview_tools
577
+ assert mode_toggle < history_tools < source_tools
578
+ assert history_tools < preview_tools
579
assert spacer < save_button < save_as_button < file_actions < file_menu
580
assert "@click=\"$store.editor.save()\"" in toolbar
581
assert "@click=\"$store.editor.saveAs()\"" in toolbar
@@ -589,6 +591,61 @@ def test_editor_toolbar_places_preview_toggle_left_and_save_on_right():
591
assert "<span>Close File</span>" in file_menu_markup
592
593
594
+def test_editor_uses_full_document_preview_and_matching_markdown_text_tools():
595
+ editor_panel = read("plugins", "_editor", "webui", "editor-panel.html")
596
+ editor_store = read("plugins", "_editor", "webui", "editor-store.js")
597
+
598
+ assert "Previous page" not in editor_panel
599
+ assert "Next page" not in editor_panel
600
+ assert "editor-page-count" not in editor_panel
601
+ assert "pagePositionLabel" not in editor_store
602
+ assert "nextPage()" not in editor_store
603
+ assert "previousPage()" not in editor_store
604
+ assert 'x-show="$store.editor.isTextDocument()"' in editor_panel
605
+ assert '$store.editor.isTextDocument() && $store.editor.isPreviewMode()' in editor_panel
606
+ assert 'mode === PREVIEW_MODE && this.isTextDocument()' in editor_store
607
+ assert "if (!this.session || !this.isTextDocument()) return;" in editor_store
608
+
609
+ source_tools_start = editor_panel.index('class="editor-tool-group editor-source-tools"')
610
+ preview_tools_start = editor_panel.index('class="editor-tool-group editor-preview-tools"')
611
+ source_tools = editor_panel[source_tools_start:preview_tools_start]
612
+ for action in ("Bold", "Italic", "List", "Numbered list", "Table"):
613
+ assert f'title="{action}"' in source_tools
614
+ assert "isMarkdown()" not in source_tools
615
+
616
+
617
+def test_editor_history_shortcuts_and_toolbar_controls_cover_markdown_and_text():
618
+ editor_panel = read("plugins", "_editor", "webui", "editor-panel.html")
619
+ editor_store = read("plugins", "_editor", "webui", "editor-store.js")
620
+
621
+ assert '@keydown.capture="$store.editor.handleEditorKeydown($event)"' in editor_panel
622
+ history_start = editor_panel.index('class="editor-tool-group editor-history-tools"')
623
+ source_start = editor_panel.index('class="editor-tool-group editor-source-tools"')
624
+ history_tools = editor_panel[history_start:source_start]
625
+ assert 'x-show="$store.editor.isTextDocument()"' in history_tools
626
+ assert 'title="Undo"' in history_tools
627
+ assert 'title="Redo"' in history_tools
628
+ assert "$store.editor.previewEditing || !$store.editor.canUndo()" in history_tools
629
+ assert "$store.editor.previewEditing || !$store.editor.canRedo()" in history_tools
630
+
631
+ keydown_start = editor_store.index("handleEditorKeydown(event)")
632
+ create_start = editor_store.index("\n async create", keydown_start)
633
+ keydown = editor_store[keydown_start:create_start]
634
+ assert 'key === "z"' in keydown
635
+ assert 'key === "y"' in keydown
636
+ assert "event.stopPropagation()" in keydown
637
+ assert "this[historyAction]()" in keydown
638
+ assert "nativeEditing" in keydown
639
+
640
+ undo_start = editor_store.index("\n undo()")
641
+ can_undo_start = editor_store.index("\n canUndo()", undo_start)
642
+ history_actions = editor_store[undo_start:can_undo_start]
643
+ assert "sourceEditor.undo" not in history_actions
644
+ assert "sourceEditor.redo" not in history_actions
645
+ assert "pushHistory(text, coalesce = false)" in editor_store
646
+ assert "this.pushHistory(this.editorText, true)" in editor_store
647
+
648
+
649
def test_desktop_text_open_with_routes_to_editor_surface():
650
desktop_session = read("plugins", "_desktop", "helpers", "desktop_session.py")
651
desktop_store = read("plugins", "_desktop", "webui", "desktop-store.js")