summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue54
-rw-r--r--app/assets/javascripts/repo/lib/common/disposable.js14
-rw-r--r--app/assets/javascripts/repo/lib/common/model.js56
-rw-r--r--app/assets/javascripts/repo/lib/common/model_manager.js32
-rw-r--r--app/assets/javascripts/repo/lib/decorations/controller.js43
-rw-r--r--app/assets/javascripts/repo/lib/diff/controller.js71
-rw-r--r--app/assets/javascripts/repo/lib/diff/diff.js30
-rw-r--r--app/assets/javascripts/repo/lib/diff/diff_worker.js10
-rw-r--r--app/assets/javascripts/repo/lib/editor.js79
-rw-r--r--app/assets/javascripts/repo/lib/editor_options.js2
-rw-r--r--app/assets/javascripts/repo/services/index.js4
-rw-r--r--app/assets/stylesheets/pages/repo.scss33
12 files changed, 392 insertions, 36 deletions
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index 1c864b176b1..f37cbd1e961 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -3,19 +3,18 @@
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();
- }
+ beforeDestroy() {
+ this.editor.dispose();
},
mounted() {
- if (this.monaco) {
+ if (this.editor && monaco) {
this.initMonaco();
} else {
monacoLoader(['vs/editor/editor.main'], () => {
- this.monaco = monaco;
+ this.editor = Editor.create(monaco);
this.initMonaco();
});
@@ -29,47 +28,25 @@ export default {
initMonaco() {
if (this.shouldHideEditor) return;
- if (this.monacoInstance) {
- this.monacoInstance.setModel(null);
- }
+ this.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.addMonacoEvents();
- }
-
- this.setupEditor();
+ this.editor.createInstance(this.$refs.editor);
})
+ .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 model = this.editor.createModel(this.activeFile);
- this.monacoInstance.setModel(newModel);
- },
- addMonacoEvents() {
- this.monacoInstance.onKeyUp(() => {
+ this.editor.attachModel(model);
+ model.onChange((m) => {
this.changeFileContent({
file: this.activeFile,
- content: this.monacoInstance.getValue(),
+ content: m.getValue(),
});
});
},
@@ -99,9 +76,14 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
- v-if="shouldHideEditor"
+ v-show="shouldHideEditor"
v-html="activeFile.html"
>
</div>
+ <div
+ v-show="!shouldHideEditor"
+ ref="editor"
+ >
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/repo/lib/common/disposable.js b/app/assets/javascripts/repo/lib/common/disposable.js
new file mode 100644
index 00000000000..84b29bdb600
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/disposable.js
@@ -0,0 +1,14 @@
+export default class Disposable {
+ constructor() {
+ this.disposers = new Set();
+ }
+
+ add(...disposers) {
+ disposers.forEach(disposer => this.disposers.add(disposer));
+ }
+
+ dispose() {
+ this.disposers.forEach(disposer => disposer.dispose());
+ this.disposers.clear();
+ }
+}
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..23c4811e6c0
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/model.js
@@ -0,0 +1,56 @@
+/* global monaco */
+import Disposable from './disposable';
+
+export default class Model {
+ constructor(monaco, file) {
+ this.monaco = monaco;
+ this.disposable = new Disposable();
+ this.file = file;
+ this.content = file.content !== '' ? file.content : file.raw;
+
+ this.disposable.add(
+ this.originalModel = this.monaco.editor.createModel(
+ this.file.raw,
+ undefined,
+ new this.monaco.Uri(null, null, `original/${this.file.path}`),
+ ),
+ this.model = this.monaco.editor.createModel(
+ this.content,
+ undefined,
+ new this.monaco.Uri(null, null, this.file.path),
+ ),
+ );
+
+ this.events = new Map();
+ }
+
+ get url() {
+ return this.model.uri.toString();
+ }
+
+ get path() {
+ return this.file.path;
+ }
+
+ getModel() {
+ return this.model;
+ }
+
+ getOriginalModel() {
+ return this.originalModel;
+ }
+
+ onChange(cb) {
+ this.events.set(
+ this.path,
+ this.disposable.add(
+ this.model.onDidChangeContent(e => cb(this.model, e)),
+ ),
+ );
+ }
+
+ dispose() {
+ this.disposable.dispose();
+ this.events.clear();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/common/model_manager.js b/app/assets/javascripts/repo/lib/common/model_manager.js
new file mode 100644
index 00000000000..fd462252795
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/model_manager.js
@@ -0,0 +1,32 @@
+import Disposable from './disposable';
+import Model from './model';
+
+export default class ModelManager {
+ constructor(monaco) {
+ this.monaco = monaco;
+ this.disposable = new Disposable();
+ this.models = new Map();
+ }
+
+ hasCachedModel(path) {
+ return this.models.has(path);
+ }
+
+ addModel(file) {
+ if (this.hasCachedModel(file.path)) {
+ return this.models.get(file.path);
+ }
+
+ const model = new Model(this.monaco, file);
+ this.models.set(model.path, model);
+ this.disposable.add(model);
+
+ return model;
+ }
+
+ dispose() {
+ // dispose of all the models
+ this.disposable.dispose();
+ this.models.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..0954b7973c4
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/decorations/controller.js
@@ -0,0 +1,43 @@
+export default class DecorationsController {
+ constructor(editor) {
+ this.editor = editor;
+ 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,
+ this.editor.instance.deltaDecorations(oldDecorations, decorations),
+ );
+ }
+
+ dispose() {
+ this.decorations.clear();
+ this.editorDecorations.clear();
+ }
+}
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..dc0b1c95e59
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/controller.js
@@ -0,0 +1,71 @@
+/* global monaco */
+import { throttle } from 'underscore';
+import DirtyDiffWorker from './diff_worker';
+import Disposable from '../common/disposable';
+
+export const getDiffChangeType = (change) => {
+ if (change.modified) {
+ return 'modified';
+ } else if (change.added) {
+ return 'added';
+ } else if (change.removed) {
+ return 'removed';
+ }
+
+ return '';
+};
+
+export const getDecorator = change => ({
+ range: new monaco.Range(
+ change.lineNumber,
+ 1,
+ change.endLineNumber,
+ 1,
+ ),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
+ },
+});
+
+export default class DirtyDiffController {
+ constructor(modelManager, decorationsController) {
+ this.disposable = new Disposable();
+ this.editorSimpleWorker = null;
+ this.modelManager = modelManager;
+ this.decorationsController = decorationsController;
+ this.dirtyDiffWorker = new DirtyDiffWorker();
+ this.throttledComputeDiff = throttle(this.computeDiff, 250);
+ this.decorate = this.decorate.bind(this);
+
+ this.dirtyDiffWorker.addEventListener('message', this.decorate);
+ }
+
+ attachModel(model) {
+ model.onChange(() => this.throttledComputeDiff(model));
+ }
+
+ computeDiff(model) {
+ this.dirtyDiffWorker.postMessage({
+ path: model.path,
+ originalContent: model.getOriginalModel().getValue(),
+ newContent: model.getModel().getValue(),
+ });
+ }
+
+ reDecorate(model) {
+ this.decorationsController.decorate(model);
+ }
+
+ decorate({ data }) {
+ const decorations = data.changes.map(change => getDecorator(change));
+ this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations);
+ }
+
+ dispose() {
+ this.disposable.dispose();
+
+ this.dirtyDiffWorker.removeEventListener('message', this.decorate);
+ this.dirtyDiffWorker.terminate();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/diff/diff.js b/app/assets/javascripts/repo/lib/diff/diff.js
new file mode 100644
index 00000000000..0e37f5c4704
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/diff.js
@@ -0,0 +1,30 @@
+import { diffLines } from 'diff';
+
+// eslint-disable-next-line import/prefer-default-export
+export const computeDiff = (originalContent, newContent) => {
+ const changes = diffLines(originalContent, newContent);
+
+ let lineNumber = 1;
+ return changes.reduce((acc, change) => {
+ const findOnLine = acc.find(c => c.lineNumber === lineNumber);
+
+ if (findOnLine) {
+ Object.assign(findOnLine, change, {
+ modified: true,
+ endLineNumber: (lineNumber + change.count) - 1,
+ });
+ } else if ('added' in change || 'removed' in change) {
+ acc.push(Object.assign({}, change, {
+ lineNumber,
+ modified: undefined,
+ endLineNumber: (lineNumber + change.count) - 1,
+ }));
+ }
+
+ if (!change.removed) {
+ lineNumber += change.count;
+ }
+
+ return acc;
+ }, []);
+};
diff --git a/app/assets/javascripts/repo/lib/diff/diff_worker.js b/app/assets/javascripts/repo/lib/diff/diff_worker.js
new file mode 100644
index 00000000000..e74c4046330
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/diff_worker.js
@@ -0,0 +1,10 @@
+import { computeDiff } from './diff';
+
+self.addEventListener('message', (e) => {
+ const data = e.data;
+
+ self.postMessage({
+ path: data.path,
+ changes: computeDiff(data.originalContent, data.newContent),
+ });
+});
diff --git a/app/assets/javascripts/repo/lib/editor.js b/app/assets/javascripts/repo/lib/editor.js
new file mode 100644
index 00000000000..db499444402
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/editor.js
@@ -0,0 +1,79 @@
+import DecorationsController from './decorations/controller';
+import DirtyDiffController from './diff/controller';
+import Disposable from './common/disposable';
+import ModelManager from './common/model_manager';
+import editorOptions from './editor_options';
+
+export default class Editor {
+ static create(monaco) {
+ this.editorInstance = new Editor(monaco);
+
+ return this.editorInstance;
+ }
+
+ constructor(monaco) {
+ this.monaco = monaco;
+ this.currentModel = null;
+ this.instance = null;
+ this.dirtyDiffController = null;
+ this.disposable = new Disposable();
+
+ this.disposable.add(
+ this.modelManager = new ModelManager(this.monaco),
+ this.decorationsController = new DecorationsController(this),
+ );
+ }
+
+ createInstance(domElement) {
+ if (!this.instance) {
+ this.disposable.add(
+ this.instance = this.monaco.editor.create(domElement, {
+ model: null,
+ readOnly: false,
+ contextmenu: true,
+ scrollBeyondLastLine: false,
+ }),
+ this.dirtyDiffController = new DirtyDiffController(
+ this.modelManager, this.decorationsController,
+ ),
+ );
+ }
+ }
+
+ createModel(file) {
+ return this.modelManager.addModel(file);
+ }
+
+ attachModel(model) {
+ this.instance.setModel(model.getModel());
+ this.dirtyDiffController.attachModel(model);
+
+ this.currentModel = model;
+
+ this.instance.updateOptions(editorOptions.reduce((acc, obj) => {
+ Object.keys(obj).forEach((key) => {
+ Object.assign(acc, {
+ [key]: obj[key](model),
+ });
+ });
+ return acc;
+ }, {}));
+
+ this.dirtyDiffController.reDecorate(model);
+ }
+
+ clearEditor() {
+ if (this.instance) {
+ this.instance.setModel(null);
+ }
+ }
+
+ dispose() {
+ this.disposable.dispose();
+
+ // dispose main monaco instance
+ if (this.instance) {
+ this.instance = null;
+ }
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/editor_options.js b/app/assets/javascripts/repo/lib/editor_options.js
new file mode 100644
index 00000000000..701affc466e
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/editor_options.js
@@ -0,0 +1,2 @@
+export default [{
+}];
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 6d274cb4ae0..402412eae71 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -275,3 +275,36 @@
height: 80px;
resize: none;
}
+
+.dirty-diff {
+ // !important need to override monaco inline style
+ width: 4px !important;
+ left: 0 !important;
+
+ &-modified {
+ background-color: $blue-500;
+ }
+
+ &-added {
+ background-color: $green-600;
+ }
+
+ &-removed {
+ height: 0 !important;
+ width: 0 !important;
+ bottom: -2px;
+ 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);
+ }
+ }
+}