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 <john.olheiser@gmail.com>
This commit is contained in:
silverwind 2020-05-14 18:06:01 +02:00 committed by GitHub
parent 984ee0113b
commit 9249c810b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 304 additions and 197 deletions

View file

@ -60,6 +60,7 @@ rules:
no-unused-vars: [2, {args: all, argsIgnorePattern: ^_, varsIgnorePattern: ^_, ignoreRestSiblings: true}] no-unused-vars: [2, {args: all, argsIgnorePattern: ^_, varsIgnorePattern: ^_, ignoreRestSiblings: true}]
no-use-before-define: [0] no-use-before-define: [0]
no-var: [2] no-var: [2]
object-curly-newline: [0]
object-curly-spacing: [2, never] object-curly-spacing: [2, never]
one-var-declaration-per-line: [0] one-var-declaration-per-line: [0]
one-var: [0] one-var: [0]

1
.gitignore vendored
View file

@ -77,6 +77,7 @@ coverage.all
/yarn.lock /yarn.lock
/public/js /public/js
/public/css /public/css
/public/fonts
/public/fomantic /public/fomantic
/public/img/svg /public/img/svg
/VERSION /VERSION

View file

@ -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_SOURCES := $(shell find web_src/js web_src/less -type f)
WEBPACK_CONFIGS := webpack.config.js WEBPACK_CONFIGS := webpack.config.js
WEBPACK_DEST := public/js/index.js public/css/index.css 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_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST)) BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))
@ -295,7 +295,7 @@ lint-frontend: node_modules
.PHONY: watch-frontend .PHONY: watch-frontend
watch-frontend: node_modules 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 .PHONY: test
test: test:
@ -598,6 +598,7 @@ $(FOMANTIC_DEST): $(FOMANTIC_CONFIGS) package-lock.json | node_modules
webpack: $(WEBPACK_DEST) webpack: $(WEBPACK_DEST)
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json | node_modules $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json | node_modules
rm -rf $(WEBPACK_DEST_DIRS)
npx webpack --hide-modules --display-entrypoints=false npx webpack --hide-modules --display-entrypoints=false
@touch $(WEBPACK_DEST) @touch $(WEBPACK_DEST)

View file

@ -52,7 +52,7 @@ DEFAULT_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.pulls,repo.wiki
PREFIX_ARCHIVE_FILES = true PREFIX_ARCHIVE_FILES = true
[repository.editor] [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 ; Separate extensions with a comma. To line wrap files without an extension, just put a comma
LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd, LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd,
; Valid file modes that have a preview API associated with them, such as api/v1/markdown ; Valid file modes that have a preview API associated with them, such as api/v1/markdown

48
package-lock.json generated
View file

@ -4861,6 +4861,27 @@
"flat-cache": "^2.0.1" "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": { "file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@ -7903,9 +7924,9 @@
} }
}, },
"jest-worker": { "jest-worker": {
"version": "25.5.0", "version": "26.0.0",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz",
"integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", "integrity": "sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw==",
"requires": { "requires": {
"merge-stream": "^2.0.0", "merge-stream": "^2.0.0",
"supports-color": "^7.0.0" "supports-color": "^7.0.0"
@ -9255,6 +9276,19 @@
"minimist": "^1.2.5" "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": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -13684,13 +13718,13 @@
} }
}, },
"terser-webpack-plugin": { "terser-webpack-plugin": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-3.0.0.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-3.0.1.tgz",
"integrity": "sha512-gHAVFtJz1gQW5cu0btPtb+5Syo7K9hRj3b0lstgfglaBhbtcOCizsaPTnxOBGmF9iIgwsrSIiraBa2xzuWND7Q==", "integrity": "sha512-eFDtq8qPUEa9hXcUzTwKXTnugIVtlqc1Z/ZVhG8LmRT3lgRY13+pQTnFLY2N7ATB6TKCHuW/IGjoAnZz9wOIqw==",
"requires": { "requires": {
"cacache": "^15.0.3", "cacache": "^15.0.3",
"find-cache-dir": "^3.3.1", "find-cache-dir": "^3.3.1",
"jest-worker": "^25.5.0", "jest-worker": "^26.0.0",
"p-limit": "^2.3.0", "p-limit": "^2.3.0",
"schema-utils": "^2.6.6", "schema-utils": "^2.6.6",
"serialize-javascript": "^3.0.0", "serialize-javascript": "^3.0.0",

View file

@ -19,6 +19,7 @@
"domino": "2.1.5", "domino": "2.1.5",
"dropzone": "5.7.0", "dropzone": "5.7.0",
"fast-glob": "3.2.2", "fast-glob": "3.2.2",
"file-loader": "6.0.0",
"fomantic-ui": "2.8.4", "fomantic-ui": "2.8.4",
"highlight.js": "10.0.2", "highlight.js": "10.0.2",
"imports-loader": "0.8.0", "imports-loader": "0.8.0",
@ -27,6 +28,8 @@
"jquery.are-you-sure": "1.9.0", "jquery.are-you-sure": "1.9.0",
"less-loader": "6.0.0", "less-loader": "6.0.0",
"mini-css-extract-plugin": "0.9.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", "optimize-css-assets-webpack-plugin": "5.0.3",
"postcss-loader": "3.0.0", "postcss-loader": "3.0.0",
"postcss-preset-env": "6.7.0", "postcss-preset-env": "6.7.0",
@ -35,7 +38,7 @@
"svgo": "1.3.2", "svgo": "1.3.2",
"svgo-loader": "2.2.1", "svgo-loader": "2.2.1",
"swagger-ui": "3.25.1", "swagger-ui": "3.25.1",
"terser-webpack-plugin": "3.0.0", "terser-webpack-plugin": "3.0.1",
"vue": "2.6.11", "vue": "2.6.11",
"vue-bar-graph": "1.2.0", "vue-bar-graph": "1.2.0",
"vue-calendar-heatmap": "0.8.4", "vue-calendar-heatmap": "0.8.4",

View file

@ -5,6 +5,7 @@
package repo package repo
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"path" "path"
@ -146,11 +147,24 @@ func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") 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) 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 // EditFile render edit file page
func EditFile(ctx *context.Context) { func EditFile(ctx *context.Context) {
editFile(ctx, false) 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["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
ctx.Data["Editorconfig"] = GetEditorConfig(ctx, form.TreePath)
if ctx.HasError() { if ctx.HasError() {
ctx.HTML(200, tplEditFile) ctx.HTML(200, tplEditFile)

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{.Language}}"> <html lang="{{.Language}}" class="theme-{{.SignedUser.Theme}}">
<head data-suburl="{{AppSubUrl}}"> <head data-suburl="{{AppSubUrl}}">
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">

View file

@ -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-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-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-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) { self.addEventListener('install', function (event) {

View file

@ -1,6 +1,6 @@
<div class="diff-file-box"> <div class="diff-file-box">
<div class="ui attached table segment"> <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> <table>
<tbody> <tbody>
{{template "repo/diff/section_unified" dict "file" .File "root" $}} {{template "repo/diff/section_unified" dict "file" .File "root" $}}

View file

@ -15,7 +15,7 @@
{{range $i, $v := .TreeNames}} {{range $i, $v := .TreeNames}}
<div class="divider"> / </div> <div class="divider"> / </div>
{{if eq $i $l}} {{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> <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}} {{else}}
<span class="section"><a href="{{EscapePound $.BranchLink}}/{{index $.TreePaths $i | EscapePound}}">{{$v}}</a></span> <span class="section"><a href="{{EscapePound $.BranchLink}}/{{index $.TreePaths $i | EscapePound}}">{{$v}}</a></span>
@ -41,11 +41,14 @@
data-markdown-file-exts="{{.MarkdownFileExts}}" data-markdown-file-exts="{{.MarkdownFileExts}}"
data-line-wrap-extensions="{{.LineWrapExtensions}}"> data-line-wrap-extensions="{{.LineWrapExtensions}}">
{{.FileContent}}</textarea> {{.FileContent}}</textarea>
<div class="editor-loading">
{{.i18n.Tr "loading"}}
</div>
</div> </div>
<div class="ui bottom attached tab segment markdown" data-tab="preview"> <div class="ui bottom attached tab segment markdown" data-tab="preview">
{{.i18n.Tr "loading"}} {{.i18n.Tr "loading"}}
</div> </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"}} {{.i18n.Tr "loading"}}
</div> </div>
</div> </div>

View file

@ -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;
}

View file

@ -20,6 +20,7 @@ import createDropzone from './features/dropzone.js';
import highlight from './features/highlight.js'; import highlight from './features/highlight.js';
import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; import ActivityTopAuthors from './components/ActivityTopAuthors.vue';
import {initNotificationsTable, initNotificationCount} from './features/notification.js'; import {initNotificationsTable, initNotificationCount} from './features/notification.js';
import {createCodeEditor} from './features/codeeditor.js';
const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; const {AppSubUrl, StaticUrlPrefix, csrf} = window.config;
@ -28,9 +29,7 @@ function htmlEncode(text) {
} }
let previewFileModes; let previewFileModes;
let simpleMDEditor;
const commentMDEditors = {}; const commentMDEditors = {};
let codeMirrorEditor;
// Silence fomantic's error logging when tabs are used without a target content element // Silence fomantic's error logging when tabs are used without a target content element
$.fn.tab.settings.silent = true; $.fn.tab.settings.silent = true;
@ -1467,62 +1466,6 @@ $.fn.getCursorPosition = function () {
return pos; 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) { function setCommentSimpleMDE($editArea) {
const simplemde = new SimpleMDE({ const simplemde = new SimpleMDE({
autoDownloadFontAwesome: false, autoDownloadFontAwesome: false,
@ -1569,27 +1512,7 @@ function setCommentSimpleMDE($editArea) {
return simplemde; return simplemde;
} }
function setCodeMirror($editArea) { async function initEditor() {
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() {
$('.js-quick-pull-choice-option').on('change', function () { $('.js-quick-pull-choice-option').on('change', function () {
if ($(this).val() === 'commit-to-new-branch') { if ($(this).val() === 'commit-to-new-branch') {
$('.quick-pull-branch-name').show(); $('.quick-pull-branch-name').show();
@ -1650,89 +1573,7 @@ function initEditor() {
const $editArea = $('.repository.editor textarea#edit_area'); const $editArea = $('.repository.editor textarea#edit_area');
if (!$editArea.length) return; if (!$editArea.length) return;
const markdownFileExts = $editArea.data('markdown-file-exts').split(','); await createCodeEditor($editArea[0], $editFilename[0], previewFileModes);
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');
// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
// to enable or disable the commit button // to enable or disable the commit button

View file

@ -1,3 +1,25 @@
// retrieve a HTML string for given SVG icon name and size in pixels
export function svg(name, size) { export function svg(name, size) {
return `<svg class="svg ${name}" width="${size}" height="${size}" aria-hidden="true"><use xlink:href="#${name}"/></svg>`; 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');
}

View file

@ -32,3 +32,40 @@
.editor-toolbar i.separator { .editor-toolbar i.separator {
border-left: none; 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;
}

View file

@ -1555,14 +1555,6 @@
text-align: center; text-align: center;
} }
.removed-code {
background-color: #ff9999;
}
.added-code {
background-color: #99ff99;
}
[data-line-num]::before { [data-line-num]::before {
content: attr(data-line-num); content: attr(data-line-num);
text-align: right; text-align: right;
@ -2865,3 +2857,11 @@ td.blob-excerpt {
height: 48px; height: 48px;
overflow: hidden; overflow: hidden;
} }
.removed-code {
background-color: #ff9999;
}
.added-code {
background-color: #99ff99;
}

View file

@ -576,10 +576,6 @@ a.ui.basic.green.label:hover {
.repository.file.editor.edit, .repository.file.editor.edit,
.repository.wiki.new .CodeMirror { .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,
.editor-preview-side, .editor-preview-side,
& + .editor-preview-side { & + .editor-preview-side {
@ -751,7 +747,11 @@ a.ui.basic.green.label:hover {
border-color: #314a37 !important; border-color: #314a37 !important;
} }
.repository .diff-file-box .code-diff tbody tr .added-code { .removed-code {
background-color: #5f3737;
}
.added-code {
background-color: #3a523a; background-color: #3a523a;
} }
@ -766,10 +766,6 @@ a.ui.basic.green.label:hover {
color: #8ab398; color: #8ab398;
} }
.repository .diff-file-box .code-diff tbody tr .removed-code {
background-color: #5f3737;
}
.tag-code, .tag-code,
.tag-code td { .tag-code td {
background: #242637 !important; background: #242637 !important;
@ -1300,6 +1296,11 @@ a.ui.labels .label:hover {
border-color: #7f98ad; border-color: #7f98ad;
} }
.edit-diff > div > .ui.table {
border-left-color: #404552 !important;
border-right-color: #404552 !important;
}
.editor-toolbar a { .editor-toolbar a {
color: #87ab63 !important; color: #87ab63 !important;
} }

View file

@ -2,6 +2,7 @@ const cssnano = require('cssnano');
const fastGlob = require('fast-glob'); const fastGlob = require('fast-glob');
const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries'); const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const PostCSSPresetEnv = require('postcss-preset-env'); const PostCSSPresetEnv = require('postcss-preset-env');
const PostCSSSafeParser = require('postcss-safe-parser'); const PostCSSSafeParser = require('postcss-safe-parser');
@ -76,6 +77,14 @@ module.exports = {
splitChunks: { splitChunks: {
chunks: 'async', chunks: 'async',
name: (_, chunks) => chunks.map((item) => item.name).join('-'), 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: { module: {
@ -91,6 +100,7 @@ module.exports = {
}, },
{ {
test: /\.worker\.js$/, test: /\.worker\.js$/,
exclude: /monaco/,
use: [ use: [
{ {
loader: 'worker-loader', loader: 'worker-loader',
@ -149,7 +159,10 @@ module.exports = {
loader: 'css-loader', loader: 'css-loader',
options: { options: {
importLoaders: 2, 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: [ plugins: [
@ -209,9 +235,14 @@ module.exports = {
new SpriteLoaderPlugin({ new SpriteLoaderPlugin({
plainSprite: true, plainSprite: true,
}), }),
new MonacoWebpackPlugin({
filename: 'js/monaco-[name].worker.js',
}),
], ],
performance: { performance: {
hints: false, hints: false,
maxEntrypointSize: Infinity,
maxAssetSize: Infinity,
}, },
resolve: { resolve: {
symlinks: false, symlinks: false,
@ -224,4 +255,7 @@ module.exports = {
'node_modules/**', 'node_modules/**',
], ],
}, },
stats: {
children: false,
},
}; };