diff --git a/.eslintrc b/.eslintrc
index a59367695e71..8f337baec597 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 e13b2c0fe617..d14544c72109 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 db70722e9a27..0c63cef9cc6d 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 06b7b96d40c1..c8797ca56a72 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 9291ebdc42ec..e2af3a95fd07 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 b76d9162c416..66d2949e2c0c 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 a821c31983e0..2fa7976e0015 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 251b5eb8fce7..eae238923820 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="{{.Language}}">
+<html lang="{{.Language}}" class="theme-{{.SignedUser.Theme}}">
 <head data-suburl="{{AppSubUrl}}">
 	<meta charset="utf-8">
 	<meta name="viewport" content="width=device-width, initial-scale=1">
diff --git a/templates/pwa/serviceworker_js.tmpl b/templates/pwa/serviceworker_js.tmpl
index edb8ba6e1d10..32975e0fd582 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 b663e4e93d5a..0ed330c57bd3 100644
--- a/templates/repo/editor/diff_preview.tmpl
+++ b/templates/repo/editor/diff_preview.tmpl
@@ -1,6 +1,6 @@
 <div class="diff-file-box">
 	<div class="ui attached table segment">
-		<div class="file-body file-code code-view code-diff">
+		<div class="file-body file-code code-view code-diff-unified">
 			<table>
 				<tbody>
 					{{template "repo/diff/section_unified" dict "file" .File "root" $}}
diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl
index 3eac405aa627..283bd3250863 100644
--- a/templates/repo/editor/edit.tmpl
+++ b/templates/repo/editor/edit.tmpl
@@ -15,7 +15,7 @@
 						{{range $i, $v := .TreeNames}}
 							<div class="divider"> / </div>
 							{{if eq $i $l}}
-								<input id="file-name" value="{{$v}}" placeholder="{{$.i18n.Tr "repo.editor.name_your_file"}}" data-ec-url-prefix="{{$.EditorconfigURLPrefix}}" required autofocus>
+								<input id="file-name" value="{{$v}}" placeholder="{{$.i18n.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.Editorconfig}}" required autofocus>
 								<span class="poping up" data-content="{{$.i18n.Tr "repo.editor.filename_help"}}" data-position="bottom center" data-variation="tiny inverted">{{svg "octicon-info" 16}}</span>
 							{{else}}
 								<span class="section"><a href="{{EscapePound $.BranchLink}}/{{index $.TreePaths $i | EscapePound}}">{{$v}}</a></span>
@@ -41,11 +41,14 @@
 						data-markdown-file-exts="{{.MarkdownFileExts}}"
 						data-line-wrap-extensions="{{.LineWrapExtensions}}">
 {{.FileContent}}</textarea>
+					<div class="editor-loading">
+						{{.i18n.Tr "loading"}}
+					</div>
 				</div>
 				<div class="ui bottom attached tab segment markdown" data-tab="preview">
 					{{.i18n.Tr "loading"}}
 				</div>
-				<div class="ui bottom attached tab segment diff" data-tab="diff">
+				<div class="ui bottom attached tab segment diff edit-diff" data-tab="diff">
 					{{.i18n.Tr "loading"}}
 				</div>
 			</div>
diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js
new file mode 100644
index 000000000000..0999d05f05c0
--- /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 a74fba34e852..02189a5f1379 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 b000c1af77ad..b511c9981d2d 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 714d41649a33..d8ba1467e9a6 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 863f2bad8ebc..6fb089636a7a 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 d56b7b8eebd3..19689d107be4 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 e87dd770cbad..d6a632ad1f97 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,
+  },
 };