diff --git a/.eslintrc b/.eslintrc
index a59367695..8f337baec 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 e13b2c0fe..d14544c72 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 db70722e9..0c63cef9c 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 06b7b96d4..c8797ca56 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 9291ebdc4..e2af3a95f 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 b76d9162c..66d2949e2 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 a821c3198..2fa7976e0 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 251b5eb8f..eae238923 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 edb8ba6e1..32975e0fd 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 b663e4e93..0ed330c57 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 3eac405aa..283bd3250 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"}}
-
diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js
new file mode 100644
index 000000000..0999d05f0
--- /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 a74fba34e..02189a5f1 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 b000c1af7..b511c9981 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 714d41649..d8ba1467e 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 863f2bad8..6fb089636 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 d56b7b8ee..19689d107 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 e87dd770c..d6a632ad1 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,
+ },
};