Created
May 22, 2024 02:51
-
-
Save benatkin/b71f7243090b36975f4103cfe7d28d24 to your computer and use it in GitHub Desktop.
Revisions
-
benatkin created this gist
May 22, 2024 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,328 @@ # Code Edit `notebook.json` ```json { "bundleFiles": [ ["codemirror-bundle.md", "codemirror-bundle.js"] ] } ``` This sets up CodeMirror. It supports getting and setting the text, and sends the `code-input` event when the text is edited. The filetype can be changed dynamically. `code-edit.js` ```js const {tags} = window.CodeMirrorModules['@lezer/highlight']; const {HighlightStyle} = window.CodeMirrorModules['@codemirror/language']; const {EditorView} = window.CodeMirrorModules['@codemirror/view']; const myTheme = EditorView.theme({ "&": { color: "#FFFF33", // Bright yellow text backgroundColor: "#FF6600", // Bright orange background }, ".cm-content": { caretColor: "#FFFF33", // Bright yellow caret }, "&.cm-focused .cm-cursor": { borderLeftColor: "#FFFF33", // Bright yellow cursor }, "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection": { backgroundColor: "#FF9933", // Light orange selection background }, ".cm-activeLine": { backgroundColor: "#FF8533", // Slightly darker orange for active line }, ".cm-gutters": { backgroundColor: "#FF6600", // Bright orange background for gutters color: "#FFFF66", // Bright yellow for line numbers border: "none", }, ".cm-tooltip": { border: "1px solid #FFFF66", backgroundColor: "#FF6600", color: "#FFFF33", }, ".cm-tooltip.cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { backgroundColor: "#FF8533", color: "#FFFF33", }, }, }, {dark: true}); const myHighlightStyle = HighlightStyle.define([ { tag: tags.keyword, color: "#FF4500" }, // Red-orange keywords { tag: tags.string, color: "#FFFF66" }, // Light yellow strings { tag: tags.comment, color: "#66FF66", fontStyle: "italic" }, // Green comments { tag: tags.function(tags.variableName), color: "#66CCFF" }, // Light blue functions { tag: tags.number, color: "#FF66CC" }, // Pink numbers { tag: tags.variableName, color: "#FFFF33" }, // Brighter yellow variable names { tag: tags.operator, color: "#FF66FF" }, // Magenta operators { tag: tags.meta, color: "#66FFFF" }, // Cyan meta { tag: tags.attributeName, color: "#FFD700" }, // Gold attribute names { tag: tags.className, color: "#FF6600" }, // Orange class names { tag: tags.typeName, color: "#66FFCC" }, // Mint green type names { tag: tags.tagName, color: "#FF4500" }, // Red-orange tag names { tag: tags.bracket, color: "#FFFF66" }, // Light yellow brackets { tag: tags.angleBracket, color: "#FFCC66" }, // Peach angle brackets { tag: tags.link, color: "#66CCFF" }, // Light blue links { tag: tags.heading, color: "#FFCC00", fontWeight: "bold" }, // Dark yellow headings { tag: tags.emphasis, color: "#FFFF33", fontStyle: "italic" }, // Brighter yellow italic { tag: tags.strong, color: "#FFFF33", fontWeight: "bold" }, // Brighter yellow bold { tag: tags.strikethrough, color: "#FFFF33", textDecoration: "line-through" }, // Brighter yellow strikethrough { tag: tags.attributeValue, color: "#FFD700" }, // Gold attribute values { tag: tags.definition(tags.typeName), color: "#FFD700" }, // Gold type definitions { tag: tags.definition(tags.className), color: "#FF6600" }, // Orange class definitions ]); console.log(myHighlightStyle) export class CodeEdit extends HTMLElement { constructor() { super() this.attachShadow({mode: 'open'}) } connectedCallback() { this.shadowRoot.adoptedStyleSheets = [this.constructor.styleSheet] this.initEditor() } static css = ` :host { display: flex; flex-direction: column; align-items: stretch; flex-grow: 1; background-color: #fff; height: 100%; } ` static get styleSheet() { if (this._styleSheet === undefined) { this._styleSheet = new CSSStyleSheet() this._styleSheet.replaceSync(this.css) } return this._styleSheet } set value(value) { if (this.view) { this.view.dispatch({changes: { from: 0, to: this.view.state.doc.length, insert: value }}) } else { this._value = value } } get value() { if (this.view) { return this.view.state.doc.toString() } else { return this._value ?? '' } } set fileType(value) { this._fileType = value if (this.view) { const langPlugins = this.langPlugins this.view.dispatch({ effects: this.languageCompartment.reconfigure(langPlugins) }) } } get fileType() { return this._fileType } get langPlugins() { const cmView = window.CodeMirrorModules['@codemirror/view'] const cmState = window.CodeMirrorModules['@codemirror/state'] const cmLanguage = window.CodeMirrorModules['@codemirror/language'] const cmJavaScript = window.CodeMirrorModules['@codemirror/lang-javascript'] const cmCss = window.CodeMirrorModules['@codemirror/lang-css'] const cmHtml = window.CodeMirrorModules['@codemirror/lang-html'] const cmJson = window.CodeMirrorModules['@codemirror/lang-json'] const cmMarkdown = window.CodeMirrorModules['@codemirror/lang-markdown'] const langPlugins = [] if (['js', 'javascript'].includes(this.fileType)) { langPlugins.push(cmJavaScript.javascriptLanguage) } else if (this.fileType === 'css') { langPlugins.push(cmCss.cssLanguage) } else if (this.fileType === 'html') { langPlugins.push(cmHtml.htmlLanguage) } else if (this.fileType === 'json') { langPlugins.push(cmJson.jsonLanguage) } else if (this.fileType === 'md') { const codeLanguages = [ cmLanguage.LanguageDescription.of({ name: 'javascript', alias: ['js'], async load() { return new cmLanguage.LanguageSupport(cmJavaScript.javascriptLanguage) }, }), cmLanguage.LanguageDescription.of({ name: 'css', async load() { return new cmLanguage.LanguageSupport(cmCss.cssLanguage) }, }), cmLanguage.LanguageDescription.of({ name: 'json', async load() { return new cmLanguage.LanguageSupport(cmJson.jsonLanguage) }, }), cmLanguage.LanguageDescription.of({ name: 'html', async load() { const javascript = new cmLanguage.LanguageSupport(cmJavaScript.javascriptLanguage) const css = new cmLanguage.LanguageSupport(cmCss.cssLanguage) return new cmLanguage.LanguageSupport(cmHtml.htmlLanguage, [css, javascript]) }, }), ] const { language, support } = cmMarkdown.markdown({codeLanguages, addKeymap: false}) const markdownSupport = new cmLanguage.LanguageSupport( language, [...support, cmState.Prec.high(cmView.keymap.of(cmMarkdown.markdownKeymap))] ) langPlugins.push( markdownSupport ) } return langPlugins } initEditor() { const cmView = window.CodeMirrorModules['@codemirror/view'] const cmState = window.CodeMirrorModules['@codemirror/state'] const cmLanguage = window.CodeMirrorModules['@codemirror/language'] const cmCommands = window.CodeMirrorModules['@codemirror/commands'] const cmAutocomplete = window.CodeMirrorModules['@codemirror/autocomplete'] const cmSearch = window.CodeMirrorModules['@codemirror/search'] const cmLint = window.CodeMirrorModules['@codemirror/lint'] this.languageCompartment = new cmState.Compartment() const langPlugins = this.langPlugins const basicSetup = [ cmView.lineNumbers(), cmView.highlightActiveLineGutter(), cmView.highlightSpecialChars(), cmCommands.history(), cmLanguage.foldGutter(), cmView.drawSelection(), cmView.dropCursor(), cmState.EditorState.allowMultipleSelections.of(true), cmLanguage.indentOnInput(), cmLanguage.bracketMatching(), cmAutocomplete.closeBrackets(), cmAutocomplete.autocompletion(), cmView.rectangularSelection(), cmView.crosshairCursor(), cmView.highlightActiveLine(), cmSearch.highlightSelectionMatches(), cmLanguage.syntaxHighlighting(myHighlightStyle), cmView.keymap.of([ ...cmAutocomplete.closeBracketsKeymap, ...cmCommands.defaultKeymap, ...cmSearch.searchKeymap, ...cmCommands.historyKeymap, ...cmLanguage.foldKeymap, ...cmAutocomplete.completionKeymap, ...cmLint.lintKeymap, ]), ] const viewTheme = cmView.EditorView.theme({ '&': {flexGrow: '1', height: '100%'}, '.cm-scroller': {overflow: 'auto'} }) this.view = new cmView.EditorView({ doc: this._value ?? '', extensions: [ ...basicSetup, this.languageCompartment.of(langPlugins), myTheme, cmView.EditorView.updateListener.of(e => { if (e.docChanged) { this.dispatchEvent(new CustomEvent( 'code-input', {bubbles: true, composed: true} )) } }), ], root: this.shadowRoot, }) this.shadowRoot.append(this.view.dom) } focus() { this.view.focus() } } ``` `app-view.js` ```js export class AppView extends HTMLElement { constructor() { super() this.attachShadow({mode: 'open'}) } connectedCallback() { const globalStyle = document.createElement('style') globalStyle.textContent = ` body { margin: 0; padding: 0; } html { box-sizing: border-box; } *, *:before, *:after { box-sizing: inherit; } ` document.head.append(globalStyle) const style = document.createElement('style') style.textContent = ` ` this.shadowRoot.append(style) const codeEdit = document.createElement('code-edit') codeEdit.fileType = 'js' codeEdit.value = `const x = 9` this.shadowRoot.append(codeEdit) } } ``` `app.js` ```js import {CodeEdit} from '/code-edit.js' import {AppView} from '/app-view.js' customElements.define('code-edit', CodeEdit) customElements.define('app-view', AppView) async function setup() { const appView = document.createElement('app-view') document.body.append(appView) } await setup() ```