From 319861792365fa3db92824079d0810b27d47f01b Mon Sep 17 00:00:00 2001 From: dstoc <539597+dstoc@users.noreply.github.com> Date: Sat, 16 Nov 2024 17:14:15 +1100 Subject: [PATCH] fix: Cursor movement in/out of transclusions * If the transcluded content was not at the end of its document then errors in findFinalEditable prevented some movements. * Transclusions did not handle -0 focus offset (start of last line). --- src/copy-paste-util.ts | 3 +-- src/markdown/transclusion.ts | 7 +++--- src/markdown/view-model-util.ts | 12 ++++++++--- tests/edit_test.ts | 38 +++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/copy-paste-util.ts b/src/copy-paste-util.ts index 2dc63c4e..bad4f91f 100644 --- a/src/copy-paste-util.ts +++ b/src/copy-paste-util.ts @@ -14,7 +14,6 @@ import { cloneNode, compareDocumentOrder, dfs, - findFinalEditable, performLogicalInsertion, removeDescendantNodes, } from './markdown/view-model-util.js'; @@ -30,7 +29,7 @@ export function insertMarkdown(markdown: string, node: ViewModelNode) { const newInlineNodes = newNodes .flatMap((node) => [...dfs(node, node)]) .filter(isInlineViewModelNode); - const newFocus = findFinalEditable(newNodes[0]); + const newFocus = newInlineNodes.at(-1); performLogicalInsertion(node, newNodes); return {newFocus, newInlineNodes}; } diff --git a/src/markdown/transclusion.ts b/src/markdown/transclusion.ts index c1ddb384..c0c6f014 100644 --- a/src/markdown/transclusion.ts +++ b/src/markdown/transclusion.ts @@ -85,10 +85,11 @@ export class MarkdownTransclusion extends LitElement implements SigpropHost { if (!this.hostContext) return; if (!this.root) return; if (this.hostContext.focusNode !== this.node) return; + const offset = this.hostContext.focusOffset ?? -1; const node = - (this.hostContext.focusOffset ?? -1) >= 0 - ? findNextEditable(this.root, this.root, true) - : findFinalEditable(this.root, true); + offset < 0 || Object.is(offset, -0) + ? findFinalEditable(this.root, this.root, true) + : findNextEditable(this.root, this.root, true); this.markdownRenderer.hostContext.focusNode = node ?? undefined; this.markdownRenderer.hostContext.focusOffset = this.hostContext.focusOffset; diff --git a/src/markdown/view-model-util.ts b/src/markdown/view-model-util.ts index 17cc6008..c14956f0 100644 --- a/src/markdown/view-model-util.ts +++ b/src/markdown/view-model-util.ts @@ -18,7 +18,9 @@ import { isInlineViewModelNode, type InlineViewModelNode, type MaybeViewModelNode, - type ViewModelNode, viewModel} from './view-model-node.js'; + type ViewModelNode, + viewModel, +} from './view-model-node.js'; import {MarkdownNode} from './node.js'; export function swapNodes(node1: ViewModelNode, node2: ViewModelNode) { @@ -160,11 +162,12 @@ export function findNextEditable( // TODO: why doesn't this return Editable? export function findFinalEditable( node: ViewModelNode, + root: ViewModelNode, include = false, ): InlineViewModelNode | null { let result: InlineViewModelNode | null = null; if (include && isInlineViewModelNode(node)) result = node; - for (const next of dfs(node)) { + for (const next of dfs(node, root)) { if (isInlineViewModelNode(next)) result = next; } return result; @@ -175,7 +178,10 @@ export function findNextDfs( root: ViewModelNode, predicate: (node: ViewModelNode) => node is T, ) { - for (const next of dfs(node, root[viewModel].parent)) { + for (const next of dfs( + node, + root[viewModel].nextSibling ?? root[viewModel].parent, + )) { if (next !== node && predicate(next)) return next; } return null; diff --git a/tests/edit_test.ts b/tests/edit_test.ts index e9ab006f..0d7c3bf1 100644 --- a/tests/edit_test.ts +++ b/tests/edit_test.ts @@ -90,6 +90,44 @@ test.describe('editing', () => { `); }); + test('follow caret movements in and out', async ({page}) => { + await page.keyboard.type('before'); + await page.keyboard.press('Enter'); + await page.keyboard.type('after'); + await page.keyboard.press('Home'); + await page.keyboard.press('ArrowUp'); + await importFile('transclusion.md', '# transclusion\naaa'); + await state.main.runCommand('insert transclusion', 'transclusion'); + await state.main.host + .locator('md-transclusion') + .waitFor({state: 'visible'}); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.type('1'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.type('2'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.type('3'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.type('4'); + expect(await exportMarkdown('test')).toMatchPretty(` + # test + bef4ore + + \`\`\`tc + transclusion + \`\`\` + + a2fter + + `); + expect(await exportMarkdown('transclusion')).toMatchPretty(` + # transclusion + 1a3aa + + `); + }); test('can be inserted and deleted', async ({page}) => { await page.keyboard.type('test'); await importFile('transclusion.md', '# transclusion\n');