Revert "After triggering markdown to bold text, the caret should appear after the space character. (#1293)" (#1303)

This reverts commit e217ca81131813034ab71bca8f9d26108c89742b.
This commit is contained in:
Dominic Gannaway
2022-02-16 10:41:45 +00:00
committed by acywatson
parent 864d518462
commit c9fbe99309
9 changed files with 142 additions and 236 deletions

View File

@ -118,40 +118,28 @@ export function $findNodeWithOffsetFromJoinedText(
const children = elementNode.getChildren(); const children = elementNode.getChildren();
const childrenLength = children.length; const childrenLength = children.length;
let runningLength = 0; let runningLength = 0;
let isPriorNodeTextNode = false;
for (let i = 0; i < childrenLength; ++i) { for (let i = 0; i < childrenLength; ++i) {
// We must examine the offsetInJoinedText that is located if (runningLength >= joinedTextLength) {
// at the length of the string.
// For example, given "hello", the length is 5, yet
// the caller still wants the node + offset at the
// right edge of the "o".
if (runningLength > joinedTextLength) {
break; break;
} }
const child = children[i]; const child = children[i];
const isChildNodeTestNode = $isTextNode(child); const childContentLength = $isTextNode(child)
const childContentLength = isChildNodeTestNode
? child.getTextContent().length ? child.getTextContent().length
: separatorLength; : separatorLength;
const newRunningLength = runningLength + childContentLength; const newRunningLength = runningLength + childContentLength;
if (
const isJoinedOffsetWithinNode = runningLength <= offsetInJoinedText &&
(isPriorNodeTextNode === false && runningLength === offsetInJoinedText) || offsetInJoinedText < newRunningLength &&
(runningLength === 0 && runningLength === offsetInJoinedText) || $isTextNode(child)
(runningLength < offsetInJoinedText && ) {
offsetInJoinedText <= newRunningLength);
if (isJoinedOffsetWithinNode && $isTextNode(child)) {
// Check isTextNode again for flow.
return { return {
node: child, node: child,
offset: offsetInJoinedText - runningLength, offset: offsetInJoinedText - runningLength,
}; };
} }
runningLength = newRunningLength; runningLength = newRunningLength;
isPriorNodeTextNode = isChildNodeTestNode;
} }
return null; return null;
} }

View File

@ -137,7 +137,7 @@ function useTypeahead(editor: LexicalEditor): void {
} }
}, },
{ {
tag: 'history-merge', tag: 'without-history',
}, },
); );
}, [editor, getTypeaheadTextNode, selectionCollapsed, suggestion]); }, [editor, getTypeaheadTextNode, selectionCollapsed, suggestion]);

View File

@ -53,7 +53,6 @@ export type AutoFormatTriggerState = $ReadOnly<{
// 2. Convert the text formatting: e.g. "**hello**" converts to bold "hello". // 2. Convert the text formatting: e.g. "**hello**" converts to bold "hello".
export type NodeTransformationKind = export type NodeTransformationKind =
| 'noTransformation'
| 'paragraphH1' | 'paragraphH1'
| 'paragraphH2' | 'paragraphH2'
| 'paragraphH3' | 'paragraphH3'
@ -63,20 +62,6 @@ export type NodeTransformationKind =
| 'paragraphCodeBlock' | 'paragraphCodeBlock'
| 'textBold'; | 'textBold';
// The scanning context provides the overall data structure for
// locating a auto formatting candidate and then transforming that candidate
// into the newly formatted stylized text.
// The context is filled out lazily to avoid redundant or up-front expensive
// calculations. For example, this includes the parent element's getTextContent() which
// ultimately gets deposited into the joinedText field.
export type ScanningContext = {
autoFormatCriteria: AutoFormatCriteria,
joinedText: ?string,
matchResultContext: MatchResultContext,
textNodeWithOffset: TextNodeWithOffset,
triggerState: AutoFormatTriggerState,
};
// The auto formatter runs these steps: // The auto formatter runs these steps:
// 1. Examine the current and prior editor states to see if a potential auto format is triggered. // 1. Examine the current and prior editor states to see if a potential auto format is triggered.
// 2. If triggered, examine the current editor state to see if it matches a particular // 2. If triggered, examine the current editor state to see if it matches a particular
@ -89,12 +74,26 @@ export type ScanningContext = {
// // // // // //
// Capture groups are defined by the regEx pattern. Certain groups must be removed, // Capture groups are defined by the regEx pattern. Certain groups must be removed,
// For example "*hello*", will require that the "*" be removed and the "hello" become bolded. // For example "*hello*", will require that the "*" be removed and the "hello" become bolded.
// We can specify ahead of time which gapture groups shoud be removed using the regExCaptureGroupsToDelete.
export type AutoFormatCriteria = $ReadOnly<{ export type AutoFormatCriteria = $ReadOnly<{
nodeTransformationKind: ?NodeTransformationKind, nodeTransformationKind: ?NodeTransformationKind,
regEx: RegExp, regEx: RegExp,
regExCaptureGroupsToDelete: ?Array<number>,
regExExpectedCaptureGroupCount: number,
requiresParagraphStart: ?boolean, requiresParagraphStart: ?boolean,
}>; }>;
// While scanning over the text, comparing each
// auto format criteria against the text, certain
// details may be captured rather than re-calculated.
// For example, scanning over each AutoFormatCriteria,
// building up the ParagraphNode's text by calling getTextContent()
// may be expensive. Rather, load this value lazily and store it for later use.
export type ScanningContext = {
joinedText: ?string,
textNodeWithOffset: TextNodeWithOffset,
};
// RegEx returns the discovered pattern matches in an array of capture groups. // RegEx returns the discovered pattern matches in an array of capture groups.
// Using this array, we can extract the sub-string per pattern, determine its // Using this array, we can extract the sub-string per pattern, determine its
// offset within the parent (except for non-textNode children) within the overall string and determine its length. Typically the length // offset within the parent (except for non-textNode children) within the overall string and determine its length. Typically the length
@ -111,8 +110,8 @@ type CaptureGroupDetail = {
// This type stores the result details when a particular // This type stores the result details when a particular
// match is found. // match is found.
export type MatchResultContext = { export type MatchResultContext = {
offsetInJoinedTextForCollapsedSelection: number, // The expected location for the blinking caret.
regExCaptureGroups: Array<CaptureGroupDetail>, regExCaptureGroups: Array<CaptureGroupDetail>,
triggerState: ?AutoFormatTriggerState,
}; };
export type AutoFormatCriteriaWithMatchResultContext = { export type AutoFormatCriteriaWithMatchResultContext = {
@ -129,6 +128,8 @@ const SEPARATOR_BETWEEN_TEXT_AND_NON_TEXT_NODES = '\u0004'; // Select an unused
const autoFormatBase: AutoFormatCriteria = { const autoFormatBase: AutoFormatCriteria = {
nodeTransformationKind: null, nodeTransformationKind: null,
regEx: /(?:)/, regEx: /(?:)/,
regExCaptureGroupsToDelete: null,
regExExpectedCaptureGroupCount: 1,
requiresParagraphStart: false, requiresParagraphStart: false,
}; };
@ -183,6 +184,7 @@ const markdownOrderedList: AutoFormatCriteria = {
...paragraphStartBase, ...paragraphStartBase,
nodeTransformationKind: 'paragraphOrderedList', nodeTransformationKind: 'paragraphOrderedList',
regEx: /^(\d+)\.\s/, regEx: /^(\d+)\.\s/,
regExExpectedCaptureGroupCount: 2 /*e.g. '321. ' returns '321. ' & '321'*/,
}; };
const markdownBold: AutoFormatCriteria = { const markdownBold: AutoFormatCriteria = {
@ -192,6 +194,10 @@ const markdownBold: AutoFormatCriteria = {
regEx: /(\*)(\s*\b)([^\*]*)(\b\s*)(\*\s)$/, regEx: /(\*)(\s*\b)([^\*]*)(\b\s*)(\*\s)$/,
// Remove the first and last capture groups. Remeber, the 0th capture group is the entire string. // Remove the first and last capture groups. Remeber, the 0th capture group is the entire string.
// e.g. "*Hello* " requires removing both "*" as well as bolding "Hello". // e.g. "*Hello* " requires removing both "*" as well as bolding "Hello".
regExCaptureGroupsToDelete: [1, 5],
// The $ will find the target at the end of the string.
regExExpectedCaptureGroupCount: 6,
}; };
const allAutoFormatCriteriaForTextNodes = [markdownBold]; const allAutoFormatCriteriaForTextNodes = [markdownBold];
@ -216,47 +222,28 @@ export function getAllAutoFormatCriteria(): AutoFormatCriteriaArray {
return allAutoFormatCriteria; return allAutoFormatCriteria;
} }
export function getInitialScanningContext(
textNodeWithOffset: TextNodeWithOffset,
triggerState: AutoFormatTriggerState,
): ScanningContext {
return {
autoFormatCriteria: {
nodeTransformationKind: 'noTransformation',
regEx: /(?:)/, // Empty reg ex will do until the precise criteria is discovered.
requiresParagraphStart: null,
},
joinedText: null,
matchResultContext: {
offsetInJoinedTextForCollapsedSelection: 0,
regExCaptureGroups: [],
},
textNodeWithOffset,
triggerState,
};
}
function getMatchResultContextWithRegEx( function getMatchResultContextWithRegEx(
textToSearch: string, textToSearch: string,
matchMustAppearAtStartOfString: boolean, matchMustAppearAtStartOfString: boolean,
matchMustAppearAtEndOfString: boolean, matchMustAppearAtEndOfString: boolean,
regEx: RegExp, regEx: RegExp,
regExExpectedCaptureGroupCount: number,
scanningContext: ScanningContext,
): null | MatchResultContext { ): null | MatchResultContext {
const matchResultContext: MatchResultContext = { const matchResultContext: MatchResultContext = {
offsetInJoinedTextForCollapsedSelection: 0,
regExCaptureGroups: [], regExCaptureGroups: [],
triggerState: null,
}; };
const regExMatches = textToSearch.match(regEx); const regExMatches = textToSearch.match(regEx);
if ( if (
regExMatches !== null && regExMatches !== null &&
regExMatches.length > 0 && regExMatches.length > 0 &&
regExMatches.length === regExExpectedCaptureGroupCount &&
(matchMustAppearAtStartOfString === false || regExMatches.index === 0) && (matchMustAppearAtStartOfString === false || regExMatches.index === 0) &&
(matchMustAppearAtEndOfString === false || (matchMustAppearAtEndOfString === false ||
regExMatches.index + regExMatches[0].length === textToSearch.length) regExMatches.index + regExMatches[0].length === textToSearch.length)
) { ) {
matchResultContext.offsetInJoinedTextForCollapsedSelection =
textToSearch.length;
const captureGroupsCount = regExMatches.length; const captureGroupsCount = regExMatches.length;
let runningLength = regExMatches.index; let runningLength = regExMatches.index;
for ( for (
@ -305,6 +292,8 @@ function getMatchResultContextForParagraphs(
true, true,
false, false,
autoFormatCriteria.regEx, autoFormatCriteria.regEx,
autoFormatCriteria.regExExpectedCaptureGroupCount,
scanningContext,
); );
} }
@ -332,6 +321,8 @@ function getMatchResultContextForText(
false, false,
true, true,
autoFormatCriteria.regEx, autoFormatCriteria.regEx,
autoFormatCriteria.regExExpectedCaptureGroupCount,
scanningContext,
); );
} else { } else {
invariant( invariant(
@ -362,13 +353,12 @@ export function getMatchResultContextForCriteria(
} }
function getNewNodeForCriteria( function getNewNodeForCriteria(
scanningContext: ScanningContext, autoFormatCriteria: AutoFormatCriteria,
matchResultContext: MatchResultContext,
children: Array<LexicalNode>, children: Array<LexicalNode>,
): null | ElementNode { ): null | ElementNode {
let newNode = null; let newNode = null;
const autoFormatCriteria = scanningContext.autoFormatCriteria;
const matchResultContext = scanningContext.matchResultContext;
if (autoFormatCriteria.nodeTransformationKind != null) { if (autoFormatCriteria.nodeTransformationKind != null) {
switch (autoFormatCriteria.nodeTransformationKind) { switch (autoFormatCriteria.nodeTransformationKind) {
case 'paragraphH1': { case 'paragraphH1': {
@ -415,8 +405,8 @@ function getNewNodeForCriteria(
case 'paragraphCodeBlock': { case 'paragraphCodeBlock': {
// Toggle code and paragraph nodes. // Toggle code and paragraph nodes.
if ( if (
scanningContext.triggerState != null && matchResultContext.triggerState != null &&
scanningContext.triggerState.isCodeBlock matchResultContext.triggerState.isCodeBlock
) { ) {
newNode = $createParagraphNode(); newNode = $createParagraphNode();
} else { } else {
@ -443,22 +433,37 @@ function updateTextNode(node: TextNode, count: number): void {
export function transformTextNodeForAutoFormatCriteria( export function transformTextNodeForAutoFormatCriteria(
scanningContext: ScanningContext, scanningContext: ScanningContext,
autoFormatCriteria: AutoFormatCriteria,
matchResultContext: MatchResultContext,
) { ) {
if (scanningContext.autoFormatCriteria.requiresParagraphStart) { if (autoFormatCriteria.requiresParagraphStart) {
transformTextNodeForParagraphs(scanningContext); transformTextNodeForParagraphs(
scanningContext,
autoFormatCriteria,
matchResultContext,
);
} else { } else {
transformTextNodeForText(scanningContext); transformTextNodeForText(
scanningContext,
autoFormatCriteria,
matchResultContext,
);
} }
} }
function transformTextNodeForParagraphs(scanningContext: ScanningContext) { function transformTextNodeForParagraphs(
scanningContext: ScanningContext,
autoFormatCriteria: AutoFormatCriteria,
matchResultContext: MatchResultContext,
) {
const textNodeWithOffset = scanningContext.textNodeWithOffset; const textNodeWithOffset = scanningContext.textNodeWithOffset;
const element = textNodeWithOffset.node.getParentOrThrow(); const element = textNodeWithOffset.node.getParentOrThrow();
const text = scanningContext.matchResultContext.regExCaptureGroups[0].text; const text = matchResultContext.regExCaptureGroups[0].text;
updateTextNode(textNodeWithOffset.node, text.length); updateTextNode(textNodeWithOffset.node, text.length);
const elementNode = getNewNodeForCriteria( const elementNode = getNewNodeForCriteria(
scanningContext, autoFormatCriteria,
matchResultContext,
element.getChildren(), element.getChildren(),
); );
@ -467,29 +472,29 @@ function transformTextNodeForParagraphs(scanningContext: ScanningContext) {
} }
} }
function transformTextNodeForText(scanningContext: ScanningContext) { function transformTextNodeForText(
const autoFormatCriteria = scanningContext.autoFormatCriteria; scanningContext: ScanningContext,
const matchResultContext = scanningContext.matchResultContext; autoFormatCriteria: AutoFormatCriteria,
matchResultContext: MatchResultContext,
) {
if (autoFormatCriteria.nodeTransformationKind != null) { if (autoFormatCriteria.nodeTransformationKind != null) {
switch (autoFormatCriteria.nodeTransformationKind) { switch (autoFormatCriteria.nodeTransformationKind) {
case 'textBold': { case 'textBold': {
if (matchResultContext.regExCaptureGroups.length !== 6) {
// The expected reg ex pattern for bold should have 6 groups.
// If it does not, then break and fail silently.
// e2e tests validate the regEx pattern.
break;
}
matchResultContext.regExCaptureGroups = matchResultContext.regExCaptureGroups =
getCaptureGroupsByResolvingAllDetails(scanningContext); getCaptureGroupsByResolvingAllDetails(
// Remove unwanted text in reg ex pattern. scanningContext,
removeTextInCaptureGroups([1, 5], matchResultContext); autoFormatCriteria,
formatTextInCaptureGroupIndex('bold', 3, matchResultContext); matchResultContext,
makeCollapsedSelectionAtOffsetInJoinedText(
matchResultContext.offsetInJoinedTextForCollapsedSelection,
matchResultContext.offsetInJoinedTextForCollapsedSelection + 1,
scanningContext.textNodeWithOffset.node.getParentOrThrow(),
); );
if (autoFormatCriteria.regExCaptureGroupsToDelete != null) {
// Remove unwanted text in reg ex patterh.
removeTextInCaptureGroups(
autoFormatCriteria.regExCaptureGroupsToDelete,
matchResultContext,
);
formatTextInCaptureGroupIndex('bold', 3, matchResultContext);
}
break; break;
} }
default: default:
@ -503,10 +508,9 @@ function transformTextNodeForText(scanningContext: ScanningContext) {
// known, the details may be fully resolved without incurring unwasted performance cost. // known, the details may be fully resolved without incurring unwasted performance cost.
function getCaptureGroupsByResolvingAllDetails( function getCaptureGroupsByResolvingAllDetails(
scanningContext: ScanningContext, scanningContext: ScanningContext,
autoFormatCriteria: AutoFormatCriteria,
matchResultContext: MatchResultContext,
): Array<CaptureGroupDetail> { ): Array<CaptureGroupDetail> {
const autoFormatCriteria = scanningContext.autoFormatCriteria;
const matchResultContext = scanningContext.matchResultContext;
const textNodeWithOffset = scanningContext.textNodeWithOffset; const textNodeWithOffset = scanningContext.textNodeWithOffset;
const regExCaptureGroups = matchResultContext.regExCaptureGroups; const regExCaptureGroups = matchResultContext.regExCaptureGroups;
const captureGroupsCount = regExCaptureGroups.length; const captureGroupsCount = regExCaptureGroups.length;
@ -633,12 +637,6 @@ function shiftCaptureGroupOffsets(
startingCaptureGroupIndex: number, startingCaptureGroupIndex: number,
matchResultContext: MatchResultContext, matchResultContext: MatchResultContext,
) { ) {
matchResultContext.offsetInJoinedTextForCollapsedSelection += delta;
invariant(
matchResultContext.offsetInJoinedTextForCollapsedSelection > 0,
'The text content string length does not correlate with insertions/deletions of new text.',
);
const regExCaptureGroups = matchResultContext.regExCaptureGroups; const regExCaptureGroups = matchResultContext.regExCaptureGroups;
const regExCaptureGroupsCount = regExCaptureGroups.length; const regExCaptureGroupsCount = regExCaptureGroups.length;
for ( for (
@ -707,52 +705,6 @@ function formatTextInCaptureGroupIndex(
const currentSelection = $getSelection(); const currentSelection = $getSelection();
if (currentSelection != null) { if (currentSelection != null) {
currentSelection.formatText(formatType); currentSelection.formatText(formatType);
const finalSelection = $createRangeSelection();
finalSelection.anchor.set(
focusTextNodeWithOffset.node.getKey(),
focusTextNodeWithOffset.offset + 1,
'text',
);
finalSelection.focus.set(
focusTextNodeWithOffset.node.getKey(),
focusTextNodeWithOffset.offset + 1,
'text',
);
$setSelection(finalSelection);
} }
} }
} }
function makeCollapsedSelectionAtOffsetInJoinedText(
offsetInJoinedText: number,
joinedTextLength: number,
parentElementNode: ElementNode,
) {
const textNodeWithOffset = $findNodeWithOffsetFromJoinedText(
parentElementNode,
joinedTextLength,
offsetInJoinedText,
TRIGGER_STRING_LENGTH,
);
if (textNodeWithOffset != null) {
const newSelection = $createRangeSelection();
newSelection.anchor.set(
textNodeWithOffset.node.getKey(),
textNodeWithOffset.offset,
'text',
);
newSelection.focus.set(
textNodeWithOffset.node.getKey(),
textNodeWithOffset.offset,
'text',
);
$setSelection(newSelection);
}
}

View File

@ -24,7 +24,6 @@ import {useEffect} from 'react';
import { import {
getAllAutoFormatCriteria, getAllAutoFormatCriteria,
getAllAutoFormatCriteriaForTextNodes, getAllAutoFormatCriteriaForTextNodes,
getInitialScanningContext,
getMatchResultContextForCriteria, getMatchResultContextForCriteria,
transformTextNodeForAutoFormatCriteria, transformTextNodeForAutoFormatCriteria,
TRIGGER_STRING, TRIGGER_STRING,
@ -32,18 +31,16 @@ import {
function getCriteriaWithMatchResultContext( function getCriteriaWithMatchResultContext(
autoFormatCriteriaArray: AutoFormatCriteriaArray, autoFormatCriteriaArray: AutoFormatCriteriaArray,
currentTriggerState: AutoFormatTriggerState,
scanningContext: ScanningContext, scanningContext: ScanningContext,
): AutoFormatCriteriaWithMatchResultContext { ): AutoFormatCriteriaWithMatchResultContext {
const currentTriggerState = scanningContext.triggerState;
const count = autoFormatCriteriaArray.length; const count = autoFormatCriteriaArray.length;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const autoFormatCriteria = autoFormatCriteriaArray[i]; const autoFormatCriteria = autoFormatCriteriaArray[i];
// Skip code block nodes, unless the nodeTransformationKind calls for toggling the code block. // Skip code block nodes, unless the nodeTransformationKind calls for toggling the code block.
if ( if (
(currentTriggerState != null && currentTriggerState.isCodeBlock === false ||
currentTriggerState.isCodeBlock === false) ||
autoFormatCriteria.nodeTransformationKind === 'paragraphCodeBlock' autoFormatCriteria.nodeTransformationKind === 'paragraphCodeBlock'
) { ) {
const matchResultContext = getMatchResultContextForCriteria( const matchResultContext = getMatchResultContextForCriteria(
@ -51,6 +48,7 @@ function getCriteriaWithMatchResultContext(
scanningContext, scanningContext,
); );
if (matchResultContext != null) { if (matchResultContext != null) {
matchResultContext.triggerState = currentTriggerState;
return { return {
autoFormatCriteria: autoFormatCriteria, autoFormatCriteria: autoFormatCriteria,
matchResultContext, matchResultContext,
@ -78,24 +76,9 @@ function getTextNodeForAutoFormatting(
function updateAutoFormatting( function updateAutoFormatting(
editor: LexicalEditor, editor: LexicalEditor,
scanningContext: ScanningContext,
): void {
editor.update(
() => {
transformTextNodeForAutoFormatCriteria(scanningContext);
},
{
tag: 'history-push',
},
);
}
function findScanningContextWithValidMatch(
editorState: EditorState,
currentTriggerState: AutoFormatTriggerState, currentTriggerState: AutoFormatTriggerState,
): null | ScanningContext { ): void {
let scanningContext = null; editor.update(() => {
editorState.read(() => {
const textNodeWithOffset = getTextNodeForAutoFormatting($getSelection()); const textNodeWithOffset = getTextNodeForAutoFormatting($getSelection());
if (textNodeWithOffset === null) { if (textNodeWithOffset === null) {
@ -103,17 +86,17 @@ function findScanningContextWithValidMatch(
} }
// Please see the declaration of ScanningContext for a detailed explanation. // Please see the declaration of ScanningContext for a detailed explanation.
const initialScanningContext = getInitialScanningContext( const scanningContext: ScanningContext = {
joinedText: null,
textNodeWithOffset, textNodeWithOffset,
currentTriggerState, };
);
const criteriaWithMatchResultContext = getCriteriaWithMatchResultContext( const criteriaWithMatchResultContext = getCriteriaWithMatchResultContext(
// Do not apply paragraph node changes like blockQuote or H1 to listNodes. Also, do not attempt to transform a list into a list using * or -. // Do not apply paragraph node changes like blockQuote or H1 to listNodes. Also, do not attempt to transform a list into a list using * or -.
currentTriggerState.isParentAListItemNode === false currentTriggerState.isParentAListItemNode === false
? getAllAutoFormatCriteria() ? getAllAutoFormatCriteria()
: getAllAutoFormatCriteriaForTextNodes(), : getAllAutoFormatCriteriaForTextNodes(),
initialScanningContext, currentTriggerState,
scanningContext,
); );
if ( if (
@ -122,23 +105,21 @@ function findScanningContextWithValidMatch(
) { ) {
return; return;
} }
scanningContext = initialScanningContext;
// Lazy fill-in the particular format criteria and any matching result information. transformTextNodeForAutoFormatCriteria(
scanningContext.autoFormatCriteria = scanningContext,
criteriaWithMatchResultContext.autoFormatCriteria; criteriaWithMatchResultContext.autoFormatCriteria,
scanningContext.matchResultContext = criteriaWithMatchResultContext.matchResultContext,
criteriaWithMatchResultContext.matchResultContext; );
}); });
return scanningContext;
} }
function findScanningContext( function shouldAttemptToAutoFormat(
editorState: EditorState,
currentTriggerState: null | AutoFormatTriggerState, currentTriggerState: null | AutoFormatTriggerState,
priorTriggerState: null | AutoFormatTriggerState, priorTriggerState: null | AutoFormatTriggerState,
): null | ScanningContext { ): boolean {
if (currentTriggerState == null || priorTriggerState == null) { if (currentTriggerState == null || priorTriggerState == null) {
return null; return false;
} }
// The below checks needs to execute relativey quickly, so perform the light-weight ones first. // The below checks needs to execute relativey quickly, so perform the light-weight ones first.
@ -148,8 +129,8 @@ function findScanningContext(
const currentTextContentLength = currentTriggerState.textContent.length; const currentTextContentLength = currentTriggerState.textContent.length;
const triggerOffset = currentTriggerState.anchorOffset - triggerStringLength; const triggerOffset = currentTriggerState.anchorOffset - triggerStringLength;
if ( return (
(currentTriggerState.hasParentNode === true && currentTriggerState.hasParentNode === true &&
currentTriggerState.isSimpleText && currentTriggerState.isSimpleText &&
currentTriggerState.isSelectionCollapsed && currentTriggerState.isSelectionCollapsed &&
currentTriggerState.nodeKey === priorTriggerState.nodeKey && currentTriggerState.nodeKey === priorTriggerState.nodeKey &&
@ -160,13 +141,8 @@ function findScanningContext(
triggerOffset, triggerOffset,
triggerStringLength, triggerStringLength,
) === TRIGGER_STRING && ) === TRIGGER_STRING &&
currentTriggerState.textContent !== priorTriggerState.textContent) === currentTriggerState.textContent !== priorTriggerState.textContent
false );
) {
return null;
}
return findScanningContextWithValidMatch(editorState, currentTriggerState);
} }
function getTriggerState( function getTriggerState(
@ -210,21 +186,16 @@ export default function useAutoFormatter(editor: LexicalEditor): void {
// However, given "#A B", where the user delets "A" should not. // However, given "#A B", where the user delets "A" should not.
let priorTriggerState: null | AutoFormatTriggerState = null; let priorTriggerState: null | AutoFormatTriggerState = null;
return editor.addListener('update', ({tags}) => { editor.addListener('update', ({tags}) => {
// Examine historic so that we are not running autoformatting within markdown. // Examine historic so that we are not running autoformatting within markdown.
if (tags.has('historic') === false) { if (tags.has('historic') === false) {
const editorState = editor.getEditorState(); const currentTriggerState = getTriggerState(editor.getEditorState());
const currentTriggerState = getTriggerState(editorState);
const scanningContext = if (
currentTriggerState == null shouldAttemptToAutoFormat(currentTriggerState, priorTriggerState) &&
? null currentTriggerState != null
: findScanningContext( ) {
editorState, updateAutoFormatting(editor, currentTriggerState);
currentTriggerState,
priorTriggerState,
);
if (scanningContext != null) {
updateAutoFormatting(editor, scanningContext);
} }
priorTriggerState = currentTriggerState; priorTriggerState = currentTriggerState;
} else { } else {

View File

@ -81,7 +81,7 @@ export function useCharacterLimit(
$wrapOverflowedNodes(offset); $wrapOverflowedNodes(offset);
}, },
{ {
tag: 'history-merge', tag: 'without-history',
}, },
); );
} }

View File

@ -22,9 +22,9 @@ import {$getSelection, $isRootNode, $isTextNode} from 'lexical';
import {useCallback, useEffect, useMemo} from 'react'; import {useCallback, useEffect, useMemo} from 'react';
type MergeAction = 0 | 1 | 2; type MergeAction = 0 | 1 | 2;
const HISTORY_MERGE = 0; const MERGE = 0;
const HISTORY_PUSH = 1; const NO_MERGE = 1;
const DISCARD_HISTORY_CANDIDATE = 2; const DISCARD = 2;
type ChangeType = 0 | 1 | 2 | 3 | 4; type ChangeType = 0 | 1 | 2 | 3 | 4;
const OTHER = 0; const OTHER = 0;
@ -204,7 +204,7 @@ function createMergeActionGetter(
if (tags.has('historic')) { if (tags.has('historic')) {
prevChangeType = OTHER; prevChangeType = OTHER;
prevChangeTime = changeTime; prevChangeTime = changeTime;
return DISCARD_HISTORY_CANDIDATE; return DISCARD;
} }
const changeType = getChangeType( const changeType = getChangeType(
@ -216,40 +216,35 @@ function createMergeActionGetter(
); );
const mergeAction = (() => { const mergeAction = (() => {
const shouldPushHistory = tags.has('history-push'); if (tags.has('without-history')) {
const shouldMergeHistory = return MERGE;
!shouldPushHistory && tags.has('history-merge');
if (shouldMergeHistory) {
return HISTORY_MERGE;
} }
if (prevEditorState === null) { if (prevEditorState === null) {
return HISTORY_PUSH; return NO_MERGE;
} }
const selection = nextEditorState._selection; const selection = nextEditorState._selection;
const prevSelection = prevEditorState._selection; const prevSelection = prevEditorState._selection;
const hasDirtyNodes = dirtyLeaves.size > 0 || dirtyElements.size > 0; const hasDirtyNodes = dirtyLeaves.size > 0 || dirtyElements.size > 0;
if (!hasDirtyNodes) { if (!hasDirtyNodes) {
if (prevSelection === null && selection !== null) { if (prevSelection === null && selection !== null) {
return HISTORY_MERGE; return MERGE;
} }
return DISCARD_HISTORY_CANDIDATE; return DISCARD;
} }
const isSameEditor = const isSameEditor =
currentHistoryEntry === null || currentHistoryEntry.editor === editor; currentHistoryEntry === null || currentHistoryEntry.editor === editor;
if ( if (
shouldPushHistory === false &&
changeType !== OTHER && changeType !== OTHER &&
changeType === prevChangeType && changeType === prevChangeType &&
changeTime < prevChangeTime + delay && changeTime < prevChangeTime + delay &&
isSameEditor isSameEditor
) { ) {
return HISTORY_MERGE; return MERGE;
} }
return HISTORY_PUSH; return NO_MERGE;
})(); })();
prevChangeTime = changeTime; prevChangeTime = changeTime;
@ -302,7 +297,7 @@ export function useHistory(
tags, tags,
); );
if (mergeAction === HISTORY_PUSH) { if (mergeAction === NO_MERGE) {
if (redoStack.length !== 0) { if (redoStack.length !== 0) {
historyState.redoStack = []; historyState.redoStack = [];
} }
@ -313,7 +308,7 @@ export function useHistory(
}); });
editor.execCommand('canUndo', true); editor.execCommand('canUndo', true);
} }
} else if (mergeAction === DISCARD_HISTORY_CANDIDATE) { } else if (mergeAction === DISCARD) {
return; return;
} }

View File

@ -490,7 +490,7 @@ class BaseLexicalEditor {
nextRootElement.setAttribute('data-lexical-editor', 'true'); nextRootElement.setAttribute('data-lexical-editor', 'true');
this._dirtyType = FULL_RECONCILE; this._dirtyType = FULL_RECONCILE;
initMutationObserver(getSelf(this)); initMutationObserver(getSelf(this));
this._updateTags.add('history-merge'); this._updateTags.add('without-history');
commitPendingUpdates(getSelf(this)); commitPendingUpdates(getSelf(this));
// TODO: remove this flag once we no longer use UEv2 internally // TODO: remove this flag once we no longer use UEv2 internally
if (!this._config.disableEvents) { if (!this._config.disableEvents) {

View File

@ -339,7 +339,7 @@ export function markAllNodesAsDirty(editor: LexicalEditor, type: string): void {
true, true,
editor._pendingEditorState === null editor._pendingEditorState === null
? { ? {
tag: 'history-merge', tag: 'without-history',
} }
: undefined, : undefined,
); );

View File

@ -62,7 +62,7 @@ export class DecoratorEditor {
this.editorState = editorState; this.editorState = editorState;
if (editorState !== null) { if (editorState !== null) {
editor.setEditorState(editorState, { editor.setEditorState(editorState, {
tag: 'history-merge', tag: 'without-history',
}); });
} }
} }