mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:45:25 +01:00 
			
		
		
		
	Make Monaco theme follow browser, fully type codeeditor.ts (#32756)
1. Monaco's theme now follows changes in dark/light mode setting, this works via [`MediaQueryList`](https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList)'s [change event](https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList/change_event). 2. Fully type the file, it now passes typescript strict mode.
This commit is contained in:
		
							parent
							
								
									5675efb3e0
								
							
						
					
					
						commit
						57a5e9acf8
					
				| @ -1,11 +1,30 @@ | |||||||
| import tinycolor from 'tinycolor2'; | import tinycolor from 'tinycolor2'; | ||||||
| import {basename, extname, isObject, isDarkTheme} from '../utils.ts'; | import {basename, extname, isObject, isDarkTheme} from '../utils.ts'; | ||||||
| import {onInputDebounce} from '../utils/dom.ts'; | import {onInputDebounce} from '../utils/dom.ts'; | ||||||
|  | import type MonacoNamespace from 'monaco-editor'; | ||||||
| 
 | 
 | ||||||
| const languagesByFilename = {}; | type Monaco = typeof MonacoNamespace; | ||||||
| const languagesByExt = {}; | 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; | ||||||
| 
 | 
 | ||||||
| const baseOptions = { | type EditorConfig = { | ||||||
|  |   indent_style?: 'tab' | 'space', | ||||||
|  |   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, | ||||||
|  |   insert_final_newline?: boolean, | ||||||
|  |   root?: boolean, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const languagesByFilename: Record<string, string> = {}; | ||||||
|  | const languagesByExt: Record<string, string> = {}; | ||||||
|  | 
 | ||||||
|  | const baseOptions: MonacoOpts = { | ||||||
|   fontFamily: 'var(--fonts-monospace)', |   fontFamily: 'var(--fonts-monospace)', | ||||||
|   fontSize: 14, // https://github.com/microsoft/monaco-editor/issues/2242
 |   fontSize: 14, // https://github.com/microsoft/monaco-editor/issues/2242
 | ||||||
|   guides: {bracketPairs: false, indentation: false}, |   guides: {bracketPairs: false, indentation: false}, | ||||||
| @ -15,21 +34,23 @@ const baseOptions = { | |||||||
|   overviewRulerLanes: 0, |   overviewRulerLanes: 0, | ||||||
|   renderLineHighlight: 'all', |   renderLineHighlight: 'all', | ||||||
|   renderLineHighlightOnlyWhenFocus: true, |   renderLineHighlightOnlyWhenFocus: true, | ||||||
|   rulers: false, |   rulers: [], | ||||||
|   scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6}, |   scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6}, | ||||||
|   scrollBeyondLastLine: false, |   scrollBeyondLastLine: false, | ||||||
|   automaticLayout: true, |   automaticLayout: true, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function getEditorconfig(input: HTMLInputElement) { | function getEditorconfig(input: HTMLInputElement): EditorConfig | null { | ||||||
|  |   const json = input.getAttribute('data-editorconfig'); | ||||||
|  |   if (!json) return null; | ||||||
|   try { |   try { | ||||||
|     return JSON.parse(input.getAttribute('data-editorconfig')); |     return JSON.parse(json); | ||||||
|   } catch { |   } catch { | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function initLanguages(monaco) { | function initLanguages(monaco: Monaco): void { | ||||||
|   for (const {filenames, extensions, id} of monaco.languages.getLanguages()) { |   for (const {filenames, extensions, id} of monaco.languages.getLanguages()) { | ||||||
|     for (const filename of filenames || []) { |     for (const filename of filenames || []) { | ||||||
|       languagesByFilename[filename] = id; |       languagesByFilename[filename] = id; | ||||||
| @ -40,35 +61,26 @@ function initLanguages(monaco) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getLanguage(filename) { | function getLanguage(filename: string): string { | ||||||
|   return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext'; |   return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext'; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function updateEditor(monaco, editor, filename, lineWrapExts) { | function updateEditor(monaco: Monaco, editor: IStandaloneCodeEditor, filename: string, lineWrapExts: string[]): void { | ||||||
|   editor.updateOptions(getFileBasedOptions(filename, lineWrapExts)); |   editor.updateOptions(getFileBasedOptions(filename, lineWrapExts)); | ||||||
|   const model = editor.getModel(); |   const model = editor.getModel(); | ||||||
|  |   if (!model) return; | ||||||
|   const language = model.getLanguageId(); |   const language = model.getLanguageId(); | ||||||
|   const newLanguage = getLanguage(filename); |   const newLanguage = getLanguage(filename); | ||||||
|   if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage); |   if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // export editor for customization - https://github.com/go-gitea/gitea/issues/10409
 | // 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) window.codeEditors = []; | ||||||
|   if (!window.codeEditors.includes(editor)) window.codeEditors.push(editor); |   if (!window.codeEditors.includes(editor)) window.codeEditors.push(editor); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, editorOpts: Record<string, any>) { | function updateTheme(monaco: Monaco): void { | ||||||
|   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); |  | ||||||
| 
 |  | ||||||
|   // https://github.com/microsoft/monaco-editor/issues/2427
 |   // 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
 |   // also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
 | ||||||
|   const styles = window.getComputedStyle(document.documentElement); |   const styles = window.getComputedStyle(document.documentElement); | ||||||
| @ -80,6 +92,7 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri | |||||||
|     rules: [ |     rules: [ | ||||||
|       { |       { | ||||||
|         background: getColor('--color-code-bg'), |         background: getColor('--color-code-bg'), | ||||||
|  |         token: '', | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|     colors: { |     colors: { | ||||||
| @ -101,6 +114,26 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri | |||||||
|       'focusBorder': '#0000', // prevent blue border
 |       'focusBorder': '#0000', // prevent blue border
 | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type CreateMonacoOpts = MonacoOpts & {language?: string}; | ||||||
|  | 
 | ||||||
|  | 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); | ||||||
|  |   let {language, ...other} = opts; | ||||||
|  |   if (!language) language = getLanguage(filename); | ||||||
|  | 
 | ||||||
|  |   const container = document.createElement('div'); | ||||||
|  |   container.className = 'monaco-editor-container'; | ||||||
|  |   if (!textarea.parentNode) throw new Error('Parent node absent'); | ||||||
|  |   textarea.parentNode.append(container); | ||||||
|  | 
 | ||||||
|  |   window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { | ||||||
|  |     updateTheme(monaco); | ||||||
|  |   }); | ||||||
|  |   updateTheme(monaco); | ||||||
| 
 | 
 | ||||||
|   const editor = monaco.editor.create(container, { |   const editor = monaco.editor.create(container, { | ||||||
|     value: textarea.value, |     value: textarea.value, | ||||||
| @ -114,8 +147,12 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri | |||||||
|   ]); |   ]); | ||||||
| 
 | 
 | ||||||
|   const model = editor.getModel(); |   const model = editor.getModel(); | ||||||
|  |   if (!model) throw new Error('Unable to get editor model'); | ||||||
|   model.onDidChangeContent(() => { |   model.onDidChangeContent(() => { | ||||||
|     textarea.value = editor.getValue({preserveBOM: true}); |     textarea.value = editor.getValue({ | ||||||
|  |       preserveBOM: true, | ||||||
|  |       lineEnding: '', | ||||||
|  |     }); | ||||||
|     textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure
 |     textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure
 | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @ -127,13 +164,13 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri | |||||||
|   return {monaco, editor}; |   return {monaco, editor}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getFileBasedOptions(filename: string, lineWrapExts: string[]) { | function getFileBasedOptions(filename: string, lineWrapExts: string[]): MonacoOpts { | ||||||
|   return { |   return { | ||||||
|     wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off', |     wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off', | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function togglePreviewDisplay(previewable: boolean) { | function togglePreviewDisplay(previewable: boolean): void { | ||||||
|   const previewTab = document.querySelector<HTMLElement>('a[data-tab="preview"]'); |   const previewTab = document.querySelector<HTMLElement>('a[data-tab="preview"]'); | ||||||
|   if (!previewTab) return; |   if (!previewTab) return; | ||||||
| 
 | 
 | ||||||
| @ -145,19 +182,19 @@ function togglePreviewDisplay(previewable: boolean) { | |||||||
|     // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active
 |     // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active
 | ||||||
|     if (previewTab.classList.contains('active')) { |     if (previewTab.classList.contains('active')) { | ||||||
|       const writeTab = document.querySelector<HTMLElement>('a[data-tab="write"]'); |       const writeTab = document.querySelector<HTMLElement>('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<IStandaloneCodeEditor> { | ||||||
|   const filename = basename(filenameInput.value); |   const filename = basename(filenameInput.value); | ||||||
|   const previewableExts = new Set((textarea.getAttribute('data-previewable-extensions') || '').split(',')); |   const previewableExts = new Set((textarea.getAttribute('data-previewable-extensions') || '').split(',')); | ||||||
|   const lineWrapExts = (textarea.getAttribute('data-line-wrap-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); |   const editorConfig = getEditorconfig(filenameInput); | ||||||
| 
 | 
 | ||||||
|   togglePreviewDisplay(previewable); |   togglePreviewDisplay(isPreviewable); | ||||||
| 
 | 
 | ||||||
|   const {monaco, editor} = await createMonaco(textarea, filename, { |   const {monaco, editor} = await createMonaco(textarea, filename, { | ||||||
|     ...baseOptions, |     ...baseOptions, | ||||||
| @ -175,14 +212,22 @@ export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameIn | |||||||
|   return editor; |   return editor; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getEditorConfigOptions(ec: Record<string, any>): Record<string, any> { | function getEditorConfigOptions(ec: EditorConfig | null): MonacoOpts { | ||||||
|   if (!isObject(ec)) return {}; |   if (!ec || !isObject(ec)) return {}; | ||||||
| 
 | 
 | ||||||
|   const opts: Record<string, any> = {}; |   const opts: MonacoOpts = {}; | ||||||
|   opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec); |   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 ('indent_size' in ec) { | ||||||
|   if ('max_line_length' in ec) opts.rulers = [Number(ec.max_line_length)]; |     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.trimAutoWhitespace = ec.trim_trailing_whitespace === true; | ||||||
|   opts.insertSpaces = ec.indent_style === 'space'; |   opts.insertSpaces = ec.indent_style === 'space'; | ||||||
|   opts.useTabStops = ec.indent_style === 'tab'; |   opts.useTabStops = ec.indent_style === 'tab'; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user