Skip to content

EditorView ​

EditorView mounts an editor, attaches the language features of a project to it, and draws their cards. It is the component most apps need.

tsx
<EditorView
    engine={engine}
    options={{ text, language: 'typescript', path: '/shop/src/order.ts', theme: 'github-light' }}
    project={project}
    uri="file:///shop/src/order.ts"
    languageId="typescript"
    onMount={(editor, language) => {
        const stop = editor.onSave(() => save(editor.getText()));
        return stop;
    }}
    className="h-96"
/>
PropTypeMeaning
engineEditorEngineThe engine that mounts the editor
optionsEditorOptionsThe mount options
projectProjectLanguageOptional; without it, or without uri or languageId, the editor has no features
uristringThe document's file: URI
languageIdstringThe LSP language id, such as typescript or php
onMount(editor, language) => void | (() => void)Called after mounting with the Editor and the EditorLanguage, or null; the function it returns runs before the editor goes
contextMenuItems(language) => ReactNodeRows of the app in the context menu
className, refOn the outer element, which needs a height

A change of engine, options, project, uri, languageId or onMount mounts the editor again. On unmount it runs the cleanup of onMount, disposes the EditorLanguage and then the editor. It never disposes the project.

EditorLanguage ​

new EditorLanguage(project, editor, uri, languageId, timers?) attaches every feature to an editor that the app mounted itself. It acquires the document from the project and holds the features as fields: completion, signature, snippets, hover, diagnostics, highlights, navigation, peek, pick, symbolPicker, rename, codeActions, contextMenu, history, definitionLink, symbols and codeVision. Semantic tokens, inlay hints, folds and selection ranges run without a field.

ts
const editor = engine.mount(element, options);
const language = new EditorLanguage(project, editor, uri, 'typescript');

await language.document.ready;

// Later, in this order:
language.dispose();
editor.dispose();

Attach one EditorLanguage per editor. EditorView already makes one, so do not attach a second to its editor. language.goTo(location) and language.jump(position) move the caret and record where it was; visit(place) moves it without a record. language.onDispose(callback) runs a callback when the language goes.

LanguagePopups ​

<LanguagePopups language={language} contextMenuItems={...} /> draws every card a feature opens over the editor: hover, suggestions, signature help, the pick list, rename, peek, the symbol picker, the context menu and the authors card. EditorView renders it; an app that mounts by hand renders it next to the editor. Each card is placed from the screen position of its character and placed again when the editor scrolls or resizes.

createHolder<T>() is the small store EditorView uses to hand the EditorLanguage it created to React: get, set and subscribe, for useSyncExternalStore.

AnchoredPopup ​

AnchoredPopup is the card all of them stand in: a layer of the page itself, placed next to a rect from rectAt and kept inside the window. It renders into document.body, so it is drawn at the page's scale even when the editor sits inside a scaled canvas.

tsx
import { useEffect, useReducer, useState } from 'react';
import { AnchoredPopup } from '@adecore/editor-react';
import { formatNumber } from '@adecore/ui/format';
import { EditorFrame } from '../shared/editor-frame.tsx';
import { SAMPLE, useEditor } from '../shared/editor.ts';

const OPTIONS = { text: SAMPLE, language: 'typescript', theme: 'demo', line: 5, column: 9 } as const;

/* A card that follows the caret: click in the editor, then move the caret or scroll. */
export default function AnchoredPopupDemo() {
    const { host, editor } = useEditor(OPTIONS);
    const [, redraw] = useReducer((count: number) => count + 1, 0);
    const [shown, setShown] = useState(false);

    useEffect(() => {
        if (editor === null) {
            return;
        }
        const stops = [
            editor.onCaret(() => {
                setShown(true);
                redraw();
            }),
            editor.onViewChange(redraw),
            editor.onBlur(() => setShown(false))
        ];
        return () => stops.forEach((stop) => stop());
    }, [editor]);

    const caret = shown ? (editor?.getCaret() ?? null) : null;
    const rect = caret === null ? null : editor!.rectAt(caret);

    return (
        <>
            <EditorFrame host={host} />
            {caret !== null && rect !== null && (
                <AnchoredPopup rect={rect} className="px-2.5 py-1.5 text-xs">
                    Line {formatNumber(caret.line + 1)}, column {formatNumber(caret.character + 1)}
                </AnchoredPopup>
            )}
        </>
    );
}
PropMeaning
rectThe box of the character, in page pixels
placementPlaceOptions: prefer 'above' or 'below', gap and margin
className, childrenThe card's content
onPointerEnter, onPointerLeaveFor a card that stays while the pointer is on it

Render it again when the rect changes; it measures and places itself on every render.

EditorRenderingProvider ​

Code in hover cards and peeks is plain text unless an EditorRenderingProvider above the editors gives it colors. Its value is an EditorRendering: the syntax theme and an optional highlight(code, language, theme) that resolves to HTML, such as Shiki's codeToHtml. The HTML is inserted as it is, so the highlighter must be one the app trusts.

tsx
<EditorRenderingProvider value={{ theme: 'github-light', highlight: (code, lang, theme) => highlighter.codeToHtml(code, { lang, theme }) }}>
    <App />
</EditorRenderingProvider>

Licenses and third-party notices are listed in each package.