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, + }, };