mirror of
https://github.com/querke/obsidian-rich-text-editor.git
synced 2026-07-22 06:33:09 +00:00
fix: better performance with code blocks
This commit is contained in:
parent
f64812c8af
commit
52b40f22f6
8 changed files with 2513 additions and 20 deletions
51
CLAUDE.md
Normal file
51
CLAUDE.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Obsidian plugin submission warnings
|
||||
|
||||
Obsidian's plugin review tooling (source code + CSS lint + dependency scan) flags
|
||||
warnings on this repo. Track known ones here so we don't waste time
|
||||
re-investigating warnings that are either already fixed or accepted as
|
||||
unavoidable trade-offs.
|
||||
|
||||
**Update this list every time a warning is fixed or a new one appears** —
|
||||
remove fixed entries, add new ones with the same reasoning format.
|
||||
|
||||
## CSS lint — accepted, not fixable without changing behavior
|
||||
|
||||
- `display: contents` (`src/mdxeditor.css`: `.rte-blocktype-select`,
|
||||
`.callout-title-host`, `.callout-toolbar-icon`) — "only partially supported
|
||||
by Obsidian 1.4.5". Required to make these wrapper elements layout-transparent
|
||||
(their children need to act as direct flex children of the surrounding
|
||||
toolbar/callout). Removing it breaks the layout.
|
||||
- `text-decoration` multi-value usages (`_underlineStrikethrough_1tncs_38`,
|
||||
`.table-formula`) — "only partially supported by Obsidian 1.4.5". Needed for
|
||||
combined underline+strikethrough text and the dotted underline on table
|
||||
formulas. Single-keyword usages (`underline`, `none`, `line-through`) are not
|
||||
flagged, only the multi-value ones.
|
||||
- `!important` (`src/mdxeditor.css`, mobile link-popover positioning) —
|
||||
overrides an inline `transform` style Radix sets at runtime; specificity
|
||||
alone can't beat an inline style.
|
||||
- `:has()` (`src/mdxeditor.css`, same mobile link-popover rule) — scopes the
|
||||
`!important` override to just the link popover, not other popups sharing
|
||||
`.mdxeditor-popup-container`.
|
||||
|
||||
These four are documented inline in `src/mdxeditor.css` with the same
|
||||
reasoning. Don't "fix" them by deleting the declarations.
|
||||
|
||||
## Dependency vulnerabilities
|
||||
|
||||
- `diff` (jsdiff DoS in `parsePatch`/`applyPatch`, GHSA-73rr-hh4g-fpgx) —
|
||||
fixed via `npm audit fix` (non-breaking, transitive dep of `@mdxeditor/editor`
|
||||
via `unidiff`/`uvu`).
|
||||
- `js-yaml` (quadratic-complexity DoS via merge keys, GHSA-h67p-54hq-rp68) —
|
||||
**not fixed**. Only resolvable by upgrading `@mdxeditor/editor` to `4.0.4`,
|
||||
which is a breaking major-version change to the plugin's core editor
|
||||
dependency. Needs a deliberate upgrade + regression pass, not a drive-by fix.
|
||||
|
||||
## Source code warnings
|
||||
|
||||
Fixed:
|
||||
- Unnecessary type assertions in `main.ts` (`editorCheckCallback`/
|
||||
`checkCallback` `.call()` results) — removed; the callback types already
|
||||
produce `boolean | void` without the `as` assertion.
|
||||
- `'+' operation on unknown` in `src/RichTextOverlay.tsx` (Notice message
|
||||
built from a caught `e`) — narrowed with
|
||||
`e instanceof Error ? e.message : String(e)`.
|
||||
|
|
@ -69,6 +69,10 @@ const context = await esbuild.context({
|
|||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
// Use production React even in dev builds: the dev variant's render
|
||||
// instrumentation (logComponentRender, createTask, runWithFiberInDEV)
|
||||
// dominates performance profiles and hides the plugin's real costs.
|
||||
define: { "process.env.NODE_ENV": '"production"' },
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
|
|
|
|||
8
main.ts
8
main.ts
|
|
@ -218,14 +218,12 @@ export default class RichTextPlugin extends Plugin {
|
|||
// behaviour so the standard editor search still works.
|
||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (original.editorCheckCallback && view) {
|
||||
// `.call` is typed `any` here (strictBindCallApply is off), so
|
||||
// annotate the result to keep the return type sound.
|
||||
const res = original.editorCheckCallback.call(
|
||||
cmd,
|
||||
checking,
|
||||
view.editor,
|
||||
view,
|
||||
) as boolean | void;
|
||||
);
|
||||
return checking ? (res ?? false) : true;
|
||||
}
|
||||
if (original.editorCallback && view) {
|
||||
|
|
@ -235,9 +233,7 @@ export default class RichTextPlugin extends Plugin {
|
|||
return true;
|
||||
}
|
||||
if (original.checkCallback) {
|
||||
const res = original.checkCallback.call(cmd, checking) as
|
||||
| boolean
|
||||
| void;
|
||||
const res = original.checkCallback.call(cmd, checking);
|
||||
return checking ? (res ?? false) : true;
|
||||
}
|
||||
if (original.callback) {
|
||||
|
|
|
|||
19
package-lock.json
generated
19
package-lock.json
generated
|
|
@ -14,6 +14,7 @@
|
|||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@lexical/link": "^0.35.0",
|
||||
"@lexical/list": "^0.35.0",
|
||||
"@lexical/react": "^0.35.0",
|
||||
"@lexical/utils": "^0.35.0",
|
||||
"@mdxeditor/editor": "^3.47.0",
|
||||
"@mdxeditor/gurx": "^1.2.4",
|
||||
|
|
@ -1619,9 +1620,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mdxeditor/editor": {
|
||||
"version": "3.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@mdxeditor/editor/-/editor-3.52.3.tgz",
|
||||
"integrity": "sha512-hKPsNPx7cjxXPeiir0LjU2ZN5bWn7dWVxV8CaCddLztGc36e921ETNpu1AXo0+K7pxc7eBt/ExSw0WDOOk2Ibw==",
|
||||
"version": "3.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@mdxeditor/editor/-/editor-3.55.0.tgz",
|
||||
"integrity": "sha512-ziJY12OQVrrdAcWAwG8njV7gPy8orEIodGdFTXpQVlIOjdV/FOIHMcETMc5XXfON3hw3uO1CcHDb3TZQQSgfqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.2.4",
|
||||
|
|
@ -3691,9 +3692,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
|
||||
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz",
|
||||
"integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
|
|
@ -7716,9 +7717,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
// ReactView.tsx
|
||||
import { oneDark } from "@codemirror/theme-one-dark";
|
||||
import { LanguageDescription } from "@codemirror/language";
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
foldGutter,
|
||||
LanguageDescription,
|
||||
syntaxHighlighting,
|
||||
} from "@codemirror/language";
|
||||
import { languages as codeLanguageData } from "@codemirror/language-data";
|
||||
import { EditorState, type Extension } from "@codemirror/state";
|
||||
import { EditorView, lineNumbers } from "@codemirror/view";
|
||||
import { basicLight } from "cm6-theme-basic-light";
|
||||
import { csharp } from "@replit/codemirror-lang-csharp";
|
||||
import {
|
||||
BlockTypeSelect,
|
||||
BoldItalicUnderlineToggles,
|
||||
codeBlockPlugin,
|
||||
CodeBlockEditorDescriptor,
|
||||
CodeBlockEditorProps,
|
||||
codeMirrorPlugin,
|
||||
CodeMirrorEditor,
|
||||
CodeToggle,
|
||||
CreateLink,
|
||||
directivesPlugin,
|
||||
|
|
@ -108,6 +119,118 @@ const CODE_BLOCK_LABEL_TO_ID = new Map(
|
|||
Object.entries(CODE_BLOCK_LANGUAGES).map(([id, label]) => [label, id]),
|
||||
);
|
||||
|
||||
// Mounting MDXEditor's full CodeMirror editor per code block dominates load
|
||||
// time on documents with many blocks: its basicSetup pulls in keymaps,
|
||||
// history, autocompletion, search, and update listeners for every instance.
|
||||
// Until the user clicks into a block (or keyboard-navigates into it, which
|
||||
// fires the focusEmitter), show a read-only CodeMirror view instead — same
|
||||
// themes, gutters, and language grammar, so it looks identical, but with none
|
||||
// of the editing machinery.
|
||||
function LazyCodeBlockEditor(props: CodeBlockEditorProps) {
|
||||
// Empty blocks are freshly inserted ones — mount eagerly so typing works
|
||||
// right away (the insert's focus event fires before we could subscribe).
|
||||
const [active, setActive] = useState(props.code === "");
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
props.focusEmitter.subscribe(() => setActive(true));
|
||||
}, [props.focusEmitter]);
|
||||
|
||||
// The real editor builds its EditorView asynchronously (it awaits the
|
||||
// language grammar), so poll a few frames for the content element.
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
let tries = 0;
|
||||
const tryFocus = () => {
|
||||
const content =
|
||||
wrapperRef.current?.querySelector<HTMLElement>(".cm-content");
|
||||
if (content) {
|
||||
content.focus();
|
||||
} else if (++tries < 20) {
|
||||
requestAnimationFrame(tryFocus);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(tryFocus);
|
||||
}, [active]);
|
||||
|
||||
useEffect(() => {
|
||||
if (active) return;
|
||||
const el = wrapperRef.current;
|
||||
if (!el) return;
|
||||
let view: EditorView | null = null;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
// Mirror the extension order of MDXEditor's CodeMirrorEditor
|
||||
// (codeMirrorExtensions first, then basicLight) so themes resolve
|
||||
// with the same precedence and the swap on click is seamless.
|
||||
const extensions: Extension[] = [
|
||||
...(activeDocument.body.classList.contains("theme-dark")
|
||||
? [oneDark]
|
||||
: []),
|
||||
basicLight,
|
||||
lineNumbers(),
|
||||
foldGutter(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.editable.of(false),
|
||||
EditorState.readOnly.of(true),
|
||||
];
|
||||
if (props.language !== "") {
|
||||
const languageData = codeLanguageData.find(
|
||||
(l) =>
|
||||
l.name === props.language ||
|
||||
l.alias.includes(props.language) ||
|
||||
l.extensions.includes(props.language),
|
||||
);
|
||||
if (languageData) {
|
||||
try {
|
||||
extensions.push(
|
||||
(await languageData.load()).extension,
|
||||
);
|
||||
} catch {
|
||||
// Grammar failed to load; render as plain text.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cancelled) return;
|
||||
view = new EditorView({
|
||||
parent: el,
|
||||
state: EditorState.create({ doc: props.code, extensions }),
|
||||
});
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
view?.destroy();
|
||||
};
|
||||
}, [active, props.code, props.language]);
|
||||
|
||||
if (active) {
|
||||
return (
|
||||
<div ref={wrapperRef} style={{ display: "contents" }}>
|
||||
<CodeMirrorEditor {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className="rte-lazy-code-block"
|
||||
data-code-lang={props.language || undefined}
|
||||
onClick={() => setActive(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const lazyCodeBlockDescriptor: CodeBlockEditorDescriptor = {
|
||||
// codeMirrorPlugin registers its descriptor at priority 1; win the match.
|
||||
priority: 2,
|
||||
// Same match as the CodeMirror descriptor, so exactly the blocks it would
|
||||
// claim get the lazy treatment.
|
||||
match: (language) => (language ?? "") in CODE_BLOCK_LANGUAGES,
|
||||
Editor: LazyCodeBlockEditor,
|
||||
};
|
||||
|
||||
// MDXEditor resolves a code block's syntax highlighting by matching the fence
|
||||
// language against @codemirror/language-data's `languages` array and calling
|
||||
// `.load()` on the match. C#'s built-in entry resolves to a legacy
|
||||
|
|
@ -1341,7 +1464,12 @@ export const RichTextEditor = forwardRef<RichTextEditorRef, Props>(
|
|||
linkPlugin(),
|
||||
tagLinkPlugin(),
|
||||
linkDialogPlugin({ showLinkTitleField: false }),
|
||||
codeBlockPlugin({ defaultCodeBlockLanguage: "js" }),
|
||||
codeBlockPlugin({
|
||||
defaultCodeBlockLanguage: "js",
|
||||
codeBlockEditorDescriptors: [
|
||||
lazyCodeBlockDescriptor,
|
||||
],
|
||||
}),
|
||||
codeMirrorPlugin({
|
||||
codeBlockLanguages: CODE_BLOCK_LANGUAGES,
|
||||
|
||||
|
|
|
|||
|
|
@ -705,7 +705,10 @@ export class RichTextOverlay {
|
|||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
new Notice("Rename failed: " + e);
|
||||
new Notice(
|
||||
"Rename failed: " +
|
||||
(e instanceof Error ? e.message : String(e)),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -224,6 +224,14 @@ div[class*="_dialogContent_"] input {
|
|||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Read-only stand-in for a not-yet-mounted CodeMirror editor (see
|
||||
LazyCodeBlockEditor). It hosts a real .cm-editor, so the usual CodeMirror
|
||||
rules apply; this only anchors the language flair. */
|
||||
.mdxeditor .rte-lazy-code-block {
|
||||
position: relative;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/** Diff viewer */
|
||||
|
||||
.mdxeditor .cm-mergeView .cm-scroller,
|
||||
|
|
@ -753,7 +761,8 @@ mark {
|
|||
/* Non-interactive language flair, shown in the corner of every code block
|
||||
(mirrors Obsidian's `code-block-flair`). The `data-code-lang` attribute is
|
||||
set in RichTextEditor.tsx from the block's selected language. */
|
||||
[class*="_codeMirrorWrapper_"][data-code-lang]::before {
|
||||
[class*="_codeMirrorWrapper_"][data-code-lang]::before,
|
||||
.rte-lazy-code-block[data-code-lang]::before {
|
||||
content: attr(data-code-lang);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
|
|||
2303
styles.css
2303
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue