code.gitea.io/gitea@v1.22.3/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: 'off', 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 'focusBorder': '#0000', // prevent blue border 102 }, 103 }); 104 105 // Quick fix: https://github.com/microsoft/monaco-editor/issues/2962 106 monaco.languages.register({id: 'vs.editor.nullLanguage'}); 107 monaco.languages.setLanguageConfiguration('vs.editor.nullLanguage', {}); 108 109 const editor = monaco.editor.create(container, { 110 value: textarea.value, 111 theme: 'gitea', 112 language, 113 ...other, 114 }); 115 116 monaco.editor.addKeybindingRules([ 117 {keybinding: monaco.KeyCode.Enter, command: null}, // disable enter from accepting code completion 118 ]); 119 120 const model = editor.getModel(); 121 model.onDidChangeContent(() => { 122 textarea.value = editor.getValue({preserveBOM: true}); 123 textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure 124 }); 125 126 exportEditor(editor); 127 128 const loading = document.querySelector('.editor-loading'); 129 if (loading) loading.remove(); 130 131 return {monaco, editor}; 132 } 133 134 function getFileBasedOptions(filename, lineWrapExts) { 135 return { 136 wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off', 137 }; 138 } 139 140 function togglePreviewDisplay(previewable) { 141 const previewTab = document.querySelector('a[data-tab="preview"]'); 142 if (!previewTab) return; 143 144 if (previewable) { 145 const newUrl = (previewTab.getAttribute('data-url') || '').replace(/(.*)\/.*/, `$1/markup`); 146 previewTab.setAttribute('data-url', newUrl); 147 previewTab.style.display = ''; 148 } else { 149 previewTab.style.display = 'none'; 150 // If the "preview" tab was active, user changes the filename to a non-previewable one, 151 // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active 152 if (previewTab.classList.contains('active')) { 153 const writeTab = document.querySelector('a[data-tab="write"]'); 154 writeTab.click(); 155 } 156 } 157 } 158 159 export async function createCodeEditor(textarea, filenameInput) { 160 const filename = basename(filenameInput.value); 161 const previewableExts = new Set((textarea.getAttribute('data-previewable-extensions') || '').split(',')); 162 const lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(','); 163 const previewable = previewableExts.has(extname(filename)); 164 const editorConfig = getEditorconfig(filenameInput); 165 166 togglePreviewDisplay(previewable); 167 168 const {monaco, editor} = await createMonaco(textarea, filename, { 169 ...baseOptions, 170 ...getFileBasedOptions(filenameInput.value, lineWrapExts), 171 ...getEditorConfigOptions(editorConfig), 172 }); 173 174 filenameInput.addEventListener('input', onInputDebounce(() => { 175 const filename = filenameInput.value; 176 const previewable = previewableExts.has(extname(filename)); 177 togglePreviewDisplay(previewable); 178 updateEditor(monaco, editor, filename, lineWrapExts); 179 })); 180 181 return editor; 182 } 183 184 function getEditorConfigOptions(ec) { 185 if (!isObject(ec)) return {}; 186 187 const opts = {}; 188 opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec); 189 if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size); 190 if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize; 191 if ('max_line_length' in ec) opts.rulers = [Number(ec.max_line_length)]; 192 opts.trimAutoWhitespace = ec.trim_trailing_whitespace === true; 193 opts.insertSpaces = ec.indent_style === 'space'; 194 opts.useTabStops = ec.indent_style === 'tab'; 195 return opts; 196 }