From 460a8ce66c1ee2b1911592e19edd9d13806a89d7 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 8 Dec 2024 09:32:42 +0100 Subject: [PATCH 1/7] Make Monaco theme follow browser, fully type codeeditor.ts --- web_src/js/features/codeeditor.ts | 100 ++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 33 deletions(-) diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index 93b2042fa920a..72df512cbc3ad 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -1,11 +1,30 @@ import tinycolor from 'tinycolor2'; import {basename, extname, isObject, isDarkTheme} from '../utils.ts'; import {onInputDebounce} from '../utils/dom.ts'; +import type MonacoNamespace from 'monaco-editor'; + +type Monaco = typeof MonacoNamespace; +type IStandaloneCodeEditor = MonacoNamespace.editor.IStandaloneCodeEditor; +type IEditorOptions = MonacoNamespace.editor.IEditorOptions; +type IGlobalEditorOptions = MonacoNamespace.editor.IGlobalEditorOptions; +type ITextModelUpdateOptions = MonacoNamespace.editor.ITextModelUpdateOptions; +type MonacoOpts = IEditorOptions & IGlobalEditorOptions & ITextModelUpdateOptions; + +type EditorConfig = { + indent_style?: 'tab' | 'space', + indent_size?: number, + tab_width?: number, + end_of_line?: 'lf' | 'cr' | 'crlf', + charset?: 'latin1' | 'utf-8' | 'utf-8-bom' | 'utf-16be' | 'utf-16le', + trim_trailing_whitespace?: boolean, + insert_final_newline?: boolean, + root?: boolean, +} -const languagesByFilename = {}; -const languagesByExt = {}; +const languagesByFilename: Record = {}; +const languagesByExt: Record = {}; -const baseOptions = { +const baseOptions: MonacoOpts = { fontFamily: 'var(--fonts-monospace)', fontSize: 14, // https://github.com/microsoft/monaco-editor/issues/2242 guides: {bracketPairs: false, indentation: false}, @@ -15,21 +34,21 @@ const baseOptions = { overviewRulerLanes: 0, renderLineHighlight: 'all', renderLineHighlightOnlyWhenFocus: true, - rulers: false, + rulers: [], scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6}, scrollBeyondLastLine: false, automaticLayout: true, }; -function getEditorconfig(input: HTMLInputElement) { +function getEditorconfig(input: HTMLInputElement): EditorConfig | null { try { - return JSON.parse(input.getAttribute('data-editorconfig')); + return JSON.parse(input.getAttribute('data-editorconfig') ?? ''); } catch { return null; } } -function initLanguages(monaco) { +function initLanguages(monaco: Monaco): void { for (const {filenames, extensions, id} of monaco.languages.getLanguages()) { for (const filename of filenames || []) { languagesByFilename[filename] = id; @@ -40,35 +59,26 @@ function initLanguages(monaco) { } } -function getLanguage(filename) { +function getLanguage(filename: string): string { return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext'; } -function updateEditor(monaco, editor, filename, lineWrapExts) { +function updateEditor(monaco: Monaco, editor: IStandaloneCodeEditor, filename: string, lineWrapExts: string[]) { editor.updateOptions(getFileBasedOptions(filename, lineWrapExts)); const model = editor.getModel(); + if (!model) return; const language = model.getLanguageId(); const newLanguage = getLanguage(filename); if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage); } // export editor for customization - https://github.com/go-gitea/gitea/issues/10409 -function exportEditor(editor) { +function exportEditor(editor: IStandaloneCodeEditor): void { if (!window.codeEditors) window.codeEditors = []; if (!window.codeEditors.includes(editor)) window.codeEditors.push(editor); } -export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, editorOpts: Record) { - const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor'); - - initLanguages(monaco); - let {language, ...other} = editorOpts; - if (!language) language = getLanguage(filename); - - const container = document.createElement('div'); - container.className = 'monaco-editor-container'; - textarea.parentNode.append(container); - +function updateTheme(monaco: Monaco): void { // https://github.com/microsoft/monaco-editor/issues/2427 // also, monaco can only parse 6-digit hex colors, so we convert the colors to that format const styles = window.getComputedStyle(document.documentElement); @@ -80,6 +90,7 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri rules: [ { background: getColor('--color-code-bg'), + token: '', }, ], colors: { @@ -101,6 +112,22 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri 'focusBorder': '#0000', // prevent blue border }, }); +} + +type CreateMonacoOpts = MonacoOpts & {language?: string}; + +export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, opts: CreateMonacoOpts) { + const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor'); + + initLanguages(monaco); + let {language, ...other} = opts; + if (!language) language = getLanguage(filename); + + const container = document.createElement('div'); + container.className = 'monaco-editor-container'; + textarea.parentNode?.append(container); + + updateTheme(monaco); const editor = monaco.editor.create(container, { value: textarea.value, @@ -109,13 +136,20 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri ...other, }); + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + updateTheme(monaco); + }); + monaco.editor.addKeybindingRules([ {keybinding: monaco.KeyCode.Enter, command: null}, // disable enter from accepting code completion ]); const model = editor.getModel(); - model.onDidChangeContent(() => { - textarea.value = editor.getValue({preserveBOM: true}); + model?.onDidChangeContent(() => { + textarea.value = editor.getValue({ + preserveBOM: true, + lineEnding: '', + }); textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure }); @@ -127,13 +161,13 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri return {monaco, editor}; } -function getFileBasedOptions(filename: string, lineWrapExts: string[]) { +function getFileBasedOptions(filename: string, lineWrapExts: string[]): IEditorOptions & IGlobalEditorOptions { return { wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off', }; } -function togglePreviewDisplay(previewable: boolean) { +function togglePreviewDisplay(previewable: boolean): void { const previewTab = document.querySelector('a[data-tab="preview"]'); if (!previewTab) return; @@ -145,19 +179,19 @@ function togglePreviewDisplay(previewable: boolean) { // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active if (previewTab.classList.contains('active')) { const writeTab = document.querySelector('a[data-tab="write"]'); - writeTab.click(); + writeTab?.click(); } } } -export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameInput: HTMLInputElement) { +export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameInput: HTMLInputElement): Promise { const filename = basename(filenameInput.value); const previewableExts = new Set((textarea.getAttribute('data-previewable-extensions') || '').split(',')); const lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(','); - const previewable = previewableExts.has(extname(filename)); + const isPreviewable = previewableExts.has(extname(filename)); const editorConfig = getEditorconfig(filenameInput); - togglePreviewDisplay(previewable); + togglePreviewDisplay(isPreviewable); const {monaco, editor} = await createMonaco(textarea, filename, { ...baseOptions, @@ -175,13 +209,13 @@ export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameIn return editor; } -function getEditorConfigOptions(ec: Record): Record { - if (!isObject(ec)) return {}; +function getEditorConfigOptions(ec: EditorConfig | null): MonacoOpts { + if (!ec || !isObject(ec)) return {}; - const opts: Record = {}; + const opts: MonacoOpts = {}; opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec); if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size); - if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize; + if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || (opts.indentSize as number); if ('max_line_length' in ec) opts.rulers = [Number(ec.max_line_length)]; opts.trimAutoWhitespace = ec.trim_trailing_whitespace === true; opts.insertSpaces = ec.indent_style === 'space'; From 7fd8d2a2d2a29ecb57b972c9ddd191b582546d00 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 8 Dec 2024 09:44:23 +0100 Subject: [PATCH 2/7] don't parse absent attribute --- web_src/js/features/codeeditor.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index 72df512cbc3ad..1e05ba15df428 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -41,8 +41,10 @@ const baseOptions: MonacoOpts = { }; function getEditorconfig(input: HTMLInputElement): EditorConfig | null { + const json = input.getAttribute('data-editorconfig'); + if (!json) return null; try { - return JSON.parse(input.getAttribute('data-editorconfig') ?? ''); + return JSON.parse(json); } catch { return null; } From ed744c1ca11af7c580ae51c69abb18364c7d57db Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 8 Dec 2024 09:59:17 +0100 Subject: [PATCH 3/7] move --- web_src/js/features/codeeditor.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index 1e05ba15df428..291694433295e 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -129,6 +129,9 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri container.className = 'monaco-editor-container'; textarea.parentNode?.append(container); + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + updateTheme(monaco); + }); updateTheme(monaco); const editor = monaco.editor.create(container, { @@ -138,10 +141,6 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri ...other, }); - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { - updateTheme(monaco); - }); - monaco.editor.addKeybindingRules([ {keybinding: monaco.KeyCode.Enter, command: null}, // disable enter from accepting code completion ]); From 8450414ecb5a8d8d01cc1d72bde969aac04a1e96 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 8 Dec 2024 12:11:20 +0100 Subject: [PATCH 4/7] further enhancements --- web_src/js/features/codeeditor.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index 291694433295e..f69e7453b4ee8 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -12,8 +12,8 @@ type MonacoOpts = IEditorOptions & IGlobalEditorOptions & ITextModelUpdateOption type EditorConfig = { indent_style?: 'tab' | 'space', - indent_size?: number, - tab_width?: number, + indent_size?: string | number, // backend emits this as string + tab_width?: string | number, // backend emits this as string end_of_line?: 'lf' | 'cr' | 'crlf', charset?: 'latin1' | 'utf-8' | 'utf-8-bom' | 'utf-16be' | 'utf-16le', trim_trailing_whitespace?: boolean, @@ -215,9 +215,17 @@ function getEditorConfigOptions(ec: EditorConfig | null): MonacoOpts { const opts: MonacoOpts = {}; opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec); - if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size); - if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || (opts.indentSize as number); - if ('max_line_length' in ec) opts.rulers = [Number(ec.max_line_length)]; + + if ('indent_size' in ec) { + opts.indentSize = Number(ec.indent_size); + } + if ('tab_width' in ec) { + opts.tabSize = Number(ec.tab_width) || Number(ec.indent_size); + } + if ('max_line_length' in ec) { + opts.rulers = [Number(ec.max_line_length)]; + } + opts.trimAutoWhitespace = ec.trim_trailing_whitespace === true; opts.insertSpaces = ec.indent_style === 'space'; opts.useTabStops = ec.indent_style === 'tab'; From ba9242d9639113cfd36d0494ec5fe3b40006ea85 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 9 Dec 2024 07:15:44 +0100 Subject: [PATCH 5/7] fix return type --- web_src/js/features/codeeditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index f69e7453b4ee8..f8b04130ce557 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -162,7 +162,7 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri return {monaco, editor}; } -function getFileBasedOptions(filename: string, lineWrapExts: string[]): IEditorOptions & IGlobalEditorOptions { +function getFileBasedOptions(filename: string, lineWrapExts: string[]): MonacoOpts { return { wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off', }; From f6ef9c08664786e52c40286b8b3289bd6f7c1e44 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 9 Dec 2024 17:46:36 +0100 Subject: [PATCH 6/7] add throws --- web_src/js/features/codeeditor.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index f8b04130ce557..6104f141feff6 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -127,7 +127,8 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri const container = document.createElement('div'); container.className = 'monaco-editor-container'; - textarea.parentNode?.append(container); + if (!textarea.parentNode) throw new Error('Parent node absent'); + textarea.parentNode.append(container); window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { updateTheme(monaco); @@ -146,7 +147,8 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri ]); const model = editor.getModel(); - model?.onDidChangeContent(() => { + if (!model) throw new Error('Unable to get editor model'); + model.onDidChangeContent(() => { textarea.value = editor.getValue({ preserveBOM: true, lineEnding: '', From 03298f8c0a3a37e19455f35f6daa6e2181354d41 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 9 Dec 2024 17:56:25 +0100 Subject: [PATCH 7/7] add return types --- web_src/js/features/codeeditor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index 6104f141feff6..62bfccd1393b2 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -65,7 +65,7 @@ function getLanguage(filename: string): string { return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext'; } -function updateEditor(monaco: Monaco, editor: IStandaloneCodeEditor, filename: string, lineWrapExts: string[]) { +function updateEditor(monaco: Monaco, editor: IStandaloneCodeEditor, filename: string, lineWrapExts: string[]): void { editor.updateOptions(getFileBasedOptions(filename, lineWrapExts)); const model = editor.getModel(); if (!model) return; @@ -118,7 +118,7 @@ function updateTheme(monaco: Monaco): void { type CreateMonacoOpts = MonacoOpts & {language?: string}; -export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, opts: CreateMonacoOpts) { +export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, opts: CreateMonacoOpts): Promise<{monaco: Monaco, editor: IStandaloneCodeEditor}> { const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor'); initLanguages(monaco);