mirror of https://github.com/laurent22/joplin.git
pull/9498/head^2
parent
6d6f9b3c3b
commit
a730d349e4
|
@ -7,7 +7,7 @@ import {
|
|||
import { classHighlighter } from '@lezer/highlight';
|
||||
|
||||
import {
|
||||
EditorView, drawSelection, highlightSpecialChars, ViewUpdate, Command,
|
||||
EditorView, drawSelection, highlightSpecialChars, ViewUpdate, Command, rectangularSelection,
|
||||
} from '@codemirror/view';
|
||||
import { history, undoDepth, redoDepth, standardKeymap } from '@codemirror/commands';
|
||||
|
||||
|
@ -203,7 +203,13 @@ const createEditor = (
|
|||
};
|
||||
},
|
||||
} : undefined),
|
||||
|
||||
// Allows multiple selections and allows selecting a rectangle
|
||||
// with ctrl (as in CodeMirror 5)
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
rectangularSelection(),
|
||||
drawSelection(),
|
||||
|
||||
highlightSpecialChars(),
|
||||
indentOnInput(),
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from '@codemirror/state';
|
||||
import { getIndentUnit, indentString, syntaxTree } from '@codemirror/language';
|
||||
import {
|
||||
RegionSpec, growSelectionToNode, renumberList,
|
||||
RegionSpec, growSelectionToNode, renumberSelectedLists,
|
||||
toggleInlineFormatGlobally, toggleRegionFormatGlobally, toggleSelectedLinesStartWith,
|
||||
isIndentationEquivalent, stripBlockquote, tabsToSpaces,
|
||||
} from './markdownReformatter';
|
||||
|
@ -354,9 +354,7 @@ export const toggleList = (listType: ListType): Command => {
|
|||
doc = state.doc;
|
||||
|
||||
// Renumber the list
|
||||
view.dispatch(state.changeByRange((sel: SelectionRange) => {
|
||||
return renumberList(state, sel);
|
||||
}));
|
||||
view.dispatch(renumberSelectedLists(state));
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -417,9 +415,7 @@ export const increaseIndent: Command = (view: EditorView): boolean => {
|
|||
view.dispatch(changes);
|
||||
|
||||
// Fix any lists
|
||||
view.dispatch(view.state.changeByRange((sel: SelectionRange) => {
|
||||
return renumberList(view.state, sel);
|
||||
}));
|
||||
view.dispatch(renumberSelectedLists(view.state));
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -468,9 +464,7 @@ export const decreaseIndent: Command = (view: EditorView): boolean => {
|
|||
view.dispatch(changes);
|
||||
|
||||
// Fix any lists
|
||||
view.dispatch(view.state.changeByRange((sel: SelectionRange) => {
|
||||
return renumberList(view.state, sel);
|
||||
}));
|
||||
view.dispatch(renumberSelectedLists(view.state));
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import {
|
||||
findInlineMatch, MatchSide, RegionSpec, tabsToSpaces, toggleRegionFormatGlobally,
|
||||
findInlineMatch, MatchSide, RegionSpec, renumberSelectedLists, tabsToSpaces, toggleRegionFormatGlobally,
|
||||
} from './markdownReformatter';
|
||||
import { Text as DocumentText, EditorSelection, EditorState } from '@codemirror/state';
|
||||
import { indentUnit } from '@codemirror/language';
|
||||
import createTestEditor from '../testUtil/createTestEditor';
|
||||
|
||||
describe('markdownReformatter', () => {
|
||||
|
||||
|
@ -142,4 +143,55 @@ describe('markdownReformatter', () => {
|
|||
expect(tabsToSpaces(state, '\t ')).toBe(' ');
|
||||
expect(tabsToSpaces(state, ' \t ')).toBe(' ');
|
||||
});
|
||||
|
||||
it('should correctly renumber a list with multiple selections', async () => {
|
||||
const firstListText = [
|
||||
'1. This',
|
||||
'\t2. is',
|
||||
'\t3. a',
|
||||
'4. test',
|
||||
'',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const secondListText = [
|
||||
'## List 2',
|
||||
'',
|
||||
'1. Test',
|
||||
'\t2. 2',
|
||||
].join('\n');
|
||||
|
||||
const editor = await createTestEditor(
|
||||
`${firstListText}${secondListText}\n\n# End`,
|
||||
EditorSelection.cursor(firstListText.length + secondListText.length),
|
||||
['OrderedList', 'ATXHeading1', 'ATXHeading2'],
|
||||
);
|
||||
|
||||
// Include a selection twice in the same list -- previously,
|
||||
const initialSelection = EditorSelection.create([
|
||||
EditorSelection.cursor('1. This\n2.'.length), // Middle of second line
|
||||
EditorSelection.cursor('1. This\n2. is\n3'.length), // Beginning of third line
|
||||
EditorSelection.cursor(firstListText.length + secondListText.length - 1), // End
|
||||
]);
|
||||
|
||||
editor.dispatch({
|
||||
selection: initialSelection,
|
||||
});
|
||||
|
||||
editor.dispatch(renumberSelectedLists(editor.state));
|
||||
|
||||
expect(editor.state.doc.toString()).toBe([
|
||||
'1. This',
|
||||
'\t1. is',
|
||||
'\t2. a',
|
||||
'2. test',
|
||||
'',
|
||||
'## List 2',
|
||||
'',
|
||||
'1. Test',
|
||||
'\t1. 2',
|
||||
'',
|
||||
'# End',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -619,20 +619,22 @@ export const toggleSelectedLinesStartWith = (
|
|||
};
|
||||
|
||||
// Ensures that ordered lists within [sel] are numbered in ascending order.
|
||||
export const renumberList = (state: EditorState, sel: SelectionRange): SelectionUpdate => {
|
||||
export const renumberSelectedLists = (state: EditorState): TransactionSpec => {
|
||||
const doc = state.doc;
|
||||
|
||||
const listItemRegex = /^(\s*)(\d+)\.\s?/;
|
||||
const changes: ChangeSpec[] = [];
|
||||
const fromLine = doc.lineAt(sel.from);
|
||||
const toLine = doc.lineAt(sel.to);
|
||||
let charsAdded = 0;
|
||||
|
||||
// Re-numbers ordered lists and sublists with numbers on each line in [linesToHandle]
|
||||
const handleLines = (linesToHandle: Line[]) => {
|
||||
const changes: ChangeSpec[] = [];
|
||||
|
||||
type ListItemRecord = {
|
||||
nextListNumber: number;
|
||||
indentationLength: number;
|
||||
};
|
||||
const listNumberStack: ListItemRecord[] = [];
|
||||
let currentGroupIndentation = '';
|
||||
let nextListNumber = 1;
|
||||
const listNumberStack: number[] = [];
|
||||
let prevLineNumber;
|
||||
|
||||
for (const line of linesToHandle) {
|
||||
|
@ -644,15 +646,38 @@ export const renumberList = (state: EditorState, sel: SelectionRange): Selection
|
|||
|
||||
const filteredText = stripBlockquote(line);
|
||||
const match = filteredText.match(listItemRegex);
|
||||
|
||||
// Skip lines that aren't the correct type (e.g. blank lines)
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const indentation = match[1];
|
||||
|
||||
const indentationLen = tabsToSpaces(state, indentation).length;
|
||||
const targetIndentLen = tabsToSpaces(state, currentGroupIndentation).length;
|
||||
let targetIndentLen = tabsToSpaces(state, currentGroupIndentation).length;
|
||||
if (targetIndentLen < indentationLen) {
|
||||
listNumberStack.push(nextListNumber);
|
||||
listNumberStack.push({ nextListNumber, indentationLength: indentationLen });
|
||||
nextListNumber = 1;
|
||||
} else if (targetIndentLen > indentationLen) {
|
||||
nextListNumber = listNumberStack.pop() ?? parseInt(match[2], 10);
|
||||
nextListNumber = parseInt(match[2], 10);
|
||||
|
||||
// Handle the case where we deindent multiple times. For example,
|
||||
// 1. test
|
||||
// 1. test
|
||||
// 1. test
|
||||
// 2. test
|
||||
while (targetIndentLen > indentationLen) {
|
||||
const listNumberRecord = listNumberStack.pop();
|
||||
|
||||
if (!listNumberRecord) {
|
||||
break;
|
||||
} else {
|
||||
targetIndentLen = listNumberRecord.indentationLength;
|
||||
nextListNumber = listNumberRecord.nextListNumber;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (targetIndentLen !== indentationLen) {
|
||||
|
@ -669,44 +694,65 @@ export const renumberList = (state: EditorState, sel: SelectionRange): Selection
|
|||
to,
|
||||
insert: inserted,
|
||||
});
|
||||
charsAdded -= to - from;
|
||||
charsAdded += inserted.length;
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
||||
|
||||
const linesToHandle: Line[] = [];
|
||||
syntaxTree(state).iterate({
|
||||
from: sel.from,
|
||||
to: sel.to,
|
||||
enter: (nodeRef: SyntaxNodeRef) => {
|
||||
if (nodeRef.name === 'ListItem') {
|
||||
for (const node of nodeRef.node.parent.getChildren('ListItem')) {
|
||||
const line = doc.lineAt(node.from);
|
||||
const filteredText = stripBlockquote(line);
|
||||
const match = filteredText.match(listItemRegex);
|
||||
if (match) {
|
||||
linesToHandle.push(line);
|
||||
// Find all selected lists
|
||||
const selectedListRanges: SelectionRange[] = [];
|
||||
for (const selection of state.selection.ranges) {
|
||||
const listLines: Line[] = [];
|
||||
|
||||
syntaxTree(state).iterate({
|
||||
from: selection.from,
|
||||
to: selection.to,
|
||||
enter: (nodeRef: SyntaxNodeRef) => {
|
||||
if (nodeRef.name === 'ListItem') {
|
||||
for (const node of nodeRef.node.parent.getChildren('ListItem')) {
|
||||
const line = doc.lineAt(node.from);
|
||||
const filteredText = stripBlockquote(line);
|
||||
const match = filteredText.match(listItemRegex);
|
||||
if (match) {
|
||||
listLines.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
listLines.sort((a, b) => a.number - b.number);
|
||||
|
||||
if (listLines.length > 0) {
|
||||
const fromLine = listLines[0];
|
||||
const toLine = listLines[listLines.length - 1];
|
||||
|
||||
selectedListRanges.push(
|
||||
EditorSelection.range(fromLine.from, toLine.to),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const changes: ChangeSpec[] = [];
|
||||
if (selectedListRanges.length > 0) {
|
||||
// Use EditorSelection.create to merge overlapping lists
|
||||
const listsToHandle = EditorSelection.create(selectedListRanges).ranges;
|
||||
|
||||
for (const listSelection of listsToHandle) {
|
||||
const lines = [];
|
||||
|
||||
const startLine = doc.lineAt(listSelection.from);
|
||||
const endLine = doc.lineAt(listSelection.to);
|
||||
|
||||
for (let i = startLine.number; i <= endLine.number; i++) {
|
||||
lines.push(doc.line(i));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
linesToHandle.sort((a, b) => a.number - b.number);
|
||||
handleLines(linesToHandle);
|
||||
|
||||
// Re-position the selection in a way that makes sense
|
||||
if (sel.empty) {
|
||||
sel = EditorSelection.cursor(toLine.to + charsAdded);
|
||||
} else {
|
||||
sel = EditorSelection.range(
|
||||
fromLine.from,
|
||||
toLine.to + charsAdded,
|
||||
);
|
||||
changes.push(...handleLines(lines));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
range: sel,
|
||||
changes,
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue