diff options
Diffstat (limited to 'web_src')
-rw-r--r-- | web_src/js/features/codeeditor.js | 104 | ||||
-rw-r--r-- | web_src/js/index.js | 165 | ||||
-rw-r--r-- | web_src/js/utils.js | 22 | ||||
-rw-r--r-- | web_src/less/_editor.less | 37 | ||||
-rw-r--r-- | web_src/less/_repository.less | 16 | ||||
-rw-r--r-- | web_src/less/themes/theme-arc-green.less | 19 |
6 files changed, 184 insertions, 179 deletions
diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js new file mode 100644 index 0000000000..0999d05f05 --- /dev/null +++ b/web_src/js/features/codeeditor.js @@ -0,0 +1,104 @@ +import {basename, extname, isObject, isDarkTheme} from '../utils.js'; + +const languagesByFilename = {}; +const languagesByExt = {}; + +function getEditorconfig(input) { + try { + return JSON.parse(input.dataset.editorconfig); + } catch (_err) { + return null; + } +} + +function initLanguages(monaco) { + for (const {filenames, extensions, id} of monaco.languages.getLanguages()) { + for (const filename of filenames || []) { + languagesByFilename[filename] = id; + } + for (const extension of extensions || []) { + languagesByExt[extension] = id; + } + } +} + +function getLanguage(filename) { + return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext'; +} + +function updateEditor(monaco, editor, filenameInput) { + const newFilename = filenameInput.value; + editor.updateOptions(getOptions(filenameInput)); + const model = editor.getModel(); + const language = model.getModeId(); + const newLanguage = getLanguage(newFilename); + if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage); +} + +export async function createCodeEditor(textarea, filenameInput, previewFileModes) { + const filename = basename(filenameInput.value); + const previewLink = document.querySelector('a[data-tab=preview]'); + const markdownExts = (textarea.dataset.markdownFileExts || '').split(','); + const lineWrapExts = (textarea.dataset.lineWrapExtensions || '').split(','); + const isMarkdown = markdownExts.includes(extname(filename)); + + if (previewLink) { + if (isMarkdown && (previewFileModes || []).includes('markdown')) { + previewLink.dataset.url = previewLink.dataset.url.replace(/(.*)\/.*/i, `$1/markdown`); + previewLink.style.display = ''; + } else { + previewLink.style.display = 'none'; + } + } + + const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor'); + initLanguages(monaco); + + const container = document.createElement('div'); + container.className = 'monaco-editor-container'; + textarea.parentNode.appendChild(container); + + const editor = monaco.editor.create(container, { + value: textarea.value, + language: getLanguage(filename), + ...getOptions(filenameInput, lineWrapExts), + }); + + const model = editor.getModel(); + model.onDidChangeContent(() => { + textarea.value = editor.getValue(); + textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure + }); + + window.addEventListener('resize', () => { + editor.layout(); + }); + + filenameInput.addEventListener('keyup', () => { + updateEditor(monaco, editor, filenameInput); + }); + + const loading = document.querySelector('.editor-loading'); + if (loading) loading.remove(); + + return editor; +} + +function getOptions(filenameInput, lineWrapExts) { + const ec = getEditorconfig(filenameInput); + const theme = isDarkTheme() ? 'vs-dark' : 'vs'; + const wordWrap = (lineWrapExts || []).includes(extname(filenameInput.value)) ? 'on' : 'off'; + + const opts = {theme, wordWrap}; + if (isObject(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 ('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'; + } + + return opts; +} diff --git a/web_src/js/index.js b/web_src/js/index.js index a74fba34e8..02189a5f13 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -20,6 +20,7 @@ import createDropzone from './features/dropzone.js'; import highlight from './features/highlight.js'; import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; import {initNotificationsTable, initNotificationCount} from './features/notification.js'; +import {createCodeEditor} from './features/codeeditor.js'; const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; @@ -28,9 +29,7 @@ function htmlEncode(text) { } let previewFileModes; -let simpleMDEditor; const commentMDEditors = {}; -let codeMirrorEditor; // Silence fomantic's error logging when tabs are used without a target content element $.fn.tab.settings.silent = true; @@ -1467,62 +1466,6 @@ $.fn.getCursorPosition = function () { return pos; }; -function setSimpleMDE($editArea) { - if (codeMirrorEditor) { - codeMirrorEditor.toTextArea(); - codeMirrorEditor = null; - } - - if (simpleMDEditor) { - return true; - } - - simpleMDEditor = new SimpleMDE({ - autoDownloadFontAwesome: false, - element: $editArea[0], - forceSync: true, - renderingConfig: { - singleLineBreaks: false - }, - indentWithTabs: false, - tabSize: 4, - spellChecker: false, - previewRender(plainText, preview) { // Async method - setTimeout(() => { - // FIXME: still send render request when return back to edit mode - $.post($editArea.data('url'), { - _csrf: csrf, - mode: 'gfm', - context: $editArea.data('context'), - text: plainText - }, (data) => { - preview.innerHTML = `<div class="markdown ui segment">${data}</div>`; - }); - }, 0); - - return 'Loading...'; - }, - toolbar: ['bold', 'italic', 'strikethrough', '|', - 'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|', - 'code', 'quote', '|', - 'unordered-list', 'ordered-list', '|', - 'link', 'image', 'table', 'horizontal-rule', '|', - 'clean-block', 'preview', 'fullscreen', 'side-by-side', '|', - { - name: 'revert-to-textarea', - action(e) { - e.toTextArea(); - }, - className: 'fa fa-file', - title: 'Revert to simple textarea', - }, - ] - }); - $(simpleMDEditor.codemirror.getInputField()).addClass('js-quick-submit'); - - return true; -} - function setCommentSimpleMDE($editArea) { const simplemde = new SimpleMDE({ autoDownloadFontAwesome: false, @@ -1569,27 +1512,7 @@ function setCommentSimpleMDE($editArea) { return simplemde; } -function setCodeMirror($editArea) { - if (simpleMDEditor) { - simpleMDEditor.toTextArea(); - simpleMDEditor = null; - } - - if (codeMirrorEditor) { - return true; - } - - codeMirrorEditor = CodeMirror.fromTextArea($editArea[0], { - lineNumbers: true - }); - codeMirrorEditor.on('change', (cm, _change) => { - $editArea.val(cm.getValue()); - }); - - return true; -} - -function initEditor() { +async function initEditor() { $('.js-quick-pull-choice-option').on('change', function () { if ($(this).val() === 'commit-to-new-branch') { $('.quick-pull-branch-name').show(); @@ -1650,89 +1573,7 @@ function initEditor() { const $editArea = $('.repository.editor textarea#edit_area'); if (!$editArea.length) return; - const markdownFileExts = $editArea.data('markdown-file-exts').split(','); - const lineWrapExtensions = $editArea.data('line-wrap-extensions').split(','); - - $editFilename.on('keyup', () => { - const val = $editFilename.val(); - let mode, spec, extension, extWithDot, dataUrl, apiCall; - - extension = extWithDot = ''; - const m = /.+\.([^.]+)$/.exec(val); - if (m) { - extension = m[1]; - extWithDot = `.${extension}`; - } - - const info = CodeMirror.findModeByExtension(extension); - const previewLink = $('a[data-tab=preview]'); - if (info) { - mode = info.mode; - spec = info.mime; - apiCall = mode; - } else { - apiCall = extension; - } - - if (previewLink.length && apiCall && previewFileModes && previewFileModes.length && previewFileModes.includes(apiCall)) { - dataUrl = previewLink.data('url'); - previewLink.data('url', dataUrl.replace(/(.*)\/.*/i, `$1/${mode}`)); - previewLink.show(); - } else { - previewLink.hide(); - } - - // If this file is a Markdown extensions, we will load that editor and return - if (markdownFileExts.includes(extWithDot)) { - if (setSimpleMDE($editArea)) { - return; - } - } - - // Else we are going to use CodeMirror - if (!codeMirrorEditor && !setCodeMirror($editArea)) { - return; - } - - if (mode) { - codeMirrorEditor.setOption('mode', spec); - CodeMirror.autoLoadMode(codeMirrorEditor, mode); - } - - if (lineWrapExtensions.includes(extWithDot)) { - codeMirrorEditor.setOption('lineWrapping', true); - } else { - codeMirrorEditor.setOption('lineWrapping', false); - } - - // get the filename without any folder - let value = $editFilename.val(); - if (value.length === 0) { - return; - } - value = value.split('/'); - value = value[value.length - 1]; - - $.getJSON($editFilename.data('ec-url-prefix') + value, (editorconfig) => { - if (editorconfig.indent_style === 'tab') { - codeMirrorEditor.setOption('indentWithTabs', true); - codeMirrorEditor.setOption('extraKeys', {}); - } else { - codeMirrorEditor.setOption('indentWithTabs', false); - // required because CodeMirror doesn't seems to use spaces correctly for {"indentWithTabs": false}: - // - https://github.com/codemirror/CodeMirror/issues/988 - // - https://codemirror.net/doc/manual.html#keymaps - codeMirrorEditor.setOption('extraKeys', { - Tab(cm) { - const spaces = new Array(parseInt(cm.getOption('indentUnit')) + 1).join(' '); - cm.replaceSelection(spaces); - } - }); - } - codeMirrorEditor.setOption('indentUnit', editorconfig.indent_size || 4); - codeMirrorEditor.setOption('tabSize', editorconfig.tab_width || 4); - }); - }).trigger('keyup'); + await createCodeEditor($editArea[0], $editFilename[0], previewFileModes); // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage // to enable or disable the commit button diff --git a/web_src/js/utils.js b/web_src/js/utils.js index b000c1af77..b511c9981d 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -1,3 +1,25 @@ +// retrieve a HTML string for given SVG icon name and size in pixels export function svg(name, size) { return `<svg class="svg ${name}" width="${size}" height="${size}" aria-hidden="true"><use xlink:href="#${name}"/></svg>`; } + +// transform /path/to/file.ext to file.ext +export function basename(path = '') { + return path ? path.replace(/^.*\//, '') : ''; +} + +// transform /path/to/file.ext to .ext +export function extname(path = '') { + const [_, ext] = /.+(\.[^.]+)$/.exec(path) || []; + return ext || ''; +} + +// test whether a variable is an object +export function isObject(obj) { + return Object.prototype.toString.call(obj) === '[object Object]'; +} + +// returns whether a dark theme is enabled +export function isDarkTheme() { + return document.documentElement.classList.contains('theme-arc-green'); +} diff --git a/web_src/less/_editor.less b/web_src/less/_editor.less index 714d41649a..d8ba1467e9 100644 --- a/web_src/less/_editor.less +++ b/web_src/less/_editor.less @@ -32,3 +32,40 @@ .editor-toolbar i.separator { border-left: none; } + +.editor-loading { + padding: 1rem; + text-align: center; +} + +.edit-diff { + padding: 0 !important; +} + +.edit-diff > div > .ui.table { + border-top: none !important; + border-bottom: none !important; + border-left: 1px solid #d4d4d5 !important; + border-right: 1px solid #d4d4d5 !important; +} + +#edit_area { + display: none; +} + +.monaco-editor-container { + width: 100%; + min-height: 200px; + height: 90vh; +} + +/* overwrite conflicting styles from fomantic */ +.monaco-editor-container .inputarea { + min-height: 0 !important; + margin: 0 !important; + padding: 0 !important; + resize: none !important; + border: none !important; + color: transparent !important; + background-color: transparent !important; +} diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 863f2bad8e..6fb089636a 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -1555,14 +1555,6 @@ text-align: center; } - .removed-code { - background-color: #ff9999; - } - - .added-code { - background-color: #99ff99; - } - [data-line-num]::before { content: attr(data-line-num); text-align: right; @@ -2865,3 +2857,11 @@ td.blob-excerpt { height: 48px; overflow: hidden; } + +.removed-code { + background-color: #ff9999; +} + +.added-code { + background-color: #99ff99; +} diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less index d56b7b8eeb..19689d107b 100644 --- a/web_src/less/themes/theme-arc-green.less +++ b/web_src/less/themes/theme-arc-green.less @@ -576,10 +576,6 @@ a.ui.basic.green.label:hover { .repository.file.editor.edit, .repository.wiki.new .CodeMirror { - border-right: 1px solid rgba(187, 187, 187, .6); - border-left: 1px solid rgba(187, 187, 187, .6); - border-bottom: 1px solid rgba(187, 187, 187, .6); - .editor-preview, .editor-preview-side, & + .editor-preview-side { @@ -751,7 +747,11 @@ a.ui.basic.green.label:hover { border-color: #314a37 !important; } -.repository .diff-file-box .code-diff tbody tr .added-code { +.removed-code { + background-color: #5f3737; +} + +.added-code { background-color: #3a523a; } @@ -766,10 +766,6 @@ a.ui.basic.green.label:hover { color: #8ab398; } -.repository .diff-file-box .code-diff tbody tr .removed-code { - background-color: #5f3737; -} - .tag-code, .tag-code td { background: #242637 !important; @@ -1300,6 +1296,11 @@ a.ui.labels .label:hover { border-color: #7f98ad; } +.edit-diff > div > .ui.table { + border-left-color: #404552 !important; + border-right-color: #404552 !important; +} + .editor-toolbar a { color: #87ab63 !important; } |