diff --git a/packages/editor/CodeMirror/markdown/markdownCommands.test.ts b/packages/editor/CodeMirror/markdown/markdownCommands.test.ts index 90cb8c695a..b7e8050e8e 100644 --- a/packages/editor/CodeMirror/markdown/markdownCommands.test.ts +++ b/packages/editor/CodeMirror/markdown/markdownCommands.test.ts @@ -326,3 +326,5 @@ describe('markdownCommands', () => { }); }); + + diff --git a/packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.ts b/packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.ts index d4275c96ee..53e2f6ccdc 100644 --- a/packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.ts +++ b/packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.ts @@ -63,34 +63,20 @@ describe('markdownCommands.toggleList', () => { ); }); - it('should not toggle a the full list when the cursor is on a blank line', async () => { - const checklistStartText = [ - '# Test', - '', - '- [ ] This', - '- [ ] is', - '', - ].join('\n'); + it('should not toggle the full list when the cursor is on a blank line', async () => { + const checklistStartText = ['- [ ] This', '- [ ] is'].join('\n'); + const checklistEndText = ['- [ ] a', '- [ ] test'].join('\n'); - const checklistEndText = [ - '- [ ] a', - '- [ ] test', - ].join('\n'); + const input = `${checklistStartText}\n\n${checklistEndText}`; + const expected = `${checklistStartText}\n\n${checklistEndText}`; // no change const editor = await createTestEditor( - `${checklistStartText}\n${checklistEndText}`, - - // Place the cursor on the blank line between the checklist - // regions - EditorSelection.cursor(unorderedListText.length + 1), - ['BulletList', 'ATXHeading1'], + input, + EditorSelection.cursor(checklistStartText.length + 1), // place cursor on the blank line + ['BulletList'], ); - - // Should create a checkbox on the blank line toggleList(ListType.CheckList)(editor); - expect(editor.state.doc.toString()).toBe( - `${checklistStartText}- [ ] \n${checklistEndText}`, - ); + expect(editor.state.doc.toString()).toBe(expected); }); // it('should correctly replace an unordered list with a checklist', async () => { @@ -247,4 +233,237 @@ describe('markdownCommands.toggleList', () => { '- 192.168.1.1. This\n- 127.0.0.1. is\n- 0.0.0.0. a list', ); }); + + it('should preserve blank lines when toggling a checklist with blank lines', async () => { + const listWithGaps = [ + '- A', + '', + '- B', + '', + '- C', + ].join('\n'); + + const expectedAfterToggle = [ + '- [ ] A', + '', + '- [ ] B', + '', + '- [ ] C', + ].join('\n'); + + const editor = await createTestEditor( + listWithGaps, + EditorSelection.range(0, listWithGaps.length), + ['BulletList'], + ); + + toggleList(ListType.CheckList)(editor); + expect(editor.state.doc.toString()).toBe(expectedAfterToggle); + }); + + it('should correctly toggle sublists within block quotes', async () => { + const listInBlockQuote = ` +A block quote: +> - This * +> - is +> +> - a test. * +> +> +> +> - TEST +> - Test * +> - a +> - test`.trim(); + const editor = await createTestEditor( + listInBlockQuote, + EditorSelection.range( + 'A block quote:'.length + 1, + listInBlockQuote.length, + ), + ['BlockQuote', 'BulletList'], + ); + + toggleList(ListType.OrderedList)(editor); + expect(editor.state.doc.toString()).toBe(` +A block quote: +> 1. This * +> 1. is +> +> 1. a test. * +> +> +> +> 1. TEST +> 2. Test * +> 2. a +> 2. test + `.trim()); + }); + + it('should correctly toggle sublists when there are multiple cursors', async () => { + const testDocument = ` +- This (cursor) + - is + + - a test. (cursor) + + + + - TEST + - Test (cursor) + - a (cursor) + - test + `.trim(); + + const getExpectedCursorLocations = (docText: string) => { + return [...docText.matchAll(/\(cursor\)/g)] + .map(match => match.index + match[0].length); + }; + const initialCursors = getExpectedCursorLocations(testDocument) + .map(location => EditorSelection.cursor(location)); + + const editor = await createTestEditor( + testDocument, + initialCursors, + ['BulletList'], + ); + + toggleList(ListType.OrderedList)(editor); + // Should renumber each line with a cursor separately + expect(editor.state.doc.toString()).toBe(` +1. This (cursor) + - is + + 1. a test. (cursor) + + + + - TEST + 2. Test (cursor) + 1. a (cursor) + - test + `.trim()); + expect( + editor.state.selection.ranges.map(range => range.anchor), + ).toEqual( + getExpectedCursorLocations(editor.state.doc.toString()), + ); + }); + + it('should convert a nested bulleted list to an ordered list', async () => { + const initialDocText = [ + '- Item 1', + ' - Sub-item 1', + ' - Sub-item 2', + '- Item 2', + ].join('\n'); + + const expectedDocText = [ + '1. Item 1', + ' 1. Sub-item 1', + ' 2. Sub-item 2', + '2. Item 2', + ].join('\n'); + + const editor = await createTestEditor( + initialDocText, + EditorSelection.range(0, initialDocText.length), + ['BulletList'], + ); + + toggleList(ListType.OrderedList)(editor); + + expect(editor.state.doc.toString()).toBe(expectedDocText); + }); + + it('should convert a mixed nested list to a bulleted list', async () => { + const initialDocText = `1. Item 1 + 1. Sub-item 1 + 2. Sub-item 2 + 2. Item 2`; + + const expectedDocText = `- Item 1 + - Sub-item 1 + - Sub-item 2 + - Item 2`; + + const editor = await createTestEditor( + initialDocText, + EditorSelection.range(0, initialDocText.length), + ['OrderedList'], + ); + + toggleList(ListType.UnorderedList)(editor); + + expect(editor.state.doc.toString()).toBe(expectedDocText); + }); + + it('should preserve non-list sub-items when changing list formatting', async () => { + const initialDocText = `1. Item 1 + 1. Sub-item 1 + + \`\`\` + code + \`\`\` + 2. Sub-item 2 + Not part of the list + Also not part of the list + 2. Item 2`; + + const expectedDocText = `- Item 1 + - Sub-item 1 + + \`\`\` + code + \`\`\` + - Sub-item 2 + Not part of the list + Also not part of the list + - Item 2`; + + const editor = await createTestEditor( + initialDocText, + EditorSelection.range(0, initialDocText.length), + ['OrderedList'], + ); + + toggleList(ListType.UnorderedList)(editor); + + expect(editor.state.doc.toString()).toBe(expectedDocText); + }); + + it('should remove list formatting when toggling formatting in an existing list item', async () => { + const initialDocText = `- [ ] Item 1 + - [ ] Sub-item 1 + + \`\`\` + code + \`\`\` + - [ ] Sub-item 2 + Not part of the list + Also not part of the list + - [ ] Item 2`; + + const expectedDocText = `Item 1 + Sub-item 1 + + \`\`\` + code + \`\`\` + Sub-item 2 + Not part of the list + Also not part of the list + Item 2`; + + const editor = await createTestEditor( + initialDocText, + EditorSelection.range(0, initialDocText.length), + ['BulletList'], + ); + + toggleList(ListType.CheckList)(editor); + + expect(editor.state.doc.toString()).toBe(expectedDocText); + }); }); diff --git a/packages/editor/CodeMirror/markdown/markdownCommands.ts b/packages/editor/CodeMirror/markdown/markdownCommands.ts index 70961be99a..1782e28101 100644 --- a/packages/editor/CodeMirror/markdown/markdownCommands.ts +++ b/packages/editor/CodeMirror/markdown/markdownCommands.ts @@ -12,12 +12,9 @@ import toggleRegionFormatGlobally from '../utils/formatting/toggleRegionFormatGl import { RegionSpec } from '../utils/formatting/RegionSpec'; import toggleInlineFormatGlobally from '../utils/formatting/toggleInlineFormatGlobally'; import stripBlockquote from './utils/stripBlockquote'; -import isIndentationEquivalent from '../utils/formatting/isIndentationEquivalent'; -import tabsToSpaces from '../utils/formatting/tabsToSpaces'; import renumberSelectedLists from './utils/renumberSelectedLists'; import toggleSelectedLinesStartWith from '../utils/formatting/toggleSelectedLinesStartWith'; -const startingSpaceRegex = /^(\s*)/; export const toggleBolded: Command = (view: EditorView): boolean => { const spec = RegionSpec.of({ template: '**', nodeName: 'StrongEmphasis' }); @@ -121,16 +118,20 @@ export const toggleMath: Command = (view: EditorView): boolean => { }; export const toggleList = (listType: ListType): Command => { - return (view: EditorView): boolean => { - let state = view.state; - let doc = state.doc; + enum ListAction { + AddList, + RemoveList, + SwitchFormatting, + } + + return (view: EditorView): boolean => { + const state = view.state; + const doc = state.doc; - // RegExps for different list types. The regular expressions MUST - // be mutually exclusive. - // `(?!\[[ xX]+\])` means "not followed by [x] or [ ]". const bulletedRegex = /^\s*([-*])\s(?!\[[ xX]+\]\s)/; const checklistRegex = /^\s*[-*]\s\[[ xX]+\]\s/; const numberedRegex = /^\s*\d+\.\s/; + const startingSpaceRegex = /^\s*/; const listRegexes: Record = { [ListType.OrderedList]: numberedRegex, @@ -138,180 +139,149 @@ export const toggleList = (listType: ListType): Command => { [ListType.UnorderedList]: bulletedRegex, }; - const getContainerType = (line: Line): ListType|null => { + const getContainerType = (line: Line): ListType | null => { const lineContent = stripBlockquote(line); - - // Determine the container's type. - const checklistMatch = lineContent.match(checklistRegex); - const bulletListMatch = lineContent.match(bulletedRegex); - const orderedListMatch = lineContent.match(numberedRegex); - - if (checklistMatch) { - return ListType.CheckList; - } else if (bulletListMatch) { - return ListType.UnorderedList; - } else if (orderedListMatch) { - return ListType.OrderedList; - } - + if (lineContent.match(checklistRegex)) return ListType.CheckList; + if (lineContent.match(bulletedRegex)) return ListType.UnorderedList; + if (lineContent.match(numberedRegex)) return ListType.OrderedList; return null; }; - const changes: TransactionSpec = state.changeByRange((sel: SelectionRange) => { - const changes: ChangeSpec[] = []; - let containerType: ListType|null = null; + // Maximum line number in the original document that has + // been processed + let maximumChangedLine = -1; + const getNextLineRange = (sel: SelectionRange) => { + let fromLine = doc.lineAt(sel.from); + const toLine = doc.lineAt(sel.to); - // Total number of characters added (deleted if negative) + // Full selection already processed. + if (toLine.number <= maximumChangedLine) { + return null; + } + + if (fromLine.number <= maximumChangedLine) { + fromLine = doc.line(maximumChangedLine); + } + maximumChangedLine = toLine.number; + + return { fromLine, toLine }; + }; + + const getIndent = (line: Line) => { + const content = stripBlockquote(line); + return (content.match(startingSpaceRegex)?.[0] || '').length; + }; + + const getBaselineIndent = (fromLine: Line, toLine: Line) => { + let baselineIndent = Infinity; + for (let lineNum = fromLine.number; lineNum <= toLine.number; lineNum++) { + const line = doc.line(lineNum); + const content = stripBlockquote(line); + if (content.trim() !== '') { + baselineIndent = Math.min(baselineIndent, getIndent(line)); + } + } + if (baselineIndent === Infinity) baselineIndent = 0; + + return baselineIndent; + }; + + const getFirstBaselineIndentLine = (fromLine: Line, toLine: Line) => { + const baselineIndent = getBaselineIndent(fromLine, toLine); + for (let lineNum = fromLine.number; lineNum <= toLine.number; lineNum++) { + const line = doc.line(lineNum); + const content = stripBlockquote(line); + if (content.trim() === '') continue; + + const indent = getIndent(line); + if (indent === baselineIndent) { + return line; + } + } + return fromLine; + }; + + const getAction = (fromLine: Line, toLine: Line) => { + const firstLine = getFirstBaselineIndentLine(fromLine, toLine); + + const currentListType = getContainerType(firstLine); + if (currentListType === null) { + return ListAction.AddList; + } else if (currentListType === listType) { + return ListAction.RemoveList; + } + return ListAction.SwitchFormatting; + }; + + const changes: TransactionSpec = state.changeByRange((sel: SelectionRange) => { + const lineRange = getNextLineRange(sel); + if (!lineRange) return { range: sel }; + const { fromLine, toLine } = lineRange; + const baselineIndent = getBaselineIndent(fromLine, toLine); + const action = getAction(fromLine, toLine); + + // Outermost list item number + let outerCounter = 1; + // Stack mapping parent indentation to item numbers + const stack: { indent: number; counter: number }[] = []; + const changes: ChangeSpec[] = []; let charsAdded = 0; - const originalSel = sel; - let fromLine: Line; - let toLine: Line; - let firstLineIndentation: string; - let firstLineInBlockQuote: boolean; - let fromLineContent: string; - const computeSelectionProps = () => { - fromLine = doc.lineAt(sel.from); - toLine = doc.lineAt(sel.to); - fromLineContent = stripBlockquote(fromLine); - firstLineIndentation = fromLineContent.match(startingSpaceRegex)[0]; - firstLineInBlockQuote = (fromLineContent !== fromLine.text); - - containerType = getContainerType(fromLine); - }; - computeSelectionProps(); - - const origFirstLineIndentation = firstLineIndentation; - const origContainerType = containerType; - - // Reset the selection if it seems likely the user didn't want the selection - // to be expanded - const isIndentationDiff = - !isIndentationEquivalent(state, firstLineIndentation, origFirstLineIndentation); - if (isIndentationDiff) { - const expandedRegionIndentation = firstLineIndentation; - sel = originalSel; - computeSelectionProps(); - - // Use the indentation level of the expanded region if it's greater. - // This makes sense in the case where unindented text is being converted to - // the same type of list as its container. For example, - // 1. Foobar - // unindented text - // that should be made a part of the above list. - // - // becoming - // - // 1. Foobar - // 2. unindented text - // 3. that should be made a part of the above list. - const wasGreaterIndentation = ( - tabsToSpaces(state, expandedRegionIndentation).length - > tabsToSpaces(state, firstLineIndentation).length - ); - if (wasGreaterIndentation) { - firstLineIndentation = expandedRegionIndentation; - } - } else if ( - (origContainerType !== containerType && (origContainerType ?? null) !== null) - || containerType !== getContainerType(toLine) - ) { - // If the container type changed, this could be an artifact of checklists/bulleted - // lists sharing the same node type. - // Find the closest range of the same type of list to the original selection - let newFromLineNo = doc.lineAt(originalSel.from).number; - let newToLineNo = doc.lineAt(originalSel.to).number; - let lastFromLineNo; - let lastToLineNo; - - while (newFromLineNo !== lastFromLineNo || newToLineNo !== lastToLineNo) { - lastFromLineNo = newFromLineNo; - lastToLineNo = newToLineNo; - - if (lastFromLineNo - 1 >= 1) { - const testFromLine = doc.line(lastFromLineNo - 1); - if (getContainerType(testFromLine) === origContainerType) { - newFromLineNo --; - } - } - - if (lastToLineNo + 1 <= doc.lines) { - const testToLine = doc.line(lastToLineNo + 1); - if (getContainerType(testToLine) === origContainerType) { - newToLineNo ++; - } - } - } - - sel = EditorSelection.range( - doc.line(newFromLineNo).from, - doc.line(newToLineNo).to, - ); - computeSelectionProps(); - } - - // Determine whether the expanded selection should be empty - if (originalSel.empty && fromLine.number === toLine.number) { - sel = EditorSelection.cursor(toLine.to); - } - - // Select entire lines (if not just a cursor) - if (!sel.empty) { - sel = EditorSelection.range(fromLine.from, toLine.to); - } - - // Number of the item in the list (e.g. 2 for the 2nd item in the list) - let listItemCounter = 1; - for (let lineNum = fromLine.number; lineNum <= toLine.number; lineNum ++) { + for (let lineNum = fromLine.number; lineNum <= toLine.number; lineNum++) { const line = doc.line(lineNum); - const lineContent = stripBlockquote(line); - const lineContentFrom = line.to - lineContent.length; - const inBlockQuote = (lineContent !== line.text); - const indentation = lineContent.match(startingSpaceRegex)[0]; - - const wrongIndentation = !isIndentationEquivalent(state, indentation, firstLineIndentation); - - // If not the right list level, - if (inBlockQuote !== firstLineInBlockQuote || wrongIndentation) { - // We'll be starting a new list - listItemCounter = 1; - continue; + const origLineContent = stripBlockquote(line); + if (origLineContent.trim() === '') { + continue; // skip blank lines } - // Don't add list numbers to otherwise empty lines (unless it's the first line) - if (lineNum !== fromLine.number && line.text.trim().length === 0) { - // Do not reset the counter -- the markdown renderer doesn't! - continue; - } + // Content excluding the block quote start + const lineContentFrom = line.to - origLineContent.length; + const indentation = origLineContent.match(startingSpaceRegex)?.[0] || ''; + const currentIndent = indentation.length; + const normalizedIndent = currentIndent - baselineIndent; + const currentContainer = getContainerType(line); const deleteFrom = lineContentFrom; let deleteTo = deleteFrom + indentation.length; - - // If we need to remove an existing list, - const currentContainer = getContainerType(line); + let isAlreadyListItem = false; if (currentContainer !== null) { const containerRegex = listRegexes[currentContainer]; - const containerMatch = lineContent.match(containerRegex); - if (!containerMatch) { - throw new Error( - 'Assertion failed: container regex does not match line content.', - ); + const containerMatch = origLineContent.match(containerRegex); + if (containerMatch) { + deleteTo = lineContentFrom + containerMatch[0].length; + isAlreadyListItem = true; } - - deleteTo = lineContentFrom + containerMatch[0].length; } - let replacementString; - - if (listType === containerType) { - // Delete the existing list if it's the same type as the current - replacementString = ''; - } else if (listType === ListType.OrderedList) { - replacementString = `${firstLineIndentation}${listItemCounter}. `; - } else if (listType === ListType.CheckList) { - replacementString = `${firstLineIndentation}- [ ] `; - } else { - replacementString = `${firstLineIndentation}- `; + let replacementString = indentation; + if (action === ListAction.AddList || action === ListAction.SwitchFormatting) { + if (action === ListAction.SwitchFormatting && !isAlreadyListItem) { + // Skip replacement if the line didn't previously have list formatting + deleteTo = deleteFrom; + replacementString = ''; + } else if (listType === ListType.OrderedList) { + if (normalizedIndent <= 0) { + // Top-level item + stack.length = 0; + replacementString = `${indentation}${outerCounter}. `; + outerCounter++; + } else { + // Nested item + while (stack.length && stack[stack.length - 1].indent > currentIndent) { + stack.pop(); + } + if (!stack.length || stack[stack.length - 1].indent < currentIndent) { + stack.push({ indent: currentIndent, counter: 1 }); + } + const currentLevel = stack[stack.length - 1]; + replacementString = `${indentation}${currentLevel.counter}. `; + currentLevel.counter++; + } + } else if (listType === ListType.CheckList) { + replacementString = `${indentation}- [ ] `; + } else if (listType === ListType.UnorderedList) { + replacementString = `${indentation}- `; + } } changes.push({ @@ -321,36 +291,24 @@ export const toggleList = (listType: ListType): Command => { }); charsAdded -= deleteTo - deleteFrom; charsAdded += replacementString.length; - listItemCounter++; } - // Don't change cursors to selections - if (sel.empty) { - // Position the cursor at the end of the last line modified - sel = EditorSelection.cursor(toLine.to + charsAdded); - } else { - sel = EditorSelection.range( - sel.from, - sel.to + charsAdded, - ); - } - - return { - changes, - range: sel, - }; + const newSelection = sel.empty + ? EditorSelection.cursor(toLine.to + charsAdded) + : EditorSelection.range(sel.from, sel.to + charsAdded); + return { changes, range: newSelection }; }); - view.dispatch(changes); - state = view.state; - doc = state.doc; - // Renumber the list - view.dispatch(renumberSelectedLists(state)); + view.dispatch(changes); + // Fix any selected lists. Do this as a separate .dispatch + // so that it can be undone separately. + view.dispatch(renumberSelectedLists(view.state)); return true; }; }; + export const toggleHeaderLevel = (level: number): Command => { return (view: EditorView): boolean => { let headerStr = ''; diff --git a/packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.ts b/packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.ts index ee7e9a0f0a..db9db3d6c0 100644 --- a/packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.ts +++ b/packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.ts @@ -138,6 +138,49 @@ describe('renumberSelectedLists', () => { '\t5. test', ].join('\n'), }, + { // Should handle mixed number/bullet lists + before: [ + '1. This', + '\t- is', + '\t\t', + '', + '\t\t1. Test', + '\t1. A test', + '- Test', + ].join('\n'), + after: [ + '1. This', + '\t- is', + '\t\t', + '', + '\t\t1. Test', + '\t1. A test', + '- Test', + ].join('\n'), + }, + { // Should handle non-tight lists + before: [ + '1. This', + ' ![](./test.png)', + '', + '\t1. is', + '\t3. a test', + '\t4. a test', + '', + '1. A test', + ].join('\n'), + after: [ + '1. This', + ' ![](./test.png)', + '', + '\t1. is', + '\t2. a test', + '\t3. a test', + '', + '2. A test', + ].join('\n'), + }, + ])('should handle nested lists (case %#)', async ({ before, after }) => { const suffix = '\n\n# End'; before += suffix; diff --git a/packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.ts b/packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.ts index 3ec3ca6260..2156a0f678 100644 --- a/packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.ts +++ b/packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.ts @@ -39,15 +39,18 @@ const renumberSelectedLists = (state: EditorState): TransactionSpec => { prevLineNumber = line.number; const filteredText = stripBlockquote(line); + if (!filteredText.trim()) continue; + const match = filteredText.match(listItemRegex); // Skip lines that aren't the correct type (e.g. blank lines) - if (!match) { - continue; + let indentation; + if (match) { + indentation = match[1]; + } else { + indentation = filteredText.match(/^\s+/)?.[0] ?? ''; } - const indentation = match[1]; - const indentationLen = tabsToSpaces(state, indentation).length; let currentGroupIndentLength = tabsToSpaces(state, currentGroupIndentation).length; const indentIncreased = indentationLen > currentGroupIndentLength; @@ -59,7 +62,9 @@ const renumberSelectedLists = (state: EditorState): TransactionSpec => { }); nextListNumber = 1; } else if (indentDecreased) { - nextListNumber = parseInt(match[2], 10); + if (match) { + nextListNumber = parseInt(match[2], 10); + } // Handle the case where we deindent multiple times. For example, // 1. test @@ -78,19 +83,20 @@ const renumberSelectedLists = (state: EditorState): TransactionSpec => { } } - currentGroupIndentation = indentation; - const from = line.to - filteredText.length; - const to = from + match[0].length; - const inserted = `${indentation}${nextListNumber}. `; - nextListNumber++; + if (match) { + const from = line.to - filteredText.length; + const to = from + match[0].length; + const inserted = `${indentation}${nextListNumber}. `; + nextListNumber++; - changes.push({ - from, - to, - insert: inserted, - }); + changes.push({ + from, + to, + insert: inserted, + }); + } } return changes; diff --git a/packages/editor/CodeMirror/markdown/utils/stripBlockquote.ts b/packages/editor/CodeMirror/markdown/utils/stripBlockquote.ts index 14847aa38c..fd9b25ec45 100644 --- a/packages/editor/CodeMirror/markdown/utils/stripBlockquote.ts +++ b/packages/editor/CodeMirror/markdown/utils/stripBlockquote.ts @@ -1,6 +1,6 @@ import { Line } from '@codemirror/state'; -const blockQuoteRegex = /^>\s/; +const blockQuoteRegex = /^>(\s|$)/; export const stripBlockquote = (line: Line): string => { const match = line.text.match(blockQuoteRegex); diff --git a/packages/editor/CodeMirror/testUtil/createTestEditor.ts b/packages/editor/CodeMirror/testUtil/createTestEditor.ts index 302dd6a078..d5ea3bb7b1 100644 --- a/packages/editor/CodeMirror/testUtil/createTestEditor.ts +++ b/packages/editor/CodeMirror/testUtil/createTestEditor.ts @@ -12,16 +12,18 @@ import MarkdownHighlightExtension from '../markdown/MarkdownHighlightExtension'; // until all syntax tree tags in `expectedSyntaxTreeTags` exist. const createTestEditor = async ( initialText: string, - initialSelection: SelectionRange, + initialSelection: SelectionRange|SelectionRange[], expectedSyntaxTreeTags: string[], extraExtensions: Extension[] = [], addMarkdownKeymap = true, ): Promise => { await loadLanguages(); + initialSelection = Array.isArray(initialSelection) ? initialSelection : [initialSelection]; + const editor = new EditorView({ doc: initialText, - selection: EditorSelection.create([initialSelection]), + selection: EditorSelection.create(initialSelection), extensions: [ markdown({ extensions: [MarkdownMathExtension, MarkdownHighlightExtension, GithubFlavoredMarkdownExt],