summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2017-11-17 10:46:26 +0000
committerPhil Hughes <me@iamphill.com>2017-11-17 10:46:26 +0000
commit7f26e4307c9bb4d1d3a06471c3a7b38a9cd50689 (patch)
tree2192d48dd8d363e2a0f832a4c7308977fd5ef5f8
parent655dca954ea3e3e4556d97cf905e482f975999fe (diff)
downloadgitlab-ce-multi-file-editor-dirty-diff-indicator.tar.gz
created editor library to manage all things editormulti-file-editor-dirty-diff-indicator
[ci skip]
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue83
-rw-r--r--app/assets/javascripts/repo/lib/common/model.js46
-rw-r--r--app/assets/javascripts/repo/lib/decorations/controller.js46
-rw-r--r--app/assets/javascripts/repo/lib/diff/controller.js73
-rw-r--r--app/assets/javascripts/repo/lib/diff/worker.js78
-rw-r--r--app/assets/javascripts/repo/lib/editor.js69
-rw-r--r--app/assets/javascripts/repo/services/index.js4
-rw-r--r--app/assets/stylesheets/pages/repo.scss22
8 files changed, 344 insertions, 77 deletions
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index 3c06e75a472..e0ce8a97665 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -3,21 +3,17 @@
import { mapGetters, mapActions } from 'vuex';
import flash from '../../flash';
import monacoLoader from '../monaco_loader';
+import editor from '../lib/editor';
export default {
destroyed() {
- if (this.monacoInstance) {
- this.monacoInstance.destroy();
- }
+ editor.dispose();
},
mounted() {
if (this.monaco) {
this.initMonaco();
} else {
- monacoLoader(['vs/editor/editor.main', 'vs/editor/common/diff/diffComputer'], (_, { DiffComputer }) => {
- this.monaco = monaco;
- this.DiffComputer = DiffComputer;
-
+ monacoLoader(['vs/editor/editor.main'], () => {
this.initMonaco();
});
}
@@ -30,84 +26,25 @@ export default {
initMonaco() {
if (this.shouldHideEditor) return;
- if (this.monacoInstance) {
- this.monacoInstance.setModel(null);
- }
+ editor.clearEditor();
this.getRawFileData(this.activeFile)
.then(() => {
- if (!this.monacoInstance) {
- this.monacoInstance = this.monaco.editor.create(this.$el, {
- model: null,
- readOnly: false,
- contextmenu: true,
- scrollBeyondLastLine: false,
- });
-
- this.languages = this.monaco.languages.getLanguages();
- }
-
- this.setupEditor();
+ editor.createInstance(this.$el);
})
+ .then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.'));
},
setupEditor() {
if (!this.activeFile) return;
- const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw;
-
- const foundLang = this.languages.find(lang =>
- lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
- );
- const newModel = this.monaco.editor.createModel(
- content, foundLang ? foundLang.id : 'plaintext',
- );
- const originalLines = this.monaco.editor.createModel(
- this.activeFile.raw, foundLang ? foundLang.id : 'plaintext',
- ).getLinesContent();
-
- this.monacoInstance.setModel(newModel);
- this.decorations = [];
-
- const modifiedType = (change) => {
- if (change.originalEndLineNumber === 0) {
- return 'added';
- } else if (change.modifiedEndLineNumber === 0) {
- return 'removed';
- }
-
- return 'modified';
- };
-
- this.monacoModelChangeContents = newModel.onDidChangeContent(() => {
- const diffComputer = new this.DiffComputer(
- originalLines,
- newModel.getLinesContent(),
- {
- shouldPostProcessCharChanges: true,
- shouldIgnoreTrimWhitespace: true,
- shouldMakePrettyDiff: true,
- },
- );
- this.decorations = this.monacoInstance.deltaDecorations(this.decorations,
- diffComputer.computeDiff().map(change => ({
- range: new monaco.Range(
- change.modifiedStartLineNumber,
- 1,
- !change.modifiedEndLineNumber ?
- change.modifiedStartLineNumber : change.modifiedEndLineNumber,
- 1,
- ),
- options: {
- isWholeLine: true,
- linesDecorationsClassName: `dirty-diff dirty-diff-${modifiedType(change)}`,
- },
- })),
- );
+ const model = editor.createModel(this.activeFile);
+ editor.attachModel(model);
+ model.onChange((m) => {
this.changeFileContent({
file: this.activeFile,
- content: this.monacoInstance.getValue(),
+ content: m.getValue(),
});
});
},
diff --git a/app/assets/javascripts/repo/lib/common/model.js b/app/assets/javascripts/repo/lib/common/model.js
new file mode 100644
index 00000000000..77d36b4f52c
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/model.js
@@ -0,0 +1,46 @@
+/* global monaco */
+
+export default class Model {
+ constructor(file) {
+ this.file = file;
+ this.content = file.content !== '' ? file.content : file.raw;
+ this.originalModel = monaco.editor.createModel(
+ this.content,
+ undefined,
+ new monaco.Uri(null, null, `original/${this.file.path}`),
+ );
+ this.model = monaco.editor.createModel(
+ this.content,
+ undefined,
+ new monaco.Uri(null, null, this.file.path),
+ );
+ this.disposers = new Map();
+ }
+
+ get url() {
+ return this.model.uri.toString();
+ }
+
+ getModel() {
+ return this.model;
+ }
+
+ getOriginalModel() {
+ return this.originalModel;
+ }
+
+ onChange(cb) {
+ this.disposers.set(
+ this.file.path,
+ this.model.onDidChangeContent(e => cb(this.model, e)),
+ );
+ }
+
+ dispose() {
+ this.model.dispose();
+ this.originalModel.dispose();
+
+ this.disposers.forEach(disposer => disposer.dispose());
+ this.disposers.clear();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/decorations/controller.js b/app/assets/javascripts/repo/lib/decorations/controller.js
new file mode 100644
index 00000000000..0ab74f1ebdf
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/decorations/controller.js
@@ -0,0 +1,46 @@
+import editor from '../editor';
+
+class DecorationsController {
+ constructor() {
+ this.decorations = new Map();
+ this.editorDecorations = new Map();
+ }
+
+ getAllDecorationsForModel(model) {
+ if (!this.decorations.has(model.url)) return [];
+
+ const modelDecorations = this.decorations.get(model.url);
+ const decorations = [];
+
+ modelDecorations.forEach(val => decorations.push(...val));
+
+ return decorations;
+ }
+
+ addDecorations(model, decorationsKey, decorations) {
+ const decorationMap = this.decorations.get(model.url) || new Map();
+
+ decorationMap.set(decorationsKey, decorations);
+
+ this.decorations.set(model.url, decorationMap);
+
+ this.decorate(model);
+ }
+
+ decorate(model) {
+ const decorations = this.getAllDecorationsForModel(model);
+ const oldDecorations = this.editorDecorations.get(model.url) || [];
+
+ this.editorDecorations.set(
+ model.url,
+ editor.instance.deltaDecorations(oldDecorations, decorations),
+ );
+ }
+
+ dispose() {
+ this.decorations.clear();
+ this.editorDecorations.clear();
+ }
+}
+
+export default new DecorationsController();
diff --git a/app/assets/javascripts/repo/lib/diff/controller.js b/app/assets/javascripts/repo/lib/diff/controller.js
new file mode 100644
index 00000000000..77d07b730cf
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/controller.js
@@ -0,0 +1,73 @@
+/* global monaco */
+import DirtyDiffWorker from './worker';
+import decorationsController from '../decorations/controller';
+
+export const getDiffChangeType = (change) => {
+ if (change.originalEndLineNumber === 0) {
+ return 'added';
+ } else if (change.modifiedEndLineNumber === 0) {
+ return 'removed';
+ }
+
+ return 'modified';
+};
+
+export const getDecorator = change => ({
+ range: new monaco.Range(
+ change.modifiedStartLineNumber,
+ 1,
+ !change.modifiedEndLineNumber ?
+ change.modifiedStartLineNumber : change.modifiedEndLineNumber,
+ 1,
+ ),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
+ },
+});
+
+export const decorate = (model, changes) => {
+ const decorations = changes.map(change => getDecorator(change));
+ decorationsController.addDecorations(model, 'dirtyDiff', decorations);
+};
+
+export default class DirtyDiffController {
+ constructor() {
+ this.editorSimpleWorker = null;
+ this.models = new Map();
+ this.worker = new DirtyDiffWorker();
+ }
+
+ attachModel(model) {
+ if (this.models.has(model.getModel().uri.toString())) return;
+
+ [model.getModel(), model.getOriginalModel()].forEach((iModel) => {
+ this.worker.attachModel({
+ url: iModel.uri.toString(),
+ versionId: iModel.getVersionId(),
+ lines: iModel.getLinesContent(),
+ EOL: '\n',
+ });
+ });
+
+ model.onChange((_, e) => this.computeDiff(model, e));
+
+ this.models.set(model.getModel().uri.toString(), model);
+ }
+
+ computeDiff(model, e) {
+ this.worker.modelChanged(model, e);
+ this.worker.compute(model, changes => decorate(model, changes));
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ reDecorate(model) {
+ decorationsController.decorate(model);
+ }
+
+ dispose() {
+ this.models.clear();
+ this.worker.dispose();
+ decorationsController.dispose();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/diff/worker.js b/app/assets/javascripts/repo/lib/diff/worker.js
new file mode 100644
index 00000000000..93d94f8d138
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/worker.js
@@ -0,0 +1,78 @@
+/* global monaco */
+export default class DirtyDiffWorker {
+ constructor() {
+ this.editorSimpleWorker = null;
+ this.models = new Map();
+ this.actions = new Set();
+
+ // eslint-disable-next-line promise/catch-or-return
+ monaco.editor.createWebWorker({
+ moduleId: 'vs/editor/common/services/editorSimpleWorker',
+ }).getProxy().then((editorSimpleWorker) => {
+ this.editorSimpleWorker = editorSimpleWorker;
+ this.ready();
+ });
+ }
+
+ // loop through all the previous cached actions
+ // this way we don't block the user from editing the file
+ ready() {
+ this.actions.forEach((action) => {
+ const methodName = Object.keys(action)[0];
+ this[methodName](...action[methodName]);
+ });
+
+ this.actions.clear();
+ }
+
+ attachModel(model) {
+ if (this.editorSimpleWorker && !this.models.has(model.url)) {
+ this.editorSimpleWorker.acceptNewModel(model);
+
+ this.models.set(model.url, model);
+ } else if (!this.editorSimpleWorker) {
+ this.actions.add({
+ attachModel: [model],
+ });
+ }
+ }
+
+ modelChanged(model, e) {
+ if (this.editorSimpleWorker) {
+ this.editorSimpleWorker.acceptModelChanged(
+ model.getModel().uri.toString(),
+ e,
+ );
+ } else {
+ this.actions.add({
+ modelChanged: [model, e],
+ });
+ }
+ }
+
+ compute(model, cb) {
+ if (this.editorSimpleWorker) {
+ // eslint-disable-next-line promise/catch-or-return
+ this.editorSimpleWorker.computeDiff(
+ model.getOriginalModel().uri.toString(),
+ model.getModel().uri.toString(),
+ ).then(cb);
+ } else {
+ this.actions.add({
+ compute: [model, cb],
+ });
+ }
+ }
+
+ dispose() {
+ this.models.forEach(model =>
+ this.editorSimpleWorker.acceptRemovedModel(model.url),
+ );
+ this.models.clear();
+
+ this.actions.clear();
+
+ this.editorSimpleWorker.dispose();
+ this.editorSimpleWorker = null;
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/editor.js b/app/assets/javascripts/repo/lib/editor.js
new file mode 100644
index 00000000000..de1c6d61b10
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/editor.js
@@ -0,0 +1,69 @@
+/* global monaco */
+import DirtyDiffController from './diff/controller';
+import Model from './common/model';
+
+class Editor {
+ constructor() {
+ this.models = new Map();
+ this.diffComputers = new Map();
+ this.currentModel = null;
+ this.instance = null;
+ this.dirtyDiffController = null;
+ }
+
+ createInstance(domElement) {
+ if (!this.instance) {
+ this.instance = monaco.editor.create(domElement, {
+ model: null,
+ readOnly: false,
+ contextmenu: true,
+ scrollBeyondLastLine: false,
+ });
+
+ this.dirtyDiffController = new DirtyDiffController();
+ }
+ }
+
+ createModel(file) {
+ if (this.models.has(file.path)) {
+ return this.models.get(file.path);
+ }
+
+ const model = new Model(file);
+ this.models.set(file.path, model);
+
+ return model;
+ }
+
+ attachModel(model) {
+ this.instance.setModel(model.getModel());
+ this.dirtyDiffController.attachModel(model);
+
+ this.currentModel = model;
+
+ this.dirtyDiffController.reDecorate(model);
+ }
+
+ clearEditor() {
+ if (this.instance) {
+ this.instance.setModel(null);
+ }
+ }
+
+ dispose() {
+ // dispose main monaco instance
+ if (this.instance) {
+ this.instance.dispose();
+ this.instance = null;
+ }
+
+ // dispose of all the models
+ this.models.forEach(model => model.dispose());
+ this.models.clear();
+
+ this.dirtyDiffController.dispose();
+ this.dirtyDiffController = null;
+ }
+}
+
+export default new Editor();
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js
index 2fb45dcb03c..994d325e991 100644
--- a/app/assets/javascripts/repo/services/index.js
+++ b/app/assets/javascripts/repo/services/index.js
@@ -16,6 +16,10 @@ export default {
return Promise.resolve(file.content);
}
+ if (file.raw) {
+ return Promise.resolve(file.raw);
+ }
+
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text());
},
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 35b5862374b..a2a2bc408db 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -70,6 +70,7 @@
.line-numbers {
cursor: pointer;
+ min-width: initial;
&:hover {
text-decoration: underline;
@@ -309,16 +310,29 @@
left: 0 !important;
&-modified {
- background-color: rgb(19, 117, 150);
+ background-color: $blue-500;
}
&-added {
- background-color: rgb(89, 119, 11);
+ background-color: $green-600;
}
&-removed {
- height: 4px!important;
+ height: 0!important;
+ width: 0!important;
bottom: -2px;
- background-color: red;
+ border-style: solid;
+ border-width: 5px;
+ border-color: transparent transparent transparent $red-500;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100px;
+ height: 1px;
+ background-color: rgba($red-500, .5);
+ }
}
}