Skip to content

Commit 9249c81

Browse files
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 <[email protected]>
1 parent 984ee01 commit 9249c81

18 files changed

+304
-197
lines changed

.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ rules:
6060
no-unused-vars: [2, {args: all, argsIgnorePattern: ^_, varsIgnorePattern: ^_, ignoreRestSiblings: true}]
6161
no-use-before-define: [0]
6262
no-var: [2]
63+
object-curly-newline: [0]
6364
object-curly-spacing: [2, never]
6465
one-var-declaration-per-line: [0]
6566
one-var: [0]

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ coverage.all
7777
/yarn.lock
7878
/public/js
7979
/public/css
80+
/public/fonts
8081
/public/fomantic
8182
/public/img/svg
8283
/VERSION

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/integrations/migration-test,$(fi
8888
WEBPACK_SOURCES := $(shell find web_src/js web_src/less -type f)
8989
WEBPACK_CONFIGS := webpack.config.js
9090
WEBPACK_DEST := public/js/index.js public/css/index.css
91-
WEBPACK_DEST_DIRS := public/js public/css
91+
WEBPACK_DEST_DIRS := public/js public/css public/fonts
9292

9393
BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
9494
BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))
@@ -295,7 +295,7 @@ lint-frontend: node_modules
295295

296296
.PHONY: watch-frontend
297297
watch-frontend: node_modules
298-
NODE_ENV=development npx webpack --hide-modules --display-entrypoints=false --watch
298+
NODE_ENV=development npx webpack --hide-modules --display-entrypoints=false --watch --progress
299299

300300
.PHONY: test
301301
test:
@@ -598,6 +598,7 @@ $(FOMANTIC_DEST): $(FOMANTIC_CONFIGS) package-lock.json | node_modules
598598
webpack: $(WEBPACK_DEST)
599599

600600
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json | node_modules
601+
rm -rf $(WEBPACK_DEST_DIRS)
601602
npx webpack --hide-modules --display-entrypoints=false
602603
@touch $(WEBPACK_DEST)
603604

custom/conf/app.ini.sample

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ DEFAULT_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.pulls,repo.wiki
5252
PREFIX_ARCHIVE_FILES = true
5353

5454
[repository.editor]
55-
; List of file extensions for which lines should be wrapped in the CodeMirror editor
55+
; List of file extensions for which lines should be wrapped in the Monaco editor
5656
; Separate extensions with a comma. To line wrap files without an extension, just put a comma
5757
LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd,
5858
; Valid file modes that have a preview API associated with them, such as api/v1/markdown

package-lock.json

Lines changed: 41 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"domino": "2.1.5",
2020
"dropzone": "5.7.0",
2121
"fast-glob": "3.2.2",
22+
"file-loader": "6.0.0",
2223
"fomantic-ui": "2.8.4",
2324
"highlight.js": "10.0.2",
2425
"imports-loader": "0.8.0",
@@ -27,6 +28,8 @@
2728
"jquery.are-you-sure": "1.9.0",
2829
"less-loader": "6.0.0",
2930
"mini-css-extract-plugin": "0.9.0",
31+
"monaco-editor": "0.20.0",
32+
"monaco-editor-webpack-plugin": "1.9.0",
3033
"optimize-css-assets-webpack-plugin": "5.0.3",
3134
"postcss-loader": "3.0.0",
3235
"postcss-preset-env": "6.7.0",
@@ -35,7 +38,7 @@
3538
"svgo": "1.3.2",
3639
"svgo-loader": "2.2.1",
3740
"swagger-ui": "3.25.1",
38-
"terser-webpack-plugin": "3.0.0",
41+
"terser-webpack-plugin": "3.0.1",
3942
"vue": "2.6.11",
4043
"vue-bar-graph": "1.2.0",
4144
"vue-calendar-heatmap": "0.8.4",

routers/repo/editor.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package repo
66

77
import (
8+
"encoding/json"
89
"fmt"
910
"io/ioutil"
1011
"path"
@@ -146,11 +147,24 @@ func editFile(ctx *context.Context, isNewFile bool) {
146147
ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
147148
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
148149
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
149-
ctx.Data["EditorconfigURLPrefix"] = fmt.Sprintf("%s/api/v1/repos/%s/editorconfig/", setting.AppSubURL, ctx.Repo.Repository.FullName())
150+
ctx.Data["Editorconfig"] = GetEditorConfig(ctx, treePath)
150151

151152
ctx.HTML(200, tplEditFile)
152153
}
153154

155+
// GetEditorConfig returns a editorconfig JSON string for given treePath or "null"
156+
func GetEditorConfig(ctx *context.Context, treePath string) string {
157+
ec, err := ctx.Repo.GetEditorconfig()
158+
if err == nil {
159+
def, err := ec.GetDefinitionForFilename(treePath)
160+
if err == nil {
161+
jsonStr, _ := json.Marshal(def)
162+
return string(jsonStr)
163+
}
164+
}
165+
return "null"
166+
}
167+
154168
// EditFile render edit file page
155169
func EditFile(ctx *context.Context) {
156170
editFile(ctx, false)
@@ -186,6 +200,7 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo
186200
ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
187201
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
188202
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
203+
ctx.Data["Editorconfig"] = GetEditorConfig(ctx, form.TreePath)
189204

190205
if ctx.HasError() {
191206
ctx.HTML(200, tplEditFile)

templates/base/head.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html lang="{{.Language}}">
2+
<html lang="{{.Language}}" class="theme-{{.SignedUser.Theme}}">
33
<head data-suburl="{{AppSubUrl}}">
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">

templates/pwa/serviceworker_js.tmpl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,17 @@ var urlsToCache = [
4545
'{{StaticUrlPrefix}}/vendor/assets/roboto-fonts/roboto-v20-latin-ext_cyrillic-ext_latin_greek_vietnamese_cyrillic_greek-ext-regular.woff2',
4646
'{{StaticUrlPrefix}}/vendor/assets/roboto-fonts/roboto-v20-latin-ext_cyrillic-ext_latin_greek_vietnamese_cyrillic_greek-ext-italic.woff2',
4747
'{{StaticUrlPrefix}}/vendor/assets/roboto-fonts/roboto-v20-latin-ext_cyrillic-ext_latin_greek_vietnamese_cyrillic_greek-ext-700.woff2',
48-
'{{StaticUrlPrefix}}/vendor/assets/roboto-fonts/roboto-v20-latin-ext_cyrillic-ext_latin_greek_vietnamese_cyrillic_greek-ext-700italic.woff2'
48+
'{{StaticUrlPrefix}}/vendor/assets/roboto-fonts/roboto-v20-latin-ext_cyrillic-ext_latin_greek_vietnamese_cyrillic_greek-ext-700italic.woff2',
49+
50+
// monaco
51+
'{{StaticUrlPrefix}}/css/monaco.css',
52+
'{{StaticUrlPrefix}}/fonts/codicon.ttf',
53+
'{{StaticUrlPrefix}}/js/monaco-css.worker.js',
54+
'{{StaticUrlPrefix}}/js/monaco-editor.worker.js',
55+
'{{StaticUrlPrefix}}/js/monaco-html.worker.js',
56+
'{{StaticUrlPrefix}}/js/monaco-json.worker.js',
57+
'{{StaticUrlPrefix}}/js/monaco.js',
58+
'{{StaticUrlPrefix}}/js/monaco-ts.worker.js'
4959
];
5060

5161
self.addEventListener('install', function (event) {

templates/repo/editor/diff_preview.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="diff-file-box">
22
<div class="ui attached table segment">
3-
<div class="file-body file-code code-view code-diff">
3+
<div class="file-body file-code code-view code-diff-unified">
44
<table>
55
<tbody>
66
{{template "repo/diff/section_unified" dict "file" .File "root" $}}

templates/repo/editor/edit.tmpl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
{{range $i, $v := .TreeNames}}
1616
<div class="divider"> / </div>
1717
{{if eq $i $l}}
18-
<input id="file-name" value="{{$v}}" placeholder="{{$.i18n.Tr "repo.editor.name_your_file"}}" data-ec-url-prefix="{{$.EditorconfigURLPrefix}}" required autofocus>
18+
<input id="file-name" value="{{$v}}" placeholder="{{$.i18n.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.Editorconfig}}" required autofocus>
1919
<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>
2020
{{else}}
2121
<span class="section"><a href="{{EscapePound $.BranchLink}}/{{index $.TreePaths $i | EscapePound}}">{{$v}}</a></span>
@@ -41,11 +41,14 @@
4141
data-markdown-file-exts="{{.MarkdownFileExts}}"
4242
data-line-wrap-extensions="{{.LineWrapExtensions}}">
4343
{{.FileContent}}</textarea>
44+
<div class="editor-loading">
45+
{{.i18n.Tr "loading"}}
46+
</div>
4447
</div>
4548
<div class="ui bottom attached tab segment markdown" data-tab="preview">
4649
{{.i18n.Tr "loading"}}
4750
</div>
48-
<div class="ui bottom attached tab segment diff" data-tab="diff">
51+
<div class="ui bottom attached tab segment diff edit-diff" data-tab="diff">
4952
{{.i18n.Tr "loading"}}
5053
</div>
5154
</div>

web_src/js/features/codeeditor.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {basename, extname, isObject, isDarkTheme} from '../utils.js';
2+
3+
const languagesByFilename = {};
4+
const languagesByExt = {};
5+
6+
function getEditorconfig(input) {
7+
try {
8+
return JSON.parse(input.dataset.editorconfig);
9+
} catch (_err) {
10+
return null;
11+
}
12+
}
13+
14+
function initLanguages(monaco) {
15+
for (const {filenames, extensions, id} of monaco.languages.getLanguages()) {
16+
for (const filename of filenames || []) {
17+
languagesByFilename[filename] = id;
18+
}
19+
for (const extension of extensions || []) {
20+
languagesByExt[extension] = id;
21+
}
22+
}
23+
}
24+
25+
function getLanguage(filename) {
26+
return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext';
27+
}
28+
29+
function updateEditor(monaco, editor, filenameInput) {
30+
const newFilename = filenameInput.value;
31+
editor.updateOptions(getOptions(filenameInput));
32+
const model = editor.getModel();
33+
const language = model.getModeId();
34+
const newLanguage = getLanguage(newFilename);
35+
if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage);
36+
}
37+
38+
export async function createCodeEditor(textarea, filenameInput, previewFileModes) {
39+
const filename = basename(filenameInput.value);
40+
const previewLink = document.querySelector('a[data-tab=preview]');
41+
const markdownExts = (textarea.dataset.markdownFileExts || '').split(',');
42+
const lineWrapExts = (textarea.dataset.lineWrapExtensions || '').split(',');
43+
const isMarkdown = markdownExts.includes(extname(filename));
44+
45+
if (previewLink) {
46+
if (isMarkdown && (previewFileModes || []).includes('markdown')) {
47+
previewLink.dataset.url = previewLink.dataset.url.replace(/(.*)\/.*/i, `$1/markdown`);
48+
previewLink.style.display = '';
49+
} else {
50+
previewLink.style.display = 'none';
51+
}
52+
}
53+
54+
const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor');
55+
initLanguages(monaco);
56+
57+
const container = document.createElement('div');
58+
container.className = 'monaco-editor-container';
59+
textarea.parentNode.appendChild(container);
60+
61+
const editor = monaco.editor.create(container, {
62+
value: textarea.value,
63+
language: getLanguage(filename),
64+
...getOptions(filenameInput, lineWrapExts),
65+
});
66+
67+
const model = editor.getModel();
68+
model.onDidChangeContent(() => {
69+
textarea.value = editor.getValue();
70+
textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure
71+
});
72+
73+
window.addEventListener('resize', () => {
74+
editor.layout();
75+
});
76+
77+
filenameInput.addEventListener('keyup', () => {
78+
updateEditor(monaco, editor, filenameInput);
79+
});
80+
81+
const loading = document.querySelector('.editor-loading');
82+
if (loading) loading.remove();
83+
84+
return editor;
85+
}
86+
87+
function getOptions(filenameInput, lineWrapExts) {
88+
const ec = getEditorconfig(filenameInput);
89+
const theme = isDarkTheme() ? 'vs-dark' : 'vs';
90+
const wordWrap = (lineWrapExts || []).includes(extname(filenameInput.value)) ? 'on' : 'off';
91+
92+
const opts = {theme, wordWrap};
93+
if (isObject(ec)) {
94+
opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec);
95+
if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size);
96+
if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize;
97+
if ('max_line_length' in ec) opts.rulers = [Number(ec.max_line_length)];
98+
opts.trimAutoWhitespace = ec.trim_trailing_whitespace === true;
99+
opts.insertSpaces = ec.indent_style === 'space';
100+
opts.useTabStops = ec.indent_style === 'tab';
101+
}
102+
103+
return opts;
104+
}

0 commit comments

Comments
 (0)