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")