diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/repo/components/repo_editor.vue | 83 | ||||
-rw-r--r-- | app/assets/javascripts/repo/lib/common/model.js | 46 | ||||
-rw-r--r-- | app/assets/javascripts/repo/lib/decorations/controller.js | 46 | ||||
-rw-r--r-- | app/assets/javascripts/repo/lib/diff/controller.js | 73 | ||||
-rw-r--r-- | app/assets/javascripts/repo/lib/diff/worker.js | 78 | ||||
-rw-r--r-- | app/assets/javascripts/repo/lib/editor.js | 69 | ||||
-rw-r--r-- | app/assets/javascripts/repo/services/index.js | 4 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/repo.scss | 22 |
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); + } } } |