From eb3bfb00fe5a269b57e7a58b30e72772f2192d32 Mon Sep 17 00:00:00 2001 From: elliot Date: Thu, 22 Jan 2026 16:49:50 -0500 Subject: [PATCH 1/4] Fix first nested checkbox removed when loading into visual editor --- .../editor/src/nodes/list/list-checked.ts | 81 +++++++++---------- packages/editor/src/nodes/list/list-pandoc.ts | 19 ++--- 2 files changed, 47 insertions(+), 53 deletions(-) diff --git a/packages/editor/src/nodes/list/list-checked.ts b/packages/editor/src/nodes/list/list-checked.ts index d9a2aff2..8c011711 100644 --- a/packages/editor/src/nodes/list/list-checked.ts +++ b/packages/editor/src/nodes/list/list-checked.ts @@ -20,7 +20,7 @@ import { findParentNodeOfType, NodeWithPos, setTextSelection } from 'prosemirror import { InputRule, wrappingInputRule } from 'prosemirror-inputrules'; import { ProsemirrorCommand, EditorCommandId } from '../../api/command'; -import { PandocToken, mapTokens } from '../../api/pandoc'; +import { PandocToken } from '../../api/pandoc'; // custom NodeView that accomodates display / interaction with item check boxes export class CheckedListItemNodeView implements NodeView { @@ -28,7 +28,7 @@ export class CheckedListItemNodeView implements NodeView { public readonly contentDOM: HTMLElement; constructor(node: ProsemirrorNode, view: EditorView, getPos: () => number) { - + // create root li element this.dom = window.document.createElement('li'); if (node.attrs.tight) { @@ -167,7 +167,7 @@ export function checkedListItemInputRule() { } export interface InputRuleWithHandler extends InputRule { - handler: (state: EditorState, match: RegExpMatchArray, start: number, end: number) => Transaction + handler: (state: EditorState, match: RegExpMatchArray, start: number, end: number) => Transaction; } // allow users to begin a new checked list by typing [x] or [ ] at the beginning of a line @@ -212,50 +212,43 @@ export function fragmentWithCheck(schema: Schema, fragment: Fragment, checked: b const kCheckedChar = '☒'; const kUncheckedChar = '☐'; -export function tokensWithChecked(tokens: PandocToken[]): { checked: null | boolean; tokens: PandocToken[] } { - // will set this flag based on inspecting the first Str token - let checked: null | boolean | undefined; - let lastWasChecked = false; - - // map tokens - const mappedTokens = mapTokens(tokens, tok => { - // if the last token was checked then strip the next space - if (tok.t === 'Space' && lastWasChecked) { - lastWasChecked = false; - return { - t: 'Str', - c: '', - }; - } - - // derive 'checked' from first chraracter of first Str token encountered - // if we find checked or unchecked then set the flag and strip off - // the first 2 chraracters (the check and the space after it) - else if (tok.t === 'Str' && checked === undefined) { - let text = tok.c as string; - if (text.charAt(0) === kCheckedChar) { - checked = true; - lastWasChecked = true; - text = text.slice(1); - } else if (text.charAt(0) === kUncheckedChar) { - checked = false; - lastWasChecked = true; - text = text.slice(1); - } else { - checked = null; - } - return { - t: 'Str', - c: text, - }; +/** + * example of `tokens`: + * ```json + * {t:"Para",c:[{t:"Str",c:"☒"},{t:"Space"},{t:"Str",c:"example"}]} + * ``` + * + * this function takes that and returns: + * + * ```json + * { + * checked: true, + * tokens: {t:"Para",c:[{t:"Str",c:""},{t:"Space"},{t:"Str",c:"example"}]}, + * } + * ``` + * + * notice that the `☒` character was removed from the first "Str" node. + */ +export function tokensWithChecked(tokens: PandocToken[]): { checked: null | boolean; tokens: PandocToken[]; } { + const checkNode = tokens[0]?.c?.[0]; + const checkChar = checkNode?.t === 'Str' ? checkNode.c.charAt(0) : ''; + const checked = checkChar === kCheckedChar ? + true : checkChar === kUncheckedChar + ? false : + null; + + const modifiedTokens = structuredClone(tokens); + // if checked, then strip the `☒` or `☐` from the first node and remove the following space (if there is one) + if (checked === true || checked === false) { + if (modifiedTokens[0].c[1].t === 'Space') { + modifiedTokens[0].c.splice(0, 2); // remove the first two tokens } else { - return tok; + modifiedTokens[0].c.splice(0, 1); // remove the first token } - }); + } - // return return { - checked: checked !== undefined ? checked : null, - tokens: mappedTokens, + checked, + tokens: modifiedTokens, }; } diff --git a/packages/editor/src/nodes/list/list-pandoc.ts b/packages/editor/src/nodes/list/list-pandoc.ts index 68b526e3..655c310c 100644 --- a/packages/editor/src/nodes/list/list-pandoc.ts +++ b/packages/editor/src/nodes/list/list-pandoc.ts @@ -86,24 +86,25 @@ export function readPandocList(nodeType: NodeType, capabilities: ListCapabilitie writer.logExampleList(); } } - + const children = getChildren(tok); - const attrs = { ...getAttrs(tok), - tight: children.length && children[0].length && children[0][0].t === 'Plain' - }; + const attrs = { + ...getAttrs(tok), + tight: children.length && children[0].length && children[0][0].t === 'Plain' + }; writer.openNode(nodeType, attrs); children.forEach((child: PandocToken[]) => { // setup tokens/attribs for output let tokens = child; - const childAttrs: { checked: null | boolean } = { checked: null }; + const childAttrs: { checked: null | boolean; } = { checked: null }; // special task list processing if the current format supports task lists if (capabilities.tasks) { // look for checkbox in first character of child tokens // if we see it, remove it and set childAttrs.checked as appropriate - const childWithChecked = tokensWithChecked(child); - childAttrs.checked = childWithChecked.checked; - tokens = childWithChecked.tokens; + const { tokens: modifiedTokens, checked } = tokensWithChecked(child); + childAttrs.checked = checked; + tokens = modifiedTokens; } // process children @@ -165,7 +166,7 @@ function listNodeOptions(node: ProsemirrorNode, capabilities: ListCapabilities): // (allow case of [paragraph,list] which is just a nested list) node.forEach(item => { if (options.tight && item.childCount > 1) { - if (item.childCount > 2 || !isList(item.child(1)) ) { + if (item.childCount > 2 || !isList(item.child(1))) { options.tight = false; } } From 6a29de0214f0a952ce925e97ea5ac29a2216bc24 Mon Sep 17 00:00:00 2001 From: elliot Date: Thu, 22 Jan 2026 16:50:05 -0500 Subject: [PATCH 2/4] Add nested checklist roundtrip test --- .../src/test/examples/nested-checked-list.qmd | 20 +++++++++++++++++++ apps/vscode/src/test/quartoDoc.test.ts | 8 ++++++++ 2 files changed, 28 insertions(+) create mode 100644 apps/vscode/src/test/examples/nested-checked-list.qmd diff --git a/apps/vscode/src/test/examples/nested-checked-list.qmd b/apps/vscode/src/test/examples/nested-checked-list.qmd new file mode 100644 index 00000000..01a6241a --- /dev/null +++ b/apps/vscode/src/test/examples/nested-checked-list.qmd @@ -0,0 +1,20 @@ +- + + - [x] hello + + - [x] hello + + - hello + +This is what you get from the Visual Editor when you create a nested list +in it: + +-
+ + - [x] hello + + - [x] hello + + - hello + +
diff --git a/apps/vscode/src/test/quartoDoc.test.ts b/apps/vscode/src/test/quartoDoc.test.ts index 9e0c804d..498d835d 100644 --- a/apps/vscode/src/test/quartoDoc.test.ts +++ b/apps/vscode/src/test/quartoDoc.test.ts @@ -55,6 +55,14 @@ suite("Quarto basics", function () { assert.equal(vscode.window.activeTextEditor, editor, 'quarto extension interferes with other files opened in VSCode!'); }); + + test("Roundtrip doesn't change nested-checked-list.qmd", async function () { + const { doc } = await openAndShowTextDocument("nested-checked-list.qmd"); + + const { before, after } = await roundtrip(doc); + + assert.equal(before, after); + }); }); /** From 4cddf7a3eac0c1eb682dc50b4d687c0792b6e108 Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 23 Jan 2026 14:45:39 -0500 Subject: [PATCH 3/4] Fix roundtrip test file --- apps/vscode/src/test/examples/nested-checked-list.qmd | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/vscode/src/test/examples/nested-checked-list.qmd b/apps/vscode/src/test/examples/nested-checked-list.qmd index 01a6241a..610e7d3e 100644 --- a/apps/vscode/src/test/examples/nested-checked-list.qmd +++ b/apps/vscode/src/test/examples/nested-checked-list.qmd @@ -1,4 +1,4 @@ -- +- - [x] hello @@ -6,8 +6,7 @@ - hello -This is what you get from the Visual Editor when you create a nested list -in it: +This is what you get from the Visual Editor when you create a nested list in it: -
@@ -17,4 +16,4 @@ in it: - hello -
+ \ No newline at end of file From 5606a7573f8112314d2c9832392b11dba82420ea Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 27 Jan 2026 16:23:54 -0500 Subject: [PATCH 4/4] Add changelog entry and tidy other entries --- apps/vscode/CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index b7feefb2..7fe9b73e 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -8,8 +8,9 @@ - Fixed cell formatting sometimes deleting code at the end of the cell (). - Removed visual/source toggle button from Positron editor action bar. You can still switch between editing modes using the command palette or context menu (). - Improved checkbox list item appearance in the Visual Editor (). -- Fix columns2 and columns3 snippets leaving `:::` in the document (). -- Add fragment snippet (). +- Fixed columns2 and columns3 snippets leaving `:::` in the document (). +- Added fragment snippet (). +- Fixed a bug where visual editor removed the first nested checkbox (). ## 1.128.0 (Release on 2026-01-08)