Skip to content

Commit 6466309

Browse files
Reduce per-keystroke work in markdown decorations
Skip doc.toString() and collectSearchMatches when there is no active search query — avoids materializing the entire document on every keystroke during normal editing. List StateFields now check whether the changed region contains list nodes before doing a full tree rebuild. When typing in headings, paragraphs, code blocks, etc., the existing decorations are mapped through position changes instead of 5 × O(n) tree iterations.
1 parent 810f80f commit 6466309

2 files changed

Lines changed: 78 additions & 8 deletions

File tree

app/src/features/editor/extensions/markdown-decorations/lists.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,58 @@ export function getListMarkerData(
139139
};
140140
}
141141

142+
const LIST_NODE_NAMES = new Set(["BulletList", "OrderedList", "ListItem"]);
143+
144+
/**
145+
* Check whether any of the changed ranges in a transaction intersect
146+
* with list-related syntax nodes. When they don't, the existing
147+
* decorations can be cheaply mapped through position changes instead
148+
* of doing a full tree iteration.
149+
*/
150+
function changesAffectLists(transaction: Transaction): boolean {
151+
let found = false;
152+
153+
transaction.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
154+
if (found) return;
155+
156+
// Check the old tree (handles deletions of list markers)
157+
const oldTree = syntaxTree(transaction.startState);
158+
const oldDoc = transaction.startState.doc;
159+
const oldFrom = oldDoc.lineAt(fromA).from;
160+
const oldTo = oldDoc.lineAt(Math.min(toA, oldDoc.length)).to;
161+
oldTree.iterate({
162+
from: oldFrom,
163+
to: oldTo,
164+
enter(node) {
165+
if (LIST_NODE_NAMES.has(node.name)) {
166+
found = true;
167+
return false;
168+
}
169+
},
170+
});
171+
172+
if (found) return;
173+
174+
// Check the new tree (handles insertions of list markers)
175+
const newTree = syntaxTree(transaction.state);
176+
const newDoc = transaction.state.doc;
177+
const newFrom = newDoc.lineAt(fromB).from;
178+
const newTo = newDoc.lineAt(Math.min(toB, newDoc.length)).to;
179+
newTree.iterate({
180+
from: newFrom,
181+
to: newTo,
182+
enter(node) {
183+
if (LIST_NODE_NAMES.has(node.name)) {
184+
found = true;
185+
return false;
186+
}
187+
},
188+
});
189+
});
190+
191+
return found;
192+
}
193+
142194
function createListStateField(
143195
decorate: (state: EditorState) => [DecorationSet, DecorationSet],
144196
) {
@@ -148,8 +200,28 @@ function createListStateField(
148200
},
149201
update(value, transaction) {
150202
if (!transaction.docChanged) {
203+
// Rebuild when the syntax tree finishes parsing (new nodes
204+
// may have appeared that weren't visible during the initial
205+
// docChanged update).
206+
if (
207+
syntaxTree(transaction.state) !== syntaxTree(transaction.startState)
208+
) {
209+
return decorate(transaction.state);
210+
}
151211
return value;
152212
}
213+
214+
// When the change doesn't touch any list nodes, map existing
215+
// decorations through position changes instead of doing a full
216+
// tree iteration. This avoids 5 × O(n) tree walks per keystroke
217+
// when typing in headings, paragraphs, code blocks, etc.
218+
if (!changesAffectLists(transaction)) {
219+
return [
220+
value[0].map(transaction.changes),
221+
value[1].map(transaction.changes),
222+
];
223+
}
224+
153225
return decorate(transaction.state);
154226
},
155227
provide(field) {

app/src/features/editor/extensions/markdown-decorations/plugin.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,9 @@ export function markdownDecorationsPlugin(searchQuery = "") {
274274
pendingFocusLossRebuild = false;
275275

276276
constructor(view: EditorView) {
277-
this.searchMatches = collectSearchMatches(
278-
view.state.doc.toString(),
279-
searchQuery,
280-
);
277+
this.searchMatches = searchQuery
278+
? collectSearchMatches(view.state.doc.toString(), searchQuery)
279+
: [];
281280
const { atomicRanges, decorations } = buildDecorations(
282281
view,
283282
this.searchMatches,
@@ -346,10 +345,9 @@ export function markdownDecorationsPlugin(searchQuery = "") {
346345

347346
rebuildFromUpdate(update: ViewUpdate) {
348347
if (update.docChanged) {
349-
this.searchMatches = collectSearchMatches(
350-
update.state.doc.toString(),
351-
searchQuery,
352-
);
348+
this.searchMatches = searchQuery
349+
? collectSearchMatches(update.state.doc.toString(), searchQuery)
350+
: [];
353351
}
354352

355353
if (isEditorDebugEnabled()) {

0 commit comments

Comments
 (0)