1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00
joplin/packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.ts

125 lines
3.2 KiB
TypeScript

import { EditorSelection, EditorState, Line, SelectionRange, TransactionSpec } from '@codemirror/state';
import growSelectionToNode from '../growSelectionToNode';
// Toggles whether all lines in the user's selection start with [regex].
const toggleSelectedLinesStartWith = (
state: EditorState,
regex: RegExp,
template: string,
matchEmpty: boolean,
// Determines where this formatting can begin on a line.
// Defaults to after a block quote marker
lineContentStartRegex = /^>\s/,
// Name associated with what [regex] matches (e.g. FencedCode)
nodeName?: string,
): TransactionSpec => {
const getLineContentStart = (line: Line): number => {
const blockQuoteMatch = line.text.match(lineContentStartRegex);
if (blockQuoteMatch) {
return line.from + blockQuoteMatch[0].length;
}
return line.from;
};
const getLineContent = (line: Line): string => {
const contentStart = getLineContentStart(line);
return line.text.substring(contentStart - line.from);
};
const changes = state.changeByRange((sel: SelectionRange) => {
// Attempt to select all lines in the region
if (nodeName && sel.empty) {
sel = growSelectionToNode(state, sel, nodeName);
}
const doc = state.doc;
const fromLine = doc.lineAt(sel.from);
const toLine = doc.lineAt(sel.to);
let hasProp = false;
let charsAdded = 0;
let charsAddedBefore = 0;
const changes = [];
const lines: Line[] = [];
for (let i = fromLine.number; i <= toLine.number; i++) {
const line = doc.line(i);
const text = getLineContent(line);
// If already matching [regex],
if (text.search(regex) === 0) {
hasProp = true;
}
lines.push(line);
}
for (const line of lines) {
const text = getLineContent(line);
const contentFrom = getLineContentStart(line);
// Only process if the line is non-empty.
if (!matchEmpty && text.trim().length === 0
// Treat the first line differently
&& fromLine.number < line.number) {
continue;
}
if (hasProp) {
const match = text.match(regex);
if (!match) {
continue;
}
changes.push({
from: contentFrom,
to: contentFrom + match[0].length,
insert: '',
});
const deletedSize = match[0].length;
if (contentFrom <= sel.from) {
// Math.min: Handles the case where some deleted characters are before sel.from
// and others are after.
charsAddedBefore -= Math.min(sel.from - contentFrom, deletedSize);
}
charsAdded -= deletedSize;
} else {
changes.push({
from: contentFrom,
insert: template,
});
charsAdded += template.length;
if (contentFrom <= sel.from) {
charsAddedBefore += template.length;
}
}
}
// If the selection is empty and a single line was changed, don't grow it.
// (user might be adding a list/header, in which case, selecting the just
// added text isn't helpful)
let newSel;
if (sel.empty && fromLine.number === toLine.number) {
newSel = EditorSelection.cursor(sel.from + charsAddedBefore);
} else {
newSel = EditorSelection.range(fromLine.from, toLine.to + charsAdded);
}
return {
changes,
// Selection should now encompass all lines that were changed.
range: newSel,
};
});
return changes;
};
export default toggleSelectedLinesStartWith;