Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from "@codemirror/autocomplete";import { defaultKeymap, indentWithTab, toggleComment } from "@codemirror/commands";import { bracketMatching, foldGutter, foldKeymap, HighlightStyle, indentOnInput, LanguageDescription, syntaxHighlighting,} from "@codemirror/language";import { languages } from "@codemirror/language-data";import { highlightSelectionMatches, searchKeymap, selectNextOccurrence } from "@codemirror/search";import { Compartment, EditorState, type Extension } from "@codemirror/state";import { crosshairCursor, drawSelection, dropCursor, EditorView, highlightActiveLine, highlightActiveLineGutter, highlightSpecialChars, keymap, lineNumbers, rectangularSelection, ViewPlugin,} from "@codemirror/view";import { tags as t } from "@lezer/highlight";import { CollabEditorElement } from "../collab/editor.ts";
/** Loads the language for `filename` after mount; `@codemirror/language-data` splits each language into its own chunk. */function languageFor(filename: string): Extension { const description = LanguageDescription.matchFilename(languages, filename); if (!description) return []; const compartment = new Compartment(); return [ compartment.of([]), ViewPlugin.define((view) => { let live = true; description.load().then((support) => { if (live) view.dispatch({ effects: compartment.reconfigure(support) }); }); return { destroy() { live = false; }, }; }), ];}
const tone = (light: string, dark: string) => `light-dark(${light}, ${dark})`;
const highlightStyle = HighlightStyle.define([ { tag: [t.keyword, t.modifier, t.operatorKeyword, t.controlKeyword], color: tone("#a626a4", "#c678dd") }, { tag: [t.string, t.special(t.string), t.regexp], color: tone("#50a14f", "#98c379") }, { tag: [t.number, t.bool, t.null, t.atom], color: tone("#986801", "#d19a66") }, { tag: [t.comment, t.meta], color: tone("#8e8e93", "#7f848e"), fontStyle: "italic" }, { tag: [t.function(t.variableName), t.function(t.propertyName)], color: tone("#4078f2", "#61afef") }, { tag: [t.typeName, t.className, t.namespace], color: tone("#c18401", "#e5c07b") }, { tag: [t.propertyName, t.attributeName], color: tone("#e45649", "#e06c75") }, { tag: [t.tagName, t.angleBracket], color: tone("#e45649", "#e06c75") }, { tag: t.definition(t.variableName), color: tone("#1d1d1f", "#e8e8ea") }, { tag: [t.heading, t.strong], fontWeight: "700" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.invalid, color: tone("#e5484d", "#ff6369") },]);
// @nibling/web's semantic tokens (packages/web/src/tokens/semantic.css), so the editor matches the// rest of the app chrome. The fallbacks keep <nib-code> usable standalone, outside that design system.const fg = "var(--nib-color-text, light-dark(#1d1d1f, #e8e8ea))";const muted = "var(--nib-color-text-muted, light-dark(#8e8e93, #6e6e73))";const background = "var(--nib-color-canvas, light-dark(#ffffff, #1b1b1d))";const hover = "var(--nib-color-hover, light-dark(#f5f5f7, #232326))";const selected = "var(--nib-color-selected, light-dark(#d7e6ff, #2c3e5c))";
const theme = EditorView.theme({ "&": { height: "100%", color: fg, backgroundColor: background, font: "var(--nib-font-code, 13px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)", colorScheme: "light dark", }, "&.cm-focused": { outline: "none" }, // A narrow pane (the host's own size, not the viewport) trims this to keep text room. ".cm-content": { padding: "var(--nib-code-padding, 8px 0)" }, // The gutter is sticky, so it needs a background for code scrolled under it. ".cm-gutters": { backgroundColor: background, border: "none", color: muted }, ".cm-lineNumbers .cm-gutterElement": { padding: "0 12px 0 16px" }, ".cm-activeLine": { backgroundColor: hover }, ".cm-activeLineGutter": { backgroundColor: "transparent", color: fg }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground": { backgroundColor: selected, }, ".cm-matchingBracket": { backgroundColor: hover, outline: "none" }, ".cm-selectionMatch": { backgroundColor: hover }, // CodeMirror's own dark-mode cursor color only applies under its `&dark` selector, which needs // `EditorView.theme(..., {dark: true})`; this editor picks its palette through `light-dark()` // instead, so the cursor needs its own color here or it stays CodeMirror's default black. ".cm-cursor, .cm-cursor-primary": { borderLeftColor: fg }, ".cm-foldGutter .cm-gutterElement": { padding: "0 6px", cursor: "pointer" },});
/** * `<nib-code filename="main.ts">`: a code editor over a `Doc`. * The language comes from `filename`, the same way the kernel would name the file. */export class NibCodeElement extends CollabEditorElement { static observedAttributes = ["filename"];
get filename(): string { return this.getAttribute("filename") ?? ""; } set filename(filename: string) { this.setAttribute("filename", filename); }
attributeChangedCallback(): void { this.remount(); }
protected extensions(): Extension { return [ lineNumbers(), highlightActiveLineGutter(), foldGutter(), highlightSpecialChars(), drawSelection(), dropCursor(), EditorState.allowMultipleSelections.of(true), indentOnInput(), syntaxHighlighting(highlightStyle), bracketMatching(), closeBrackets(), autocompletion(), rectangularSelection(), crosshairCursor(), highlightActiveLine(), highlightSelectionMatches(), keymap.of([ ...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...foldKeymap, ...completionKeymap, { key: "Mod-/", run: toggleComment }, { key: "Mod-d", run: selectNextOccurrence }, indentWithTab, ]), languageFor(this.filename), // The accessible name is the filename, same as the textarea it replaces; the rest turns off // every platform's text assistance on the contenteditable, the way `codeField` does for a field. EditorView.contentAttributes.of({ "aria-label": this.filename, autocorrect: "off", autocapitalize: "off", spellcheck: "false", writingsuggestions: "false", }), theme, ]; }}
declare global { interface HTMLElementTagNameMap { "nib-code": NibCodeElement; }}
if (!customElements.get("nib-code")) customElements.define("nib-code", NibCodeElement);