summaryrefslogtreecommitdiffstats
path: root/web_src
diff options
context:
space:
mode:
Diffstat (limited to 'web_src')
-rw-r--r--web_src/js/features/codeeditor.js104
-rw-r--r--web_src/js/index.js165
-rw-r--r--web_src/js/utils.js22
-rw-r--r--web_src/less/_editor.less37
-rw-r--r--web_src/less/_repository.less16
-rw-r--r--web_src/less/themes/theme-arc-green.less19
6 files changed, 184 insertions, 179 deletions
diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js
new file mode 100644
index 0000000000..0999d05f05
--- /dev/null
+++ b/web_src/js/features/codeeditor.js
@@ -0,0 +1,104 @@
+import {basename, extname, isObject, isDarkTheme} from '../utils.js';
+
+const languagesByFilename = {};
+const languagesByExt = {};
+
+function getEditorconfig(input) {
+ try {
+ return JSON.parse(input.dataset.editorconfig);
+ } catch (_err) {
+ return null;
+ }
+}
+
+function initLanguages(monaco) {
+ for (const {filenames, extensions, id} of monaco.languages.getLanguages()) {
+ for (const filename of filenames || []) {
+ languagesByFilename[filename] = id;
+ }
+ for (const extension of extensions || []) {
+ languagesByExt[extension] = id;
+ }
+ }
+}
+
+function getLanguage(filename) {
+ return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext';
+}
+
+function updateEditor(monaco, editor, filenameInput) {
+ const newFilename = filenameInput.value;
+ editor.updateOptions(getOptions(filenameInput));
+ const model = editor.getModel();
+ const language = model.getModeId();
+ const newLanguage = getLanguage(newFilename);
+ if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage);
+}
+
+export async function createCodeEditor(textarea, filenameInput, previewFileModes) {
+ const filename = basename(filenameInput.value);
+ const previewLink = document.querySelector('a[data-tab=preview]');
+ const markdownExts = (textarea.dataset.markdownFileExts || '').split(',');
+ const lineWrapExts = (textarea.dataset.lineWrapExtensions || '').split(',');
+ const isMarkdown = markdownExts.includes(extname(filename));
+
+ if (previewLink) {
+ if (isMarkdown && (previewFileModes || []).includes('markdown')) {
+ previewLink.dataset.url = previewLink.dataset.url.replace(/(.*)\/.*/i, `$1/markdown`);
+ previewLink.style.display = '';
+ } else {
+ previewLink.style.display = 'none';
+ }
+ }
+
+ const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor');
+ initLanguages(monaco);
+
+ const container = document.createElement('div');
+ container.className = 'monaco-editor-container';
+ textarea.parentNode.appendChild(container);
+
+ const editor = monaco.editor.create(container, {
+ value: textarea.value,
+ language: getLanguage(filename),
+ ...getOptions(filenameInput, lineWrapExts),
+ });
+
+ const model = editor.getModel();
+ model.onDidChangeContent(() => {
+ textarea.value = editor.getValue();
+ textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure
+ });
+
+ window.addEventListener('resize', () => {
+ editor.layout();
+ });
+
+ filenameInput.addEventListener('keyup', () => {
+ updateEditor(monaco, editor, filenameInput);
+ });
+
+ const loading = document.querySelector('.editor-loading');
+ if (loading) loading.remove();
+
+ return editor;
+}
+
+function getOptions(filenameInput, lineWrapExts) {
+ const ec = getEditorconfig(filenameInput);
+ const theme = isDarkTheme() ? 'vs-dark' : 'vs';
+ const wordWrap = (lineWrapExts || []).includes(extname(filenameInput.value)) ? 'on' : 'off';
+
+ const opts = {theme, wordWrap};
+ if (isObject(ec)) {
+ opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec);
+ if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size);
+ if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize;
+ if ('max_line_length' in ec) opts.rulers = [Number(ec.max_line_length)];
+ opts.trimAutoWhitespace = ec.trim_trailing_whitespace === true;
+ opts.insertSpaces = ec.indent_style === 'space';
+ opts.useTabStops = ec.indent_style === 'tab';
+ }
+
+ return opts;
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index a74fba34e8..02189a5f13 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -20,6 +20,7 @@ import createDropzone from './features/dropzone.js';
import highlight from './features/highlight.js';
import ActivityTopAuthors from './components/ActivityTopAuthors.vue';
import {initNotificationsTable, initNotificationCount} from './features/notification.js';
+import {createCodeEditor} from './features/codeeditor.js';
const {AppSubUrl, StaticUrlPrefix, csrf} = window.config;
@@ -28,9 +29,7 @@ function htmlEncode(text) {
}
let previewFileModes;
-let simpleMDEditor;
const commentMDEditors = {};
-let codeMirrorEditor;
// Silence fomantic's error logging when tabs are used without a target content element
$.fn.tab.settings.silent = true;
@@ -1467,62 +1466,6 @@ $.fn.getCursorPosition = function () {
return pos;
};
-function setSimpleMDE($editArea) {
- if (codeMirrorEditor) {
- codeMirrorEditor.toTextArea();
- codeMirrorEditor = null;
- }
-
- if (simpleMDEditor) {
- return true;
- }
-
- simpleMDEditor = new SimpleMDE({
- autoDownloadFontAwesome: false,
- element: $editArea[0],
- forceSync: true,
- renderingConfig: {
- singleLineBreaks: false
- },
- indentWithTabs: false,
- tabSize: 4,
- spellChecker: false,
- previewRender(plainText, preview) { // Async method
- setTimeout(() => {
- // FIXME: still send render request when return back to edit mode
- $.post($editArea.data('url'), {
- _csrf: csrf,
- mode: 'gfm',
- context: $editArea.data('context'),
- text: plainText
- }, (data) => {
- preview.innerHTML = `<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 b000c1af77..b511c9981d 100644
--- a/web_src/js/utils.js
+++ b/web_src/js/utils.js
@@ -1,3 +1,25 @@
+// retrieve a HTML string for given SVG icon name and size in pixels
export function svg(name, size) {
return `<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 714d41649a..d8ba1467e9 100644
--- a/web_src/less/_editor.less
+++ b/web_src/less/_editor.less
@@ -32,3 +32,40 @@
.editor-toolbar i.separator {
border-left: none;
}
+
+.editor-loading {
+ padding: 1rem;
+ text-align: center;
+}
+
+.edit-diff {
+ padding: 0 !important;
+}
+
+.edit-diff > div > .ui.table {
+ border-top: none !important;
+ border-bottom: none !important;
+ border-left: 1px solid #d4d4d5 !important;
+ border-right: 1px solid #d4d4d5 !important;
+}
+
+#edit_area {
+ display: none;
+}
+
+.monaco-editor-container {
+ width: 100%;
+ min-height: 200px;
+ height: 90vh;
+}
+
+/* overwrite conflicting styles from fomantic */
+.monaco-editor-container .inputarea {
+ min-height: 0 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ resize: none !important;
+ border: none !important;
+ color: transparent !important;
+ background-color: transparent !important;
+}
diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less
index 863f2bad8e..6fb089636a 100644
--- a/web_src/less/_repository.less
+++ b/web_src/less/_repository.less
@@ -1555,14 +1555,6 @@
text-align: center;
}
- .removed-code {
- background-color: #ff9999;
- }
-
- .added-code {
- background-color: #99ff99;
- }
-
[data-line-num]::before {
content: attr(data-line-num);
text-align: right;
@@ -2865,3 +2857,11 @@ td.blob-excerpt {
height: 48px;
overflow: hidden;
}
+
+.removed-code {
+ background-color: #ff9999;
+}
+
+.added-code {
+ background-color: #99ff99;
+}
diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less
index d56b7b8eeb..19689d107b 100644
--- a/web_src/less/themes/theme-arc-green.less
+++ b/web_src/less/themes/theme-arc-green.less
@@ -576,10 +576,6 @@ a.ui.basic.green.label:hover {
.repository.file.editor.edit,
.repository.wiki.new .CodeMirror {
- border-right: 1px solid rgba(187, 187, 187, .6);
- border-left: 1px solid rgba(187, 187, 187, .6);
- border-bottom: 1px solid rgba(187, 187, 187, .6);
-
.editor-preview,
.editor-preview-side,
& + .editor-preview-side {
@@ -751,7 +747,11 @@ a.ui.basic.green.label:hover {
border-color: #314a37 !important;
}
-.repository .diff-file-box .code-diff tbody tr .added-code {
+.removed-code {
+ background-color: #5f3737;
+}
+
+.added-code {
background-color: #3a523a;
}
@@ -766,10 +766,6 @@ a.ui.basic.green.label:hover {
color: #8ab398;
}
-.repository .diff-file-box .code-diff tbody tr .removed-code {
- background-color: #5f3737;
-}
-
.tag-code,
.tag-code td {
background: #242637 !important;
@@ -1300,6 +1296,11 @@ a.ui.labels .label:hover {
border-color: #7f98ad;
}
+.edit-diff > div > .ui.table {
+ border-left-color: #404552 !important;
+ border-right-color: #404552 !important;
+}
+
.editor-toolbar a {
color: #87ab63 !important;
}