Skip to content

Commit 9b62246

Browse files
luarmrclaude
andcommitted
perf: optimize CodeMirror for large XML configs
Add dynamic performance optimization based on document size: Small documents (≤50 lines): - Line numbers: enabled - Full document rendering Large documents (>50 lines): - Line numbers: disabled (major performance boost) - Viewport rendering (viewportMargin: 10) - Only renders visible lines The editor recreates when crossing the 50-line threshold to apply new settings. This makes editing 1000+ line configs usable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent a11836e commit 9b62246

File tree

1 file changed

+49
-35
lines changed
  • web/apps/labelstudio/src/pages/CreateProject/Config

1 file changed

+49
-35
lines changed

web/apps/labelstudio/src/pages/CreateProject/Config/Config.jsx

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,53 @@ const Configurator = ({
420420
if (value === "visual") loadVisual(true);
421421
};
422422

423+
// Calculate line count for dynamic optimization
424+
const lineCount = React.useMemo(() => config.split('\n').length, [config]);
425+
426+
// Memoize autocomplete functions to prevent recreating options on every render
427+
const completeAfter = React.useCallback((cm, pred) => {
428+
if (!pred || pred()) {
429+
setTimeout(() => {
430+
if (!cm.state.completionActive) cm.showHint({ completeSingle: false });
431+
}, 100);
432+
}
433+
return CM.Pass;
434+
}, []);
435+
436+
const completeIfInTag = React.useCallback((cm) => {
437+
return completeAfter(cm, () => {
438+
const token = cm.getTokenAt(cm.getCursor());
439+
440+
if (token.type === "string" && (!/['"]$/.test(token.string) || token.string.length === 1)) return false;
441+
442+
const inner = CM.innerMode(cm.getMode(), token.state).state;
443+
444+
return inner.tagName;
445+
});
446+
}, [completeAfter]);
447+
448+
// Dynamic CodeMirror options based on document size
449+
const dynamicCodeMirrorOptions = React.useMemo(() => {
450+
const isLargeDocument = lineCount > 50;
451+
452+
return {
453+
mode: "xml",
454+
theme: "default",
455+
lineNumbers: !isLargeDocument,
456+
viewportMargin: isLargeDocument ? 10 : Infinity,
457+
extraKeys: {
458+
"'<'": completeAfter,
459+
"' '": completeIfInTag,
460+
"'='": completeIfInTag,
461+
"Ctrl-Space": "autocomplete",
462+
},
463+
hintOptions: { schemaInfo: tags },
464+
};
465+
}, [lineCount, completeAfter, completeIfInTag]);
466+
467+
// Key to force CodeMirror recreation when crossing threshold
468+
const editorKey = React.useMemo(() => lineCount > 50 ? 'large' : 'small', [lineCount]);
469+
423470
const onChange = React.useCallback(
424471
(config) => {
425472
try {
@@ -451,27 +498,6 @@ const Configurator = ({
451498
return res;
452499
};
453500

454-
function completeAfter(cm, pred) {
455-
if (!pred || pred()) {
456-
setTimeout(() => {
457-
if (!cm.state.completionActive) cm.showHint({ completeSingle: false });
458-
}, 100);
459-
}
460-
return CM.Pass;
461-
}
462-
463-
function completeIfInTag(cm) {
464-
return completeAfter(cm, () => {
465-
const token = cm.getTokenAt(cm.getCursor());
466-
467-
if (token.type === "string" && (!/['"]$/.test(token.string) || token.string.length === 1)) return false;
468-
469-
const inner = CM.innerMode(cm.getMode(), token.state).state;
470-
471-
return inner.tagName;
472-
});
473-
}
474-
475501
const extra = (
476502
<p className={configClass.elem("tags-link")}>
477503
Configure the labeling interface with tags.
@@ -504,6 +530,7 @@ const Configurator = ({
504530
{configure === "code" && (
505531
<div className={configClass.elem("code")} style={{ display: configure === "code" ? undefined : "none" }}>
506532
<CodeEditor
533+
key={editorKey}
507534
name="code"
508535
id="edit_code"
509536
value={config}
@@ -512,20 +539,7 @@ const Configurator = ({
512539
detach
513540
border
514541
extensions={["hint", "xml-hint"]}
515-
options={{
516-
mode: "xml",
517-
theme: "default",
518-
lineNumbers: true,
519-
extraKeys: {
520-
"'<'": completeAfter,
521-
// "'/'": completeIfAfterLt,
522-
"' '": completeIfInTag,
523-
"'='": completeIfInTag,
524-
"Ctrl-Space": "autocomplete",
525-
},
526-
hintOptions: { schemaInfo: tags },
527-
}}
528-
// don't close modal with Escape while editing config
542+
options={dynamicCodeMirrorOptions}
529543
onKeyDown={(editor, e) => {
530544
if (e.code === "Escape") e.stopPropagation();
531545
}}

0 commit comments

Comments
 (0)