From 9249c810b883406e649586f6731a820c1c96b698 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 14 May 2020 18:06:01 +0200 Subject: [PATCH] Switch code editor to Monaco (#11366) * Switch code editor to Monaco This switches out CodeMirror for Monaco which is based on the same code base as VS code and should work pretty similar to it. It does add a few async chunks, totalling around 10MB to our build. It currently supports around 65 languages and in the default configuration, each language would emit one ugly [number].js chunk, so I opted to combine them all into a single file for now. CodeMirror is still being used under the hood by SimpleMDE so it can not be removed yet. * inline editorconfig, fix diff, use for markdown, remove more dead code * refactors, remove jquery usage * use tab_width * fix intellisense * rename function for clarity * misc tweaks, enable webpack progress display * only use --progress on dev build * remove useless borders in arc-green * fix typo * remove obsolete comment * small refactor * fix file creation and various refactors * unset useTabStops too when no editorconfig * small refactor * disable webpack's [big] warnings * remove useless await * fix dark theme check * rename chunk to 'monaco' * add to .gitignore and delete webpack dest before build * increase editor height * support more editorconfig properties * remove empty element filter * rename Co-authored-by: John Olheiser --- .eslintrc | 1 + .gitignore | 1 + Makefile | 5 +- custom/conf/app.ini.sample | 2 +- package-lock.json | 48 ++++++- package.json | 5 +- routers/repo/editor.go | 17 ++- templates/base/head.tmpl | 2 +- templates/pwa/serviceworker_js.tmpl | 12 +- templates/repo/editor/diff_preview.tmpl | 2 +- templates/repo/editor/edit.tmpl | 7 +- web_src/js/features/codeeditor.js | 104 ++++++++++++++ web_src/js/index.js | 165 +---------------------- web_src/js/utils.js | 22 +++ web_src/less/_editor.less | 37 +++++ web_src/less/_repository.less | 16 +-- web_src/less/themes/theme-arc-green.less | 19 +-- webpack.config.js | 36 ++++- 18 files changed, 304 insertions(+), 197 deletions(-) create mode 100644 web_src/js/features/codeeditor.js diff --git a/.eslintrc b/.eslintrc index a59367695e..8f337baec5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -60,6 +60,7 @@ rules: no-unused-vars: [2, {args: all, argsIgnorePattern: ^_, varsIgnorePattern: ^_, ignoreRestSiblings: true}] no-use-before-define: [0] no-var: [2] + object-curly-newline: [0] object-curly-spacing: [2, never] one-var-declaration-per-line: [0] one-var: [0] diff --git a/.gitignore b/.gitignore index e13b2c0fe6..d14544c721 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ coverage.all /yarn.lock /public/js /public/css +/public/fonts /public/fomantic /public/img/svg /VERSION diff --git a/Makefile b/Makefile index db70722e9a..0c63cef9cc 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/integrations/migration-test,$(fi WEBPACK_SOURCES := $(shell find web_src/js web_src/less -type f) WEBPACK_CONFIGS := webpack.config.js WEBPACK_DEST := public/js/index.js public/css/index.css -WEBPACK_DEST_DIRS := public/js public/css +WEBPACK_DEST_DIRS := public/js public/css public/fonts BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST)) @@ -295,7 +295,7 @@ lint-frontend: node_modules .PHONY: watch-frontend watch-frontend: node_modules - NODE_ENV=development npx webpack --hide-modules --display-entrypoints=false --watch + NODE_ENV=development npx webpack --hide-modules --display-entrypoints=false --watch --progress .PHONY: test test: @@ -598,6 +598,7 @@ $(FOMANTIC_DEST): $(FOMANTIC_CONFIGS) package-lock.json | node_modules webpack: $(WEBPACK_DEST) $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json | node_modules + rm -rf $(WEBPACK_DEST_DIRS) npx webpack --hide-modules --display-entrypoints=false @touch $(WEBPACK_DEST) diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 06b7b96d40..c8797ca56a 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -52,7 +52,7 @@ DEFAULT_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.pulls,repo.wiki PREFIX_ARCHIVE_FILES = true [repository.editor] -; List of file extensions for which lines should be wrapped in the CodeMirror editor +; List of file extensions for which lines should be wrapped in the Monaco editor ; Separate extensions with a comma. To line wrap files without an extension, just put a comma LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd, ; Valid file modes that have a preview API associated with them, such as api/v1/markdown diff --git a/package-lock.json b/package-lock.json index 9291ebdc42..e2af3a95fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4861,6 +4861,27 @@ "flat-cache": "^2.0.1" } }, + "file-loader": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.0.0.tgz", + "integrity": "sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ==", + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -7903,9 +7924,9 @@ } }, "jest-worker": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", - "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz", + "integrity": "sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw==", "requires": { "merge-stream": "^2.0.0", "supports-color": "^7.0.0" @@ -9255,6 +9276,19 @@ "minimist": "^1.2.5" } }, + "monaco-editor": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz", + "integrity": "sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ==" + }, + "monaco-editor-webpack-plugin": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.9.0.tgz", + "integrity": "sha512-tOiiToc94E1sb50BgZ8q8WK/bxus77SRrwCqIpAB5er3cpX78SULbEBY4YPOB8kDolOzKRt30WIHG/D6gz69Ww==", + "requires": { + "loader-utils": "^1.2.3" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -13684,13 +13718,13 @@ } }, "terser-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-gHAVFtJz1gQW5cu0btPtb+5Syo7K9hRj3b0lstgfglaBhbtcOCizsaPTnxOBGmF9iIgwsrSIiraBa2xzuWND7Q==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-3.0.1.tgz", + "integrity": "sha512-eFDtq8qPUEa9hXcUzTwKXTnugIVtlqc1Z/ZVhG8LmRT3lgRY13+pQTnFLY2N7ATB6TKCHuW/IGjoAnZz9wOIqw==", "requires": { "cacache": "^15.0.3", "find-cache-dir": "^3.3.1", - "jest-worker": "^25.5.0", + "jest-worker": "^26.0.0", "p-limit": "^2.3.0", "schema-utils": "^2.6.6", "serialize-javascript": "^3.0.0", diff --git a/package.json b/package.json index b76d9162c4..66d2949e2c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "domino": "2.1.5", "dropzone": "5.7.0", "fast-glob": "3.2.2", + "file-loader": "6.0.0", "fomantic-ui": "2.8.4", "highlight.js": "10.0.2", "imports-loader": "0.8.0", @@ -27,6 +28,8 @@ "jquery.are-you-sure": "1.9.0", "less-loader": "6.0.0", "mini-css-extract-plugin": "0.9.0", + "monaco-editor": "0.20.0", + "monaco-editor-webpack-plugin": "1.9.0", "optimize-css-assets-webpack-plugin": "5.0.3", "postcss-loader": "3.0.0", "postcss-preset-env": "6.7.0", @@ -35,7 +38,7 @@ "svgo": "1.3.2", "svgo-loader": "2.2.1", "swagger-ui": "3.25.1", - "terser-webpack-plugin": "3.0.0", + "terser-webpack-plugin": "3.0.1", "vue": "2.6.11", "vue-bar-graph": "1.2.0", "vue-calendar-heatmap": "0.8.4", diff --git a/routers/repo/editor.go b/routers/repo/editor.go index a821c31983..2fa7976e00 100644 --- a/routers/repo/editor.go +++ b/routers/repo/editor.go @@ -5,6 +5,7 @@ package repo import ( + "encoding/json" "fmt" "io/ioutil" "path" @@ -146,11 +147,24 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") - ctx.Data["EditorconfigURLPrefix"] = fmt.Sprintf("%s/api/v1/repos/%s/editorconfig/", setting.AppSubURL, ctx.Repo.Repository.FullName()) + ctx.Data["Editorconfig"] = GetEditorConfig(ctx, treePath) ctx.HTML(200, tplEditFile) } +// GetEditorConfig returns a editorconfig JSON string for given treePath or "null" +func GetEditorConfig(ctx *context.Context, treePath string) string { + ec, err := ctx.Repo.GetEditorconfig() + if err == nil { + def, err := ec.GetDefinitionForFilename(treePath) + if err == nil { + jsonStr, _ := json.Marshal(def) + return string(jsonStr) + } + } + return "null" +} + // EditFile render edit file page func EditFile(ctx *context.Context) { editFile(ctx, false) @@ -186,6 +200,7 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") + ctx.Data["Editorconfig"] = GetEditorConfig(ctx, form.TreePath) if ctx.HasError() { ctx.HTML(200, tplEditFile) diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 251b5eb8fc..eae2389238 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -1,5 +1,5 @@ - + diff --git a/templates/pwa/serviceworker_js.tmpl b/templates/pwa/serviceworker_js.tmpl index edb8ba6e1d..32975e0fd5 100644 --- a/templates/pwa/serviceworker_js.tmpl +++ b/templates/pwa/serviceworker_js.tmpl @@ -45,7 +45,17 @@ var urlsToCache = [ '{{StaticUrlPrefix}}/vendor/assets/roboto-fonts/roboto-v20-latin-ext_cyrillic-ext_latin_greek_vietnamese_cyrillic_greek-ext-regular.woff2', '{{StaticUrlPrefix}}/vendor/assets/roboto-fonts/roboto-v20-latin-ext_cyrillic-ext_latin_greek_vietnamese_cyrillic_greek-ext-italic.woff2', '{{StaticUrlPrefix}}/vendor/assets/roboto-fonts/roboto-v20-latin-ext_cyrillic-ext_latin_greek_vietnamese_cyrillic_greek-ext-700.woff2', - '{{StaticUrlPrefix}}/vendor/assets/roboto-fonts/roboto-v20-latin-ext_cyrillic-ext_latin_greek_vietnamese_cyrillic_greek-ext-700italic.woff2' + '{{StaticUrlPrefix}}/vendor/assets/roboto-fonts/roboto-v20-latin-ext_cyrillic-ext_latin_greek_vietnamese_cyrillic_greek-ext-700italic.woff2', + + // monaco + '{{StaticUrlPrefix}}/css/monaco.css', + '{{StaticUrlPrefix}}/fonts/codicon.ttf', + '{{StaticUrlPrefix}}/js/monaco-css.worker.js', + '{{StaticUrlPrefix}}/js/monaco-editor.worker.js', + '{{StaticUrlPrefix}}/js/monaco-html.worker.js', + '{{StaticUrlPrefix}}/js/monaco-json.worker.js', + '{{StaticUrlPrefix}}/js/monaco.js', + '{{StaticUrlPrefix}}/js/monaco-ts.worker.js' ]; self.addEventListener('install', function (event) { diff --git a/templates/repo/editor/diff_preview.tmpl b/templates/repo/editor/diff_preview.tmpl index b663e4e93d..0ed330c57b 100644 --- a/templates/repo/editor/diff_preview.tmpl +++ b/templates/repo/editor/diff_preview.tmpl @@ -1,6 +1,6 @@
-
+
{{template "repo/diff/section_unified" dict "file" .File "root" $}} diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index 3eac405aa6..283bd32508 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -15,7 +15,7 @@ {{range $i, $v := .TreeNames}}
/
{{if eq $i $l}} - + {{svg "octicon-info" 16}} {{else}} {{$v}} @@ -41,11 +41,14 @@ data-markdown-file-exts="{{.MarkdownFileExts}}" data-line-wrap-extensions="{{.LineWrapExtensions}}"> {{.FileContent}} +
+ {{.i18n.Tr "loading"}} +
{{.i18n.Tr "loading"}}
-
+
{{.i18n.Tr "loading"}}
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 = `
${data}
`; - }); - }, 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 ``; } + +// 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; } diff --git a/webpack.config.js b/webpack.config.js index e87dd770cb..d6a632ad1f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,6 +2,7 @@ const cssnano = require('cssnano'); const fastGlob = require('fast-glob'); const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const PostCSSPresetEnv = require('postcss-preset-env'); const PostCSSSafeParser = require('postcss-safe-parser'); @@ -76,6 +77,14 @@ module.exports = { splitChunks: { chunks: 'async', name: (_, chunks) => chunks.map((item) => item.name).join('-'), + cacheGroups: { + // this bundles all monaco's languages into one file instead of emitting 1-65.js files + monaco: { + test: /monaco-editor/, + name: 'monaco', + chunks: 'async' + } + } } }, module: { @@ -91,6 +100,7 @@ module.exports = { }, { test: /\.worker\.js$/, + exclude: /monaco/, use: [ { loader: 'worker-loader', @@ -149,7 +159,10 @@ module.exports = { loader: 'css-loader', options: { importLoaders: 2, - url: false, + url: (_url, resourcePath) => { + // only resolve URLs for dependencies + return resourcePath.includes('node_modules'); + }, } }, { @@ -187,6 +200,19 @@ module.exports = { }, ], }, + { + test: /\.(ttf|woff2?)$/, + use: [ + { + loader: 'file-loader', + options: { + name: '[name].[ext]', + outputPath: 'fonts/', + publicPath: (url) => `../fonts/${url}`, // seems required for monaco's font + }, + }, + ], + }, ], }, plugins: [ @@ -209,9 +235,14 @@ module.exports = { new SpriteLoaderPlugin({ plainSprite: true, }), + new MonacoWebpackPlugin({ + filename: 'js/monaco-[name].worker.js', + }), ], performance: { hints: false, + maxEntrypointSize: Infinity, + maxAssetSize: Infinity, }, resolve: { symlinks: false, @@ -224,4 +255,7 @@ module.exports = { 'node_modules/**', ], }, + stats: { + children: false, + }, };