code.gitea.io/gitea@v1.21.7/web_src/js/features/codeeditor.js (about) 1 import tinycolor from 'tinycolor2'; 2 import {basename, extname, isObject, isDarkTheme} from '../utils.js'; 3 import {onInputDebounce} from '../utils/dom.js'; 4 5 const languagesByFilename = {}; 6 const languagesByExt = {}; 7 8 const baseOptions = { 9 fontFamily: 'var(--fonts-monospace)', 10 fontSize: 14, // https://github.com/microsoft/monaco-editor/issues/2242 11 guides: {bracketPairs: false, indentation: false}, 12 links: false, 13 minimap: {enabled: false}, 14 occurrencesHighlight: false, 15 overviewRulerLanes: 0, 16 renderLineHighlight: 'all', 17 renderLineHighlightOnlyWhenFocus: true, 18 rulers: false, 19 scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6}, 20 scrollBeyondLastLine: false, 21 automaticLayout: true, 22 }; 23 24 function getEditorconfig(input) { 25 try { 26 return JSON.parse(input.getAttribute('data-editorconfig')); 27 } catch { 28 return null; 29 } 30 } 31 32 function initLanguages(monaco) { 33 for (const {filenames, extensions, id} of monaco.languages.getLanguages()) { 34 for (const filename of filenames || []) { 35 languagesByFilename[filename] = id; 36 } 37 for (const extension of extensions || []) { 38 languagesByExt[extension] = id; 39 } 40 } 41 } 42 43 function getLanguage(filename) { 44 return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext'; 45 } 46 47 function updateEditor(monaco, editor, filename, lineWrapExts) { 48 editor.updateOptions(getFileBasedOptions(filename, lineWrapExts)); 49 const model = editor.getModel(); 50 const language = model.getLanguageId(); 51 const newLanguage = getLanguage(filename); 52 if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage); 53 } 54 55 // export editor for customization - https://github.com/go-gitea/gitea/issues/10409 56 function exportEditor(editor) { 57 if (!window.codeEditors) window.codeEditors = []; 58 if (!window.codeEditors.includes(editor)) window.codeEditors.push(editor); 59 } 60 61 export async function createMonaco(textarea, filename, editorOpts) { 62 const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor'); 63 64 initLanguages(monaco); 65 let {language, ...other} = editorOpts; 66 if (!language) language = getLanguage(filename); 67 68 const container = document.createElement('div'); 69 container.className = 'monaco-editor-container'; 70 textarea.parentNode.append(container); 71 72 // https://github.com/microsoft/monaco-editor/issues/2427 73 // also, monaco can only parse 6-digit hex colors, so we convert the colors to that format 74 const styles = window.getComputedStyle(document.documentElement); 75 const getColor = (name) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6'); 76 77 monaco.editor.defineTheme('gitea', { 78 base: isDarkTheme() ? 'vs-dark' : 'vs', 79 inherit: true, 80 rules: [ 81 { 82 background: getColor('--color-code-bg'), 83 } 84 ], 85 colors: { 86 'editor.background': getColor('--color-code-bg'), 87 'editor.foreground': getColor('--color-text'), 88 'editor.inactiveSelectionBackground': getColor('--color-primary-light-4'), 89 'editor.lineHighlightBackground': getColor('--color-editor-line-highlight'), 90 'editor.selectionBackground': getColor('--color-primary-light-3'), 91 'editor.selectionForeground': getColor('--color-primary-light-3'), 92 'editorLineNumber.background': getColor('--color-code-bg'), 93 'editorLineNumber.foreground': getColor('--color-secondary-dark-6'), 94 'editorWidget.background': getColor('--color-body'), 95 'editorWidget.border': getColor('--color-secondary'), 96 'input.background': getColor('--color-input-background'), 97 'input.border': getColor('--color-input-border'), 98 'input.foreground': getColor('--color-input-text'), 99 'scrollbar.shadow': getColor('--color-shadow'), 100 'progressBar.background': getColor('--color-primary'), 101 } 102 }); 103 104 // Quick fix: https://github.com/microsoft/monaco-editor/issues/2962 105 monaco.languages.register({id: 'vs.editor.nullLanguage'}); 106 monaco.languages.setLanguageConfiguration('vs.editor.nullLanguage', {}); 107 108 const editor = monaco.editor.create(container, { 109 value: textarea.value, 110 theme: 'gitea', 111 language, 112 ...other, 113 }); 114 115 const model = editor.getModel(); 116 model.onDidChangeContent(() => { 117 textarea.value = editor.getValue({preserveBOM: true}); 118 textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure 119 }); 120 121 exportEditor(editor); 122 123 const loading = document.querySelector('.editor-loading'); 124 if (loading) loading.remove(); 125 126 return {monaco, editor}; 127 } 128 129 function getFileBasedOptions(filename, lineWrapExts) { 130 return { 131 wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off', 132 }; 133 } 134 135 function togglePreviewDisplay(previewable) { 136 const previewTab = document.querySelector('a[data-tab="preview"]'); 137 if (!previewTab) return; 138 139 if (previewable) { 140 const newUrl = (previewTab.getAttribute('data-url') || '').replace(/(.*)\/.*/, `$1/markup`); 141 previewTab.setAttribute('data-url', newUrl); 142 previewTab.style.display = ''; 143 } else { 144 previewTab.style.display = 'none'; 145 // If the "preview" tab was active, user changes the filename to a non-previewable one, 146 // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active 147 if (previewTab.classList.contains('active')) { 148 const writeTab = document.querySelector('a[data-tab="write"]'); 149 writeTab.click(); 150 } 151 } 152 } 153 154 export async function createCodeEditor(textarea, filenameInput) { 155 const filename = basename(filenameInput.value); 156 const previewableExts = new Set((textarea.getAttribute('data-previewable-extensions') || '').split(',')); 157 const lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(','); 158 const previewable = previewableExts.has(extname(filename)); 159 const editorConfig = getEditorconfig(filenameInput); 160 161 togglePreviewDisplay(previewable); 162 163 const {monaco, editor} = await createMonaco(textarea, filename, { 164 ...baseOptions, 165 ...getFileBasedOptions(filenameInput.value, lineWrapExts), 166 ...getEditorConfigOptions(editorConfig), 167 }); 168 169 filenameInput.addEventListener('input', onInputDebounce(() => { 170 const filename = filenameInput.value; 171 const previewable = previewableExts.has(extname(filename)); 172 togglePreviewDisplay(previewable); 173 updateEditor(monaco, editor, filename, lineWrapExts); 174 })); 175 176 return editor; 177 } 178 179 function getEditorConfigOptions(ec) { 180 if (!isObject(ec)) return {}; 181 182 const opts = {}; 183 opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec); 184 if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size); 185 if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize; 186 if ('max_line_length' in ec) opts.rulers = [Number(ec.max_line_length)]; 187 opts.trimAutoWhitespace = ec.trim_trailing_whitespace === true; 188 opts.insertSpaces = ec.indent_style === 'space'; 189 opts.useTabStops = ec.indent_style === 'tab'; 190 return opts; 191 }