From 37864fb0da4dde677097193ba9ace8931b21db37 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 16 Nov 2017 12:55:01 +0000 Subject: Multi-file editor dirty diff indicator [ci skip] --- .../javascripts/repo/components/repo_editor.vue | 50 +++++++++++++++++++--- app/assets/stylesheets/pages/repo.scss | 20 +++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 1c864b176b1..3c06e75a472 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -14,8 +14,9 @@ export default { if (this.monaco) { this.initMonaco(); } else { - monacoLoader(['vs/editor/editor.main'], () => { + monacoLoader(['vs/editor/editor.main', 'vs/editor/common/diff/diffComputer'], (_, { DiffComputer }) => { this.monaco = monaco; + this.DiffComputer = DiffComputer; this.initMonaco(); }); @@ -44,8 +45,6 @@ export default { }); this.languages = this.monaco.languages.getLanguages(); - - this.addMonacoEvents(); } this.setupEditor(); @@ -62,11 +61,50 @@ export default { 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); - }, - addMonacoEvents() { - this.monacoInstance.onKeyUp(() => { + 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)}`, + }, + })), + ); + this.changeFileContent({ file: this.activeFile, content: this.monacoInstance.getValue(), diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index d93c51d5448..35b5862374b 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -302,3 +302,23 @@ .multi-file-table-col-name { width: 350px; } + +.dirty-diff { + // !important need to override monaco inline style + width: 4px !important; + left: 0 !important; + + &-modified { + background-color: rgb(19, 117, 150); + } + + &-added { + background-color: rgb(89, 119, 11); + } + + &-removed { + height: 4px!important; + bottom: -2px; + background-color: red; + } +} -- cgit v1.2.1 From c90b520d793be1a1f69811aafdeff00e4f463346 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 17 Nov 2017 10:46:26 +0000 Subject: created editor library to manage all things editor [ci skip] --- .../javascripts/repo/components/repo_editor.vue | 83 +++------------------- app/assets/javascripts/repo/lib/common/model.js | 46 ++++++++++++ .../javascripts/repo/lib/decorations/controller.js | 46 ++++++++++++ app/assets/javascripts/repo/lib/diff/controller.js | 73 +++++++++++++++++++ app/assets/javascripts/repo/lib/diff/worker.js | 78 ++++++++++++++++++++ app/assets/javascripts/repo/lib/editor.js | 69 ++++++++++++++++++ app/assets/javascripts/repo/services/index.js | 4 ++ app/assets/stylesheets/pages/repo.scss | 22 ++++-- 8 files changed, 344 insertions(+), 77 deletions(-) create mode 100644 app/assets/javascripts/repo/lib/common/model.js create mode 100644 app/assets/javascripts/repo/lib/decorations/controller.js create mode 100644 app/assets/javascripts/repo/lib/diff/controller.js create mode 100644 app/assets/javascripts/repo/lib/diff/worker.js create mode 100644 app/assets/javascripts/repo/lib/editor.js 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); + } } } -- cgit v1.2.1 From eb8a609519fe45b13aa945e36637381741313aed Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 21 Nov 2017 14:22:33 +0000 Subject: added disposable manager added model manager [ci skip] --- .../javascripts/repo/components/repo_editor.vue | 14 +++-- .../javascripts/repo/lib/common/disposable.js | 14 +++++ app/assets/javascripts/repo/lib/common/model.js | 66 +++++++++++++++++----- .../javascripts/repo/lib/common/model_manager.js | 31 ++++++++++ .../javascripts/repo/lib/decorations/controller.js | 2 +- app/assets/javascripts/repo/lib/diff/controller.js | 25 +++----- app/assets/javascripts/repo/lib/diff/worker.js | 39 +++++++------ app/assets/javascripts/repo/lib/editor.js | 41 +++++++------- 8 files changed, 152 insertions(+), 80 deletions(-) create mode 100644 app/assets/javascripts/repo/lib/common/disposable.js create mode 100644 app/assets/javascripts/repo/lib/common/model_manager.js diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index e0ce8a97665..8772b45669f 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -3,13 +3,15 @@ import { mapGetters, mapActions } from 'vuex'; import flash from '../../flash'; import monacoLoader from '../monaco_loader'; -import editor from '../lib/editor'; +import Editor from '../lib/editor'; export default { destroyed() { - editor.dispose(); + this.editor.dispose(); }, mounted() { + this.editor = Editor.create(); + if (this.monaco) { this.initMonaco(); } else { @@ -26,11 +28,11 @@ export default { initMonaco() { if (this.shouldHideEditor) return; - editor.clearEditor(); + this.editor.clearEditor(); this.getRawFileData(this.activeFile) .then(() => { - editor.createInstance(this.$el); + this.editor.createInstance(this.$el); }) .then(() => this.setupEditor()) .catch(() => flash('Error setting up monaco. Please try again.')); @@ -38,9 +40,9 @@ export default { setupEditor() { if (!this.activeFile) return; - const model = editor.createModel(this.activeFile); + const model = this.editor.createModel(this.activeFile); - editor.attachModel(model); + this.editor.attachModel(model); model.onChange((m) => { this.changeFileContent({ file: this.activeFile, 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 index 77d36b4f52c..75828bfb09d 100644 --- a/app/assets/javascripts/repo/lib/common/model.js +++ b/app/assets/javascripts/repo/lib/common/model.js @@ -1,26 +1,59 @@ /* global monaco */ +import Disposable from './disposable'; export default class Model { constructor(file) { + this.disposable = new Disposable(); 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.disposable.add( + 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(); + + this.attachedToWorker = false; + this.events = new Map(); } get url() { return this.model.uri.toString(); } + get originalUrl() { + return this.originalModel.uri.toString(); + } + + get path() { + return this.file.path; + } + + get diffModel() { + return { + url: this.model.uri.toString(), + versionId: this.model.getVersionId(), + lines: this.model.getLinesContent(), + EOL: '\n', + }; + } + + get originalDiffModel() { + return { + url: this.originalModel.uri.toString(), + versionId: this.originalModel.getVersionId(), + lines: this.originalModel.getLinesContent(), + EOL: '\n', + }; + } + getModel() { return this.model; } @@ -29,18 +62,21 @@ export default class Model { return this.originalModel; } + setAttachedToWorker(val) { + this.attachedToWorker = val; + } + onChange(cb) { - this.disposers.set( + this.events.set( this.file.path, this.model.onDidChangeContent(e => cb(this.model, e)), ); } dispose() { - this.model.dispose(); - this.originalModel.dispose(); + this.disposable.dispose(); - this.disposers.forEach(disposer => disposer.dispose()); - this.disposers.clear(); + this.events.forEach(disposer => disposer.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..fdb1e148681 --- /dev/null +++ b/app/assets/javascripts/repo/lib/common/model_manager.js @@ -0,0 +1,31 @@ +import Disposable from './disposable'; +import Model from './model'; + +export default class ModelManager { + constructor() { + 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(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 index 0ab74f1ebdf..eb369dbbfe1 100644 --- a/app/assets/javascripts/repo/lib/decorations/controller.js +++ b/app/assets/javascripts/repo/lib/decorations/controller.js @@ -33,7 +33,7 @@ class DecorationsController { this.editorDecorations.set( model.url, - editor.instance.deltaDecorations(oldDecorations, decorations), + editor.editorInstance.instance.deltaDecorations(oldDecorations, decorations), ); } diff --git a/app/assets/javascripts/repo/lib/diff/controller.js b/app/assets/javascripts/repo/lib/diff/controller.js index 77d07b730cf..39c6cb92e0f 100644 --- a/app/assets/javascripts/repo/lib/diff/controller.js +++ b/app/assets/javascripts/repo/lib/diff/controller.js @@ -1,4 +1,5 @@ /* global monaco */ +import Disposable from '../common/disposable'; import DirtyDiffWorker from './worker'; import decorationsController from '../decorations/controller'; @@ -32,27 +33,21 @@ export const decorate = (model, changes) => { }; export default class DirtyDiffController { - constructor() { + constructor(modelManager) { + this.disposable = new Disposable(); this.editorSimpleWorker = null; - this.models = new Map(); - this.worker = new DirtyDiffWorker(); + this.modelManager = modelManager; + this.disposable.add(this.worker = new DirtyDiffWorker()); } attachModel(model) { - if (this.models.has(model.getModel().uri.toString())) return; + if (model.attachedToWorker) return; - [model.getModel(), model.getOriginalModel()].forEach((iModel) => { - this.worker.attachModel({ - url: iModel.uri.toString(), - versionId: iModel.getVersionId(), - lines: iModel.getLinesContent(), - EOL: '\n', - }); + [model.getModel(), model.getOriginalModel()].forEach(() => { + this.worker.attachModel(model); }); model.onChange((_, e) => this.computeDiff(model, e)); - - this.models.set(model.getModel().uri.toString(), model); } computeDiff(model, e) { @@ -66,8 +61,6 @@ export default class DirtyDiffController { } dispose() { - this.models.clear(); - this.worker.dispose(); - decorationsController.dispose(); + this.disposable.dispose(); } } diff --git a/app/assets/javascripts/repo/lib/diff/worker.js b/app/assets/javascripts/repo/lib/diff/worker.js index 93d94f8d138..39047d85507 100644 --- a/app/assets/javascripts/repo/lib/diff/worker.js +++ b/app/assets/javascripts/repo/lib/diff/worker.js @@ -1,15 +1,17 @@ /* global monaco */ +import Disposable from '../common/disposable'; + export default class DirtyDiffWorker { constructor() { this.editorSimpleWorker = null; - this.models = new Map(); + this.disposable = new Disposable(); 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.disposable.add(this.editorSimpleWorker = editorSimpleWorker); this.ready(); }); } @@ -26,10 +28,11 @@ export default class DirtyDiffWorker { } attachModel(model) { - if (this.editorSimpleWorker && !this.models.has(model.url)) { - this.editorSimpleWorker.acceptNewModel(model); + if (this.editorSimpleWorker && !model.attachedToWorker) { + this.editorSimpleWorker.acceptNewModel(model.diffModel); + this.editorSimpleWorker.acceptNewModel(model.originalDiffModel); - this.models.set(model.url, model); + model.setAttachedToWorker(true); } else if (!this.editorSimpleWorker) { this.actions.add({ attachModel: [model], @@ -40,7 +43,7 @@ export default class DirtyDiffWorker { modelChanged(model, e) { if (this.editorSimpleWorker) { this.editorSimpleWorker.acceptModelChanged( - model.getModel().uri.toString(), + model.url, e, ); } else { @@ -52,27 +55,23 @@ export default class DirtyDiffWorker { 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(), + return this.editorSimpleWorker.computeDiff( + model.originalUrl, + model.url, ).then(cb); - } else { - this.actions.add({ - compute: [model, cb], - }); } + + this.actions.add({ + compute: [model, cb], + }); + + return null; } dispose() { - this.models.forEach(model => - this.editorSimpleWorker.acceptRemovedModel(model.url), - ); - this.models.clear(); - this.actions.clear(); - this.editorSimpleWorker.dispose(); + this.disposable.dispose(); this.editorSimpleWorker = null; } } diff --git a/app/assets/javascripts/repo/lib/editor.js b/app/assets/javascripts/repo/lib/editor.js index de1c6d61b10..173eacbd65c 100644 --- a/app/assets/javascripts/repo/lib/editor.js +++ b/app/assets/javascripts/repo/lib/editor.js @@ -1,14 +1,24 @@ /* global monaco */ import DirtyDiffController from './diff/controller'; -import Model from './common/model'; +import Disposable from './common/disposable'; +import ModelManager from './common/model_manager'; + +export default class Editor { + static create() { + this.editorInstance = new Editor(); + + return this.editorInstance; + } -class Editor { constructor() { - this.models = new Map(); this.diffComputers = new Map(); this.currentModel = null; this.instance = null; this.dirtyDiffController = null; + this.modelManager = new ModelManager(); + this.disposable = new Disposable(); + + this.disposable.add(this.modelManager); } createInstance(domElement) { @@ -20,19 +30,14 @@ class Editor { scrollBeyondLastLine: false, }); - this.dirtyDiffController = new DirtyDiffController(); + this.dirtyDiffController = new DirtyDiffController(this.modelManager); + + this.disposable.add(this.dirtyDiffController, this.instance); } } 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; + return this.modelManager.addModel(file); } attachModel(model) { @@ -51,19 +56,11 @@ class Editor { } dispose() { + this.disposable.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(); -- cgit v1.2.1 From caa2f10e719e700ae7d51ddb0180e9505fb1ffa6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 22 Nov 2017 09:08:30 +0000 Subject: change how diffing works [ci skip] --- app/assets/javascripts/repo/lib/diff/controller.js | 30 +++--- app/assets/javascripts/repo/lib/diff/worker.js | 103 ++++++--------------- app/assets/javascripts/repo/lib/editor.js | 25 +++-- package.json | 1 + yarn.lock | 4 + 5 files changed, 59 insertions(+), 104 deletions(-) diff --git a/app/assets/javascripts/repo/lib/diff/controller.js b/app/assets/javascripts/repo/lib/diff/controller.js index 39c6cb92e0f..6c97f1cc24d 100644 --- a/app/assets/javascripts/repo/lib/diff/controller.js +++ b/app/assets/javascripts/repo/lib/diff/controller.js @@ -1,24 +1,25 @@ /* global monaco */ -import Disposable from '../common/disposable'; import DirtyDiffWorker from './worker'; +import Disposable from '../common/disposable'; import decorationsController from '../decorations/controller'; export const getDiffChangeType = (change) => { - if (change.originalEndLineNumber === 0) { + if (change.modified) { + return 'modified'; + } else if (change.added) { return 'added'; - } else if (change.modifiedEndLineNumber === 0) { + } else if (change.removed) { return 'removed'; } - return 'modified'; + return ''; }; export const getDecorator = change => ({ range: new monaco.Range( - change.modifiedStartLineNumber, + change.lineNumber, 1, - !change.modifiedEndLineNumber ? - change.modifiedStartLineNumber : change.modifiedEndLineNumber, + change.endLineNumber, 1, ), options: { @@ -37,22 +38,15 @@ export default class DirtyDiffController { this.disposable = new Disposable(); this.editorSimpleWorker = null; this.modelManager = modelManager; - this.disposable.add(this.worker = new DirtyDiffWorker()); + this.dirtyDiffWorker = new DirtyDiffWorker(); } attachModel(model) { - if (model.attachedToWorker) return; - - [model.getModel(), model.getOriginalModel()].forEach(() => { - this.worker.attachModel(model); - }); - - model.onChange((_, e) => this.computeDiff(model, e)); + model.onChange(() => this.computeDiff(model)); } - computeDiff(model, e) { - this.worker.modelChanged(model, e); - this.worker.compute(model, changes => decorate(model, changes)); + computeDiff(model) { + decorate(model, this.dirtyDiffWorker.compute(model)); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/repo/lib/diff/worker.js b/app/assets/javascripts/repo/lib/diff/worker.js index 39047d85507..0007c7060dc 100644 --- a/app/assets/javascripts/repo/lib/diff/worker.js +++ b/app/assets/javascripts/repo/lib/diff/worker.js @@ -1,77 +1,34 @@ -/* global monaco */ -import Disposable from '../common/disposable'; +import { diffLines } from 'diff'; export default class DirtyDiffWorker { - constructor() { - this.editorSimpleWorker = null; - this.disposable = new Disposable(); - 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.disposable.add(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 && !model.attachedToWorker) { - this.editorSimpleWorker.acceptNewModel(model.diffModel); - this.editorSimpleWorker.acceptNewModel(model.originalDiffModel); - - model.setAttachedToWorker(true); - } else if (!this.editorSimpleWorker) { - this.actions.add({ - attachModel: [model], - }); - } - } - - modelChanged(model, e) { - if (this.editorSimpleWorker) { - this.editorSimpleWorker.acceptModelChanged( - model.url, - e, - ); - } else { - this.actions.add({ - modelChanged: [model, e], - }); - } - } - - compute(model, cb) { - if (this.editorSimpleWorker) { - return this.editorSimpleWorker.computeDiff( - model.originalUrl, - model.url, - ).then(cb); - } - - this.actions.add({ - compute: [model, cb], - }); - - return null; - } - - dispose() { - this.actions.clear(); - - this.disposable.dispose(); - this.editorSimpleWorker = null; + // eslint-disable-next-line class-methods-use-this + compute(model) { + const originalContent = model.getOriginalModel().getValue(); + const newContent = model.getModel().getValue(); + 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: change.count > 1 ? lineNumber + change.count : lineNumber, + }); + } else if ('added' in change || 'removed' in change) { + acc.push(Object.assign({}, change, { + lineNumber, + modified: undefined, + endLineNumber: change.count > 1 ? lineNumber + change.count : lineNumber, + })); + } + + if (!change.removed) { + lineNumber += change.count; + } + + return acc; + }, []); } } diff --git a/app/assets/javascripts/repo/lib/editor.js b/app/assets/javascripts/repo/lib/editor.js index 173eacbd65c..ea3bfe16462 100644 --- a/app/assets/javascripts/repo/lib/editor.js +++ b/app/assets/javascripts/repo/lib/editor.js @@ -11,28 +11,27 @@ export default class Editor { } constructor() { - this.diffComputers = new Map(); this.currentModel = null; this.instance = null; this.dirtyDiffController = null; - this.modelManager = new ModelManager(); this.disposable = new Disposable(); - this.disposable.add(this.modelManager); + this.disposable.add( + this.modelManager = new ModelManager(), + ); } createInstance(domElement) { if (!this.instance) { - this.instance = monaco.editor.create(domElement, { - model: null, - readOnly: false, - contextmenu: true, - scrollBeyondLastLine: false, - }); - - this.dirtyDiffController = new DirtyDiffController(this.modelManager); - - this.disposable.add(this.dirtyDiffController, this.instance); + this.disposable.add( + this.instance = monaco.editor.create(domElement, { + model: null, + readOnly: false, + contextmenu: true, + scrollBeyondLastLine: false, + }), + this.dirtyDiffController = new DirtyDiffController(this.modelManager), + ); } } diff --git a/package.json b/package.json index 8c1b2c401ed..5b4676bf4ae 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "css-loader": "^0.28.0", "d3": "^3.5.11", "deckar01-task_list": "^2.0.0", + "diff": "^3.4.0", "document-register-element": "1.3.0", "dropzone": "^4.2.0", "emoji-unicode-version": "^0.2.1", diff --git a/yarn.lock b/yarn.lock index 73cc4f11500..2050f9eeed7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1895,6 +1895,10 @@ di@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" +diff@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" + diffie-hellman@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" -- cgit v1.2.1 From 7c04e4086c0554ca93ce229fc4bee8dd82941e80 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 22 Nov 2017 10:18:33 +0000 Subject: changed endLineNumber calculation [ci skip] --- app/assets/javascripts/repo/lib/diff/worker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/repo/lib/diff/worker.js b/app/assets/javascripts/repo/lib/diff/worker.js index 0007c7060dc..aa684e6e2a5 100644 --- a/app/assets/javascripts/repo/lib/diff/worker.js +++ b/app/assets/javascripts/repo/lib/diff/worker.js @@ -14,13 +14,13 @@ export default class DirtyDiffWorker { if (findOnLine) { Object.assign(findOnLine, change, { modified: true, - endLineNumber: change.count > 1 ? lineNumber + change.count : lineNumber, + endLineNumber: (lineNumber + change.count) - 1, }); } else if ('added' in change || 'removed' in change) { acc.push(Object.assign({}, change, { lineNumber, modified: undefined, - endLineNumber: change.count > 1 ? lineNumber + change.count : lineNumber, + endLineNumber: (lineNumber + change.count) - 1, })); } -- cgit v1.2.1 From 809a27e61e070a125e2fe5183574f1bf35668a52 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 22 Nov 2017 13:39:16 +0000 Subject: started move to web worker for diff calculation --- .../javascripts/repo/components/repo_editor.vue | 6 ++-- app/assets/javascripts/repo/lib/common/model.js | 41 ++++++++++------------ .../javascripts/repo/lib/common/model_manager.js | 5 +-- .../javascripts/repo/lib/decorations/controller.js | 11 +++--- app/assets/javascripts/repo/lib/diff/controller.js | 24 +++++++------ app/assets/javascripts/repo/lib/diff/diff.js | 38 ++++++++++++++++++++ app/assets/javascripts/repo/lib/diff/worker.js | 34 ------------------ app/assets/javascripts/repo/lib/editor.js | 18 ++++++---- 8 files changed, 90 insertions(+), 87 deletions(-) create mode 100644 app/assets/javascripts/repo/lib/diff/diff.js delete mode 100644 app/assets/javascripts/repo/lib/diff/worker.js diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 8772b45669f..fa6070dbf92 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -10,12 +10,12 @@ export default { this.editor.dispose(); }, mounted() { - this.editor = Editor.create(); - if (this.monaco) { this.initMonaco(); } else { monacoLoader(['vs/editor/editor.main'], () => { + this.editor = Editor.create(monaco); + this.initMonaco(); }); } @@ -35,7 +35,7 @@ export default { this.editor.createInstance(this.$el); }) .then(() => this.setupEditor()) - .catch(() => flash('Error setting up monaco. Please try again.')); + .catch((e) => { throw e;flash('Error setting up monaco. Please try again.'); }); }, setupEditor() { if (!this.activeFile) return; diff --git a/app/assets/javascripts/repo/lib/common/model.js b/app/assets/javascripts/repo/lib/common/model.js index 75828bfb09d..fd6f41f87b1 100644 --- a/app/assets/javascripts/repo/lib/common/model.js +++ b/app/assets/javascripts/repo/lib/common/model.js @@ -2,25 +2,25 @@ import Disposable from './disposable'; export default class Model { - constructor(file) { + 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 = monaco.editor.createModel( - this.content, + this.originalModel = this.monaco.editor.createModel( + this.file.raw, undefined, - new monaco.Uri(null, null, `original/${this.file.path}`), + new this.monaco.Uri(null, null, `original/${this.file.path}`), ), - this.model = monaco.editor.createModel( + this.model = this.monaco.editor.createModel( this.content, undefined, - new monaco.Uri(null, null, this.file.path), + new this.monaco.Uri(null, null, this.file.path), ), ); - this.attachedToWorker = false; this.events = new Map(); } @@ -37,19 +37,18 @@ export default class Model { } get diffModel() { - return { - url: this.model.uri.toString(), - versionId: this.model.getVersionId(), - lines: this.model.getLinesContent(), - EOL: '\n', - }; + return Model.getDiffModel(this.model); } get originalDiffModel() { + return Model.getDiffModel(this.originalModel); + } + + static getDiffModel(model) { return { - url: this.originalModel.uri.toString(), - versionId: this.originalModel.getVersionId(), - lines: this.originalModel.getLinesContent(), + url: model.uri.toString(), + versionId: model.getVersionId(), + lines: model.getLinesContent(), EOL: '\n', }; } @@ -62,21 +61,17 @@ export default class Model { return this.originalModel; } - setAttachedToWorker(val) { - this.attachedToWorker = val; - } - onChange(cb) { this.events.set( this.file.path, - this.model.onDidChangeContent(e => cb(this.model, e)), + this.disposable.add( + this.model.onDidChangeContent(e => cb(this.model, e)), + ), ); } dispose() { this.disposable.dispose(); - - this.events.forEach(disposer => disposer.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 index fdb1e148681..fd462252795 100644 --- a/app/assets/javascripts/repo/lib/common/model_manager.js +++ b/app/assets/javascripts/repo/lib/common/model_manager.js @@ -2,7 +2,8 @@ import Disposable from './disposable'; import Model from './model'; export default class ModelManager { - constructor() { + constructor(monaco) { + this.monaco = monaco; this.disposable = new Disposable(); this.models = new Map(); } @@ -16,7 +17,7 @@ export default class ModelManager { return this.models.get(file.path); } - const model = new Model(file); + const model = new Model(this.monaco, file); this.models.set(model.path, model); this.disposable.add(model); diff --git a/app/assets/javascripts/repo/lib/decorations/controller.js b/app/assets/javascripts/repo/lib/decorations/controller.js index eb369dbbfe1..0954b7973c4 100644 --- a/app/assets/javascripts/repo/lib/decorations/controller.js +++ b/app/assets/javascripts/repo/lib/decorations/controller.js @@ -1,7 +1,6 @@ -import editor from '../editor'; - -class DecorationsController { - constructor() { +export default class DecorationsController { + constructor(editor) { + this.editor = editor; this.decorations = new Map(); this.editorDecorations = new Map(); } @@ -33,7 +32,7 @@ class DecorationsController { this.editorDecorations.set( model.url, - editor.editorInstance.instance.deltaDecorations(oldDecorations, decorations), + this.editor.instance.deltaDecorations(oldDecorations, decorations), ); } @@ -42,5 +41,3 @@ class DecorationsController { 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 index 6c97f1cc24d..9b6c164a81c 100644 --- a/app/assets/javascripts/repo/lib/diff/controller.js +++ b/app/assets/javascripts/repo/lib/diff/controller.js @@ -1,7 +1,7 @@ /* global monaco */ -import DirtyDiffWorker from './worker'; +import DirtyDiffWorker from './diff'; +console.log(DirtyDiffWorker); import Disposable from '../common/disposable'; -import decorationsController from '../decorations/controller'; export const getDiffChangeType = (change) => { if (change.modified) { @@ -28,17 +28,14 @@ export const getDecorator = change => ({ }, }); -export const decorate = (model, changes) => { - const decorations = changes.map(change => getDecorator(change)); - decorationsController.addDecorations(model, 'dirtyDiff', decorations); -}; - export default class DirtyDiffController { - constructor(modelManager) { + constructor(modelManager, decorationsController) { this.disposable = new Disposable(); this.editorSimpleWorker = null; this.modelManager = modelManager; - this.dirtyDiffWorker = new DirtyDiffWorker(); + this.decorationsController = decorationsController; + console.log(DirtyDiffWorker); + // this.dirtyDiffWorker = new DirtyDiffWorker(); } attachModel(model) { @@ -46,12 +43,17 @@ export default class DirtyDiffController { } computeDiff(model) { - decorate(model, this.dirtyDiffWorker.compute(model)); + this.decorate(model, this.dirtyDiffWorker.compute(model)); } // eslint-disable-next-line class-methods-use-this reDecorate(model) { - decorationsController.decorate(model); + this.decorationsController.decorate(model); + } + + decorate(model, changes) { + const decorations = changes.map(change => getDecorator(change)); + this.decorationsController.addDecorations(model, 'dirtyDiff', decorations); } dispose() { 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..bc183fec70a --- /dev/null +++ b/app/assets/javascripts/repo/lib/diff/diff.js @@ -0,0 +1,38 @@ +import { diffLines } from 'diff'; + +// export default class DirtyDiffWorker { +// // eslint-disable-next-line class-methods-use-this +// compute(model) { +// console.time('a'); +// const originalContent = model.getOriginalModel().getValue(); +// const newContent = model.getModel().getValue(); +// const changes = diffLines(originalContent, newContent); +// +// let lineNumber = 1; +// const a = 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; +// }, []); +// console.timeEnd('a'); +// +// return a; +// } +// } diff --git a/app/assets/javascripts/repo/lib/diff/worker.js b/app/assets/javascripts/repo/lib/diff/worker.js deleted file mode 100644 index aa684e6e2a5..00000000000 --- a/app/assets/javascripts/repo/lib/diff/worker.js +++ /dev/null @@ -1,34 +0,0 @@ -import { diffLines } from 'diff'; - -export default class DirtyDiffWorker { - // eslint-disable-next-line class-methods-use-this - compute(model) { - const originalContent = model.getOriginalModel().getValue(); - const newContent = model.getModel().getValue(); - 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/editor.js b/app/assets/javascripts/repo/lib/editor.js index ea3bfe16462..e82fbfc0e9c 100644 --- a/app/assets/javascripts/repo/lib/editor.js +++ b/app/assets/javascripts/repo/lib/editor.js @@ -1,36 +1,40 @@ -/* global monaco */ +import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; import ModelManager from './common/model_manager'; export default class Editor { - static create() { - this.editorInstance = new Editor(); + static create(monaco) { + this.editorInstance = new Editor(monaco); return this.editorInstance; } - constructor() { + 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.modelManager = new ModelManager(this.monaco), + this.decorationsController = new DecorationsController(this), ); } createInstance(domElement) { if (!this.instance) { this.disposable.add( - this.instance = monaco.editor.create(domElement, { + this.instance = this.monaco.editor.create(domElement, { model: null, readOnly: false, contextmenu: true, scrollBeyondLastLine: false, }), - this.dirtyDiffController = new DirtyDiffController(this.modelManager), + this.dirtyDiffController = new DirtyDiffController( + this.modelManager, this.decorationsController, + ), ); } } -- cgit v1.2.1 From 3a1c3c5df5ed6868f62c6c4d4d727ff49f188af3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 22 Nov 2017 14:08:28 +0000 Subject: added web worker to calculate diffs [ci skip] --- app/assets/javascripts/repo/lib/diff/controller.js | 22 ++++---- app/assets/javascripts/repo/lib/diff/diff.js | 63 ++++++++++------------ .../javascripts/repo/lib/diff/worker.diff.js | 10 ++++ package.json | 3 +- yarn.lock | 28 +++++++++- 5 files changed, 79 insertions(+), 47 deletions(-) create mode 100644 app/assets/javascripts/repo/lib/diff/worker.diff.js diff --git a/app/assets/javascripts/repo/lib/diff/controller.js b/app/assets/javascripts/repo/lib/diff/controller.js index 9b6c164a81c..cd1a2025f17 100644 --- a/app/assets/javascripts/repo/lib/diff/controller.js +++ b/app/assets/javascripts/repo/lib/diff/controller.js @@ -1,6 +1,5 @@ /* global monaco */ -import DirtyDiffWorker from './diff'; -console.log(DirtyDiffWorker); +import DirtyDiffWorker from 'worker-loader!./worker.diff'; import Disposable from '../common/disposable'; export const getDiffChangeType = (change) => { @@ -34,8 +33,9 @@ export default class DirtyDiffController { this.editorSimpleWorker = null; this.modelManager = modelManager; this.decorationsController = decorationsController; - console.log(DirtyDiffWorker); - // this.dirtyDiffWorker = new DirtyDiffWorker(); + this.dirtyDiffWorker = new DirtyDiffWorker(); + + this.dirtyDiffWorker.addEventListener('message', e => this.decorate(e)); } attachModel(model) { @@ -43,20 +43,24 @@ export default class DirtyDiffController { } computeDiff(model) { - this.decorate(model, this.dirtyDiffWorker.compute(model)); + this.dirtyDiffWorker.postMessage({ + path: model.path, + originalContent: model.getOriginalModel().getValue(), + newContent: model.getModel().getValue(), + }); } - // eslint-disable-next-line class-methods-use-this reDecorate(model) { this.decorationsController.decorate(model); } - decorate(model, changes) { - const decorations = changes.map(change => getDecorator(change)); - this.decorationsController.addDecorations(model, 'dirtyDiff', decorations); + decorate({ data }) { + const decorations = data.changes.map(change => getDecorator(change)); + this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations); } dispose() { this.disposable.dispose(); + this.dirtyDiffWorker.terminate(); } } diff --git a/app/assets/javascripts/repo/lib/diff/diff.js b/app/assets/javascripts/repo/lib/diff/diff.js index bc183fec70a..ada24740688 100644 --- a/app/assets/javascripts/repo/lib/diff/diff.js +++ b/app/assets/javascripts/repo/lib/diff/diff.js @@ -1,38 +1,29 @@ import { diffLines } from 'diff'; -// export default class DirtyDiffWorker { -// // eslint-disable-next-line class-methods-use-this -// compute(model) { -// console.time('a'); -// const originalContent = model.getOriginalModel().getValue(); -// const newContent = model.getModel().getValue(); -// const changes = diffLines(originalContent, newContent); -// -// let lineNumber = 1; -// const a = 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; -// }, []); -// console.timeEnd('a'); -// -// return a; -// } -// } +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/worker.diff.js b/app/assets/javascripts/repo/lib/diff/worker.diff.js new file mode 100644 index 00000000000..e74c4046330 --- /dev/null +++ b/app/assets/javascripts/repo/lib/diff/worker.diff.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/package.json b/package.json index 5b4676bf4ae..8f06ffc1b6d 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^2.0.4", "nodemon": "^1.11.0", - "webpack-dev-server": "^2.6.1" + "webpack-dev-server": "^2.6.1", + "worker-loader": "^1.1.0" } } diff --git a/yarn.lock b/yarn.lock index 2050f9eeed7..f631c0167e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -116,6 +116,15 @@ ajv@^4.7.0, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" +ajv@^5.0.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.4.0.tgz#32d1cf08dbc80c432f426f12e10b2511f6b46474" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + ajv@^5.1.5: version "5.2.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" @@ -2531,6 +2540,10 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -3847,7 +3860,7 @@ loader-utils@^0.2.15, loader-utils@^0.2.5: json5 "^0.5.0" object-assign "^4.0.1" -loader-utils@^1.0.2, loader-utils@^1.1.0: +loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" dependencies: @@ -5538,6 +5551,12 @@ sax@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -6605,6 +6624,13 @@ wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" +worker-loader@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.0.tgz#8cf21869a07add84d66f821d948d23c1eb98e809" + dependencies: + loader-utils "^1.0.0" + schema-utils "^0.3.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" -- cgit v1.2.1 From fd818194f157b4facd88ec829d5b43929c465de5 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 22 Nov 2017 16:40:06 +0000 Subject: fixed web worker performance [ci skip] --- app/assets/javascripts/repo/components/repo_editor.vue | 2 +- app/assets/javascripts/repo/lib/diff/controller.js | 11 ++++++++--- app/assets/javascripts/repo/lib/diff/diff.js | 1 + app/assets/javascripts/repo/lib/diff/diff_worker.js | 10 ++++++++++ app/assets/javascripts/repo/lib/diff/worker.diff.js | 10 ---------- config/webpack.config.js | 4 ++++ 6 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/repo/lib/diff/diff_worker.js delete mode 100644 app/assets/javascripts/repo/lib/diff/worker.diff.js diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index fa6070dbf92..f0fc0031e35 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -35,7 +35,7 @@ export default { this.editor.createInstance(this.$el); }) .then(() => this.setupEditor()) - .catch((e) => { throw e;flash('Error setting up monaco. Please try again.'); }); + .catch(() => flash('Error setting up monaco. Please try again.')); }, setupEditor() { if (!this.activeFile) return; diff --git a/app/assets/javascripts/repo/lib/diff/controller.js b/app/assets/javascripts/repo/lib/diff/controller.js index cd1a2025f17..dc0b1c95e59 100644 --- a/app/assets/javascripts/repo/lib/diff/controller.js +++ b/app/assets/javascripts/repo/lib/diff/controller.js @@ -1,5 +1,6 @@ /* global monaco */ -import DirtyDiffWorker from 'worker-loader!./worker.diff'; +import { throttle } from 'underscore'; +import DirtyDiffWorker from './diff_worker'; import Disposable from '../common/disposable'; export const getDiffChangeType = (change) => { @@ -34,12 +35,14 @@ export default class DirtyDiffController { 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', e => this.decorate(e)); + this.dirtyDiffWorker.addEventListener('message', this.decorate); } attachModel(model) { - model.onChange(() => this.computeDiff(model)); + model.onChange(() => this.throttledComputeDiff(model)); } computeDiff(model) { @@ -61,6 +64,8 @@ export default class DirtyDiffController { 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 index ada24740688..0e37f5c4704 100644 --- a/app/assets/javascripts/repo/lib/diff/diff.js +++ b/app/assets/javascripts/repo/lib/diff/diff.js @@ -1,5 +1,6 @@ import { diffLines } from 'diff'; +// eslint-disable-next-line import/prefer-default-export export const computeDiff = (originalContent, newContent) => { const changes = diffLines(originalContent, newContent); 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/diff/worker.diff.js b/app/assets/javascripts/repo/lib/diff/worker.diff.js deleted file mode 100644 index e74c4046330..00000000000 --- a/app/assets/javascripts/repo/lib/diff/worker.diff.js +++ /dev/null @@ -1,10 +0,0 @@ -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/config/webpack.config.js b/config/webpack.config.js index f7a7182a627..78ced4c3e8c 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -116,6 +116,10 @@ var config = { loader: 'url-loader', options: { limit: 2048 }, }, + { + test: /\_worker\.js$/, + loader: 'worker-loader', + }, { test: /\.(worker(\.min)?\.js|pdf|bmpr)$/, exclude: /node_modules/, -- cgit v1.2.1 From 061be86e052d42a66fa82dd09eab990cf5cf0986 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 28 Nov 2017 14:43:47 +0000 Subject: added specs --- app/assets/javascripts/repo/lib/common/model.js | 23 +-- .../javascripts/repo/lib/common/disposable_spec.js | 44 ++++++ .../repo/lib/common/model_manager_spec.js | 81 ++++++++++ spec/javascripts/repo/lib/common/model_spec.js | 84 ++++++++++ .../repo/lib/decorations/controller_spec.js | 120 ++++++++++++++ spec/javascripts/repo/lib/diff/controller_spec.js | 176 +++++++++++++++++++++ spec/javascripts/repo/lib/diff/diff_spec.js | 80 ++++++++++ spec/javascripts/repo/lib/editor_spec.js | 128 +++++++++++++++ 8 files changed, 714 insertions(+), 22 deletions(-) create mode 100644 spec/javascripts/repo/lib/common/disposable_spec.js create mode 100644 spec/javascripts/repo/lib/common/model_manager_spec.js create mode 100644 spec/javascripts/repo/lib/common/model_spec.js create mode 100644 spec/javascripts/repo/lib/decorations/controller_spec.js create mode 100644 spec/javascripts/repo/lib/diff/controller_spec.js create mode 100644 spec/javascripts/repo/lib/diff/diff_spec.js create mode 100644 spec/javascripts/repo/lib/editor_spec.js diff --git a/app/assets/javascripts/repo/lib/common/model.js b/app/assets/javascripts/repo/lib/common/model.js index fd6f41f87b1..23c4811e6c0 100644 --- a/app/assets/javascripts/repo/lib/common/model.js +++ b/app/assets/javascripts/repo/lib/common/model.js @@ -28,31 +28,10 @@ export default class Model { return this.model.uri.toString(); } - get originalUrl() { - return this.originalModel.uri.toString(); - } - get path() { return this.file.path; } - get diffModel() { - return Model.getDiffModel(this.model); - } - - get originalDiffModel() { - return Model.getDiffModel(this.originalModel); - } - - static getDiffModel(model) { - return { - url: model.uri.toString(), - versionId: model.getVersionId(), - lines: model.getLinesContent(), - EOL: '\n', - }; - } - getModel() { return this.model; } @@ -63,7 +42,7 @@ export default class Model { onChange(cb) { this.events.set( - this.file.path, + this.path, this.disposable.add( this.model.onDidChangeContent(e => cb(this.model, e)), ), diff --git a/spec/javascripts/repo/lib/common/disposable_spec.js b/spec/javascripts/repo/lib/common/disposable_spec.js new file mode 100644 index 00000000000..62c3913bf4d --- /dev/null +++ b/spec/javascripts/repo/lib/common/disposable_spec.js @@ -0,0 +1,44 @@ +import Disposable from '~/repo/lib/common/disposable'; + +describe('Multi-file editor library disposable class', () => { + let instance; + let disposableClass; + + beforeEach(() => { + instance = new Disposable(); + + disposableClass = { + dispose: jasmine.createSpy('dispose'), + }; + }); + + afterEach(() => { + instance.dispose(); + }); + + describe('add', () => { + it('adds disposable classes', () => { + instance.add(disposableClass); + + expect(instance.disposers.size).toBe(1); + }); + }); + + describe('dispose', () => { + beforeEach(() => { + instance.add(disposableClass); + }); + + it('calls dispose on all cached disposers', () => { + instance.dispose(); + + expect(disposableClass.dispose).toHaveBeenCalled(); + }); + + it('clears cached disposers', () => { + instance.dispose(); + + expect(instance.disposers.size).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/common/model_manager_spec.js b/spec/javascripts/repo/lib/common/model_manager_spec.js new file mode 100644 index 00000000000..8c134f178c0 --- /dev/null +++ b/spec/javascripts/repo/lib/common/model_manager_spec.js @@ -0,0 +1,81 @@ +/* global monaco */ +import monacoLoader from '~/repo/monaco_loader'; +import ModelManager from '~/repo/lib/common/model_manager'; +import { file } from '../../helpers'; + +describe('Multi-file editor library model manager', () => { + let instance; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + instance = new ModelManager(monaco); + + done(); + }); + }); + + afterEach(() => { + instance.dispose(); + }); + + describe('addModel', () => { + it('caches model', () => { + instance.addModel(file()); + + expect(instance.models.size).toBe(1); + }); + + it('caches model by file path', () => { + instance.addModel(file('path-name')); + + expect(instance.models.keys().next().value).toBe('path-name'); + }); + + it('adds model into disposable', () => { + spyOn(instance.disposable, 'add').and.callThrough(); + + instance.addModel(file()); + + expect(instance.disposable.add).toHaveBeenCalled(); + }); + + it('returns cached model', () => { + spyOn(instance.models, 'get').and.callThrough(); + + instance.addModel(file()); + instance.addModel(file()); + + expect(instance.models.get).toHaveBeenCalled(); + }); + }); + + describe('hasCachedModel', () => { + it('returns false when no models exist', () => { + expect(instance.hasCachedModel('path')).toBeFalsy(); + }); + + it('returns true when model exists', () => { + instance.addModel(file('path-name')); + + expect(instance.hasCachedModel('path-name')).toBeTruthy(); + }); + }); + + describe('dispose', () => { + it('clears cached models', () => { + instance.addModel(file()); + + instance.dispose(); + + expect(instance.models.size).toBe(0); + }); + + it('calls disposable dispose', () => { + spyOn(instance.disposable, 'dispose').and.callThrough(); + + instance.dispose(); + + expect(instance.disposable.dispose).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/common/model_spec.js b/spec/javascripts/repo/lib/common/model_spec.js new file mode 100644 index 00000000000..d41ade237ca --- /dev/null +++ b/spec/javascripts/repo/lib/common/model_spec.js @@ -0,0 +1,84 @@ +/* global monaco */ +import monacoLoader from '~/repo/monaco_loader'; +import Model from '~/repo/lib/common/model'; +import { file } from '../../helpers'; + +describe('Multi-file editor library model', () => { + let model; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + model = new Model(monaco, file('path')); + + done(); + }); + }); + + afterEach(() => { + model.dispose(); + }); + + it('creates original model & new model', () => { + expect(model.originalModel).not.toBeNull(); + expect(model.model).not.toBeNull(); + }); + + describe('path', () => { + it('returns file path', () => { + expect(model.path).toBe('path'); + }); + }); + + describe('getModel', () => { + it('returns model', () => { + expect(model.getModel()).toBe(model.model); + }); + }); + + describe('getOriginalModel', () => { + it('returns original model', () => { + expect(model.getOriginalModel()).toBe(model.originalModel); + }); + }); + + describe('onChange', () => { + it('caches event by path', () => { + model.onChange(() => {}); + + expect(model.events.size).toBe(1); + expect(model.events.keys().next().value).toBe('path'); + }); + + it('calls callback on change', (done) => { + const spy = jasmine.createSpy(); + model.onChange(spy); + + model.getModel().setValue('123'); + + setTimeout(() => { + expect(spy).toHaveBeenCalledWith(model.getModel(), jasmine.anything()); + done(); + }); + }); + }); + + describe('dispose', () => { + it('calls disposable dispose', () => { + spyOn(model.disposable, 'dispose').and.callThrough(); + + model.dispose(); + + expect(model.disposable.dispose).toHaveBeenCalled(); + }); + + it('clears events', () => { + model.onChange(() => {}); + + expect(model.events.size).toBe(1); + + model.dispose(); + + expect(model.events.size).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/decorations/controller_spec.js b/spec/javascripts/repo/lib/decorations/controller_spec.js new file mode 100644 index 00000000000..2e32e8fa0bd --- /dev/null +++ b/spec/javascripts/repo/lib/decorations/controller_spec.js @@ -0,0 +1,120 @@ +/* global monaco */ +import monacoLoader from '~/repo/monaco_loader'; +import editor from '~/repo/lib/editor'; +import DecorationsController from '~/repo/lib/decorations/controller'; +import Model from '~/repo/lib/common/model'; +import { file } from '../../helpers'; + +describe('Multi-file editor library decorations controller', () => { + let editorInstance; + let controller; + let model; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + editorInstance = editor.create(monaco); + editorInstance.createInstance(document.createElement('div')); + + controller = new DecorationsController(editorInstance); + model = new Model(monaco, file('path')); + + done(); + }); + }); + + afterEach(() => { + model.dispose(); + editorInstance.dispose(); + controller.dispose(); + }); + + describe('getAllDecorationsForModel', () => { + it('returns empty array when no decorations exist for model', () => { + const decorations = controller.getAllDecorationsForModel(model); + + expect(decorations).toEqual([]); + }); + + it('returns decorations by model URL', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + const decorations = controller.getAllDecorationsForModel(model); + + expect(decorations[0]).toEqual({ decoration: 'decorationValue' }); + }); + }); + + describe('addDecorations', () => { + it('caches decorations in a new map', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.decorations.size).toBe(1); + }); + + it('does not create new cache model', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]); + + expect(controller.decorations.size).toBe(1); + }); + + it('caches decorations by model URL', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.decorations.size).toBe(1); + expect(controller.decorations.keys().next().value).toBe('path'); + }); + + it('calls decorate method', () => { + spyOn(controller, 'decorate'); + + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.decorate).toHaveBeenCalled(); + }); + }); + + describe('decorate', () => { + it('sets decorations on editor instance', () => { + spyOn(controller.editor.instance, 'deltaDecorations'); + + controller.decorate(model); + + expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []); + }); + + it('caches decorations', () => { + spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]); + + controller.decorate(model); + + expect(controller.editorDecorations.size).toBe(1); + }); + + it('caches decorations by model URL', () => { + spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]); + + controller.decorate(model); + + expect(controller.editorDecorations.keys().next().value).toBe('path'); + }); + }); + + describe('dispose', () => { + it('clears cached decorations', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + controller.dispose(); + + expect(controller.decorations.size).toBe(0); + }); + + it('clears cached editorDecorations', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + controller.dispose(); + + expect(controller.editorDecorations.size).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/diff/controller_spec.js b/spec/javascripts/repo/lib/diff/controller_spec.js new file mode 100644 index 00000000000..ed62e28d3a3 --- /dev/null +++ b/spec/javascripts/repo/lib/diff/controller_spec.js @@ -0,0 +1,176 @@ +/* global monaco */ +import monacoLoader from '~/repo/monaco_loader'; +import editor from '~/repo/lib/editor'; +import ModelManager from '~/repo/lib/common/model_manager'; +import DecorationsController from '~/repo/lib/decorations/controller'; +import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/repo/lib/diff/controller'; +import { computeDiff } from '~/repo/lib/diff/diff'; +import { file } from '../../helpers'; + +describe('Multi-file editor library dirty diff controller', () => { + let editorInstance; + let controller; + let modelManager; + let decorationsController; + let model; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + editorInstance = editor.create(monaco); + editorInstance.createInstance(document.createElement('div')); + + modelManager = new ModelManager(monaco); + decorationsController = new DecorationsController(editorInstance); + + model = modelManager.addModel(file()); + + controller = new DirtyDiffController(modelManager, decorationsController); + + done(); + }); + }); + + afterEach(() => { + controller.dispose(); + model.dispose(); + decorationsController.dispose(); + editorInstance.dispose(); + }); + + describe('getDiffChangeType', () => { + ['added', 'removed', 'modified'].forEach((type) => { + it(`returns ${type}`, () => { + const change = { + [type]: true, + }; + + expect(getDiffChangeType(change)).toBe(type); + }); + }); + }); + + describe('getDecorator', () => { + ['added', 'removed', 'modified'].forEach((type) => { + it(`returns with linesDecorationsClassName for ${type}`, () => { + const change = { + [type]: true, + }; + + expect( + getDecorator(change).options.linesDecorationsClassName, + ).toBe(`dirty-diff dirty-diff-${type}`); + }); + + it('returns with line numbers', () => { + const change = { + lineNumber: 1, + endLineNumber: 2, + [type]: true, + }; + + const range = getDecorator(change).range; + + expect(range.startLineNumber).toBe(1); + expect(range.endLineNumber).toBe(2); + expect(range.startColumn).toBe(1); + expect(range.endColumn).toBe(1); + }); + }); + }); + + describe('attachModel', () => { + it('adds change event callback', () => { + spyOn(model, 'onChange'); + + controller.attachModel(model); + + expect(model.onChange).toHaveBeenCalled(); + }); + + it('calls throttledComputeDiff on change', () => { + spyOn(controller, 'throttledComputeDiff'); + + controller.attachModel(model); + + model.getModel().setValue('123'); + + expect(controller.throttledComputeDiff).toHaveBeenCalled(); + }); + }); + + describe('computeDiff', () => { + it('posts to worker', () => { + spyOn(controller.dirtyDiffWorker, 'postMessage'); + + controller.computeDiff(model); + + expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({ + path: model.path, + originalContent: '', + newContent: '', + }); + }); + }); + + describe('reDecorate', () => { + it('calls decorations controller decorate', () => { + spyOn(controller.decorationsController, 'decorate'); + + controller.reDecorate(model); + + expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model); + }); + }); + + describe('decorate', () => { + it('adds decorations into decorations controller', () => { + spyOn(controller.decorationsController, 'addDecorations'); + + controller.decorate({ data: { changes: [], path: 'path' } }); + + expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith('path', 'dirtyDiff', jasmine.anything()); + }); + + it('adds decorations into editor', () => { + const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations'); + + controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } }); + + expect(spy).toHaveBeenCalledWith([], [{ + range: new monaco.Range( + 1, 1, 1, 1, + ), + options: { + isWholeLine: true, + linesDecorationsClassName: 'dirty-diff dirty-diff-modified', + }, + }]); + }); + }); + + describe('dispose', () => { + it('calls disposable dispose', () => { + spyOn(controller.disposable, 'dispose').and.callThrough(); + + controller.dispose(); + + expect(controller.disposable.dispose).toHaveBeenCalled(); + }); + + it('terminates worker', () => { + spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough(); + + controller.dispose(); + + expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled(); + }); + + it('removes worker event listener', () => { + spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough(); + + controller.dispose(); + + expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything()); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js new file mode 100644 index 00000000000..8a18ee89c47 --- /dev/null +++ b/spec/javascripts/repo/lib/diff/diff_spec.js @@ -0,0 +1,80 @@ +import { computeDiff } from '~/repo/lib/diff/diff'; + +describe('Multi-file editor library diff calculator', () => { + describe('computeDiff', () => { + it('returns empty array if no changes', () => { + const diff = computeDiff('123', '123'); + + expect(diff).toEqual([]); + }); + + describe('modified', () => { + it('', () => { + const diff = computeDiff('123', '1234'); + + expect(diff[0].added).toBeTruthy(); + expect(diff[0].modified).toBeTruthy(); + expect(diff[0].removed).toBeUndefined(); + }); + + it('', () => { + const diff = computeDiff('123\n123\n123', '123\n1234\n123'); + + expect(diff[0].added).toBeTruthy(); + expect(diff[0].modified).toBeTruthy(); + expect(diff[0].removed).toBeUndefined(); + expect(diff[0].lineNumber).toBe(2); + }); + }); + + describe('added', () => { + it('', () => { + const diff = computeDiff('123', '123\n123'); + + expect(diff[0].added).toBeTruthy(); + expect(diff[0].modified).toBeUndefined(); + expect(diff[0].removed).toBeUndefined(); + }); + + it('', () => { + const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123'); + + expect(diff[0].added).toBeTruthy(); + expect(diff[0].modified).toBeUndefined(); + expect(diff[0].removed).toBeUndefined(); + expect(diff[0].lineNumber).toBe(3); + }); + }); + + describe('removed', () => { + it('', () => { + const diff = computeDiff('123', ''); + + expect(diff[0].added).toBeUndefined(); + expect(diff[0].modified).toBeUndefined(); + expect(diff[0].removed).toBeTruthy(); + }); + + it('', () => { + const diff = computeDiff('123\n123\n123', '123\n123'); + + expect(diff[0].added).toBeUndefined(); + expect(diff[0].modified).toBeTruthy(); + expect(diff[0].removed).toBeTruthy(); + expect(diff[0].lineNumber).toBe(2); + }); + }); + + it('includes line number of change', () => { + const diff = computeDiff('123', ''); + + expect(diff[0].lineNumber).toBe(1); + }); + + it('includes end line number of change', () => { + const diff = computeDiff('123', ''); + + expect(diff[0].endLineNumber).toBe(1); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/editor_spec.js b/spec/javascripts/repo/lib/editor_spec.js new file mode 100644 index 00000000000..cd32832a232 --- /dev/null +++ b/spec/javascripts/repo/lib/editor_spec.js @@ -0,0 +1,128 @@ +/* global monaco */ +import monacoLoader from '~/repo/monaco_loader'; +import editor from '~/repo/lib/editor'; +import { file } from '../helpers'; + +describe('Multi-file editor library', () => { + let instance; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + instance = editor.create(monaco); + + done(); + }); + }); + + afterEach(() => { + instance.dispose(); + }); + + it('creates instance of editor', () => { + expect(editor.editorInstance).not.toBeNull(); + }); + + describe('createInstance', () => { + let el; + + beforeEach(() => { + el = document.createElement('div'); + }); + + it('creates editor instance', () => { + spyOn(instance.monaco.editor, 'create').and.callThrough(); + + instance.createInstance(el); + + expect(instance.monaco.editor.create).toHaveBeenCalled(); + }); + + it('creates dirty diff controller', () => { + instance.createInstance(el); + + expect(instance.dirtyDiffController).not.toBeNull(); + }); + }); + + describe('createModel', () => { + it('calls model manager addModel', () => { + spyOn(instance.modelManager, 'addModel'); + + instance.createModel('FILE'); + + expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE'); + }); + }); + + describe('attachModel', () => { + let model; + + beforeEach(() => { + instance.createInstance(document.createElement('div')); + + model = instance.createModel(file()); + }); + + it('sets the current model on the instance', () => { + instance.attachModel(model); + + expect(instance.currentModel).toBe(model); + }); + + it('attaches the model to the current instance', () => { + spyOn(instance.instance, 'setModel'); + + instance.attachModel(model); + + expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel()); + }); + + it('attaches the model to the dirty diff controller', () => { + spyOn(instance.dirtyDiffController, 'attachModel'); + + instance.attachModel(model); + + expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model); + }); + + it('re-decorates with the dirty diff controller', () => { + spyOn(instance.dirtyDiffController, 'reDecorate'); + + instance.attachModel(model); + + expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model); + }); + }); + + describe('clearEditor', () => { + it('resets the editor model', () => { + instance.createInstance(document.createElement('div')); + + spyOn(instance.instance, 'setModel'); + + instance.clearEditor(); + + expect(instance.instance.setModel).toHaveBeenCalledWith(null); + }); + }); + + describe('dispose', () => { + it('calls disposble dispose method', () => { + spyOn(instance.disposable, 'dispose').and.callThrough(); + + instance.dispose(); + + expect(instance.disposable.dispose).toHaveBeenCalled(); + }); + + it('resets instance', () => { + instance.createInstance(document.createElement('div')); + + expect(instance.instance).not.toBeNull(); + + instance.dispose(); + + expect(instance.instance).toBeNull(); + }); + }); +}); -- cgit v1.2.1 From 5fceb8d1ecb8b3b682ad0cc73a26b99068864f3f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 28 Nov 2017 15:21:46 +0000 Subject: fixed scss-lint --- app/assets/stylesheets/pages/repo.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index a2a2bc408db..c45a478f2df 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -306,8 +306,8 @@ .dirty-diff { // !important need to override monaco inline style - width: 4px !important; - left: 0 !important; + width: 4px !important; + left: 0 !important; &-modified { background-color: $blue-500; @@ -318,8 +318,8 @@ } &-removed { - height: 0!important; - width: 0!important; + height: 0 !important; + width: 0 !important; bottom: -2px; border-style: solid; border-width: 5px; -- cgit v1.2.1 From d22bb0397b04f2870c6fbbeb2912ef6627e4afb7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 28 Nov 2017 16:51:37 +0000 Subject: fixed failing specs --- app/assets/javascripts/repo/components/repo_editor.vue | 13 +++++++++---- package.json | 6 +++--- spec/javascripts/repo/components/repo_editor_spec.js | 10 +++++++--- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index f0fc0031e35..f37cbd1e961 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -6,11 +6,11 @@ import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; export default { - destroyed() { + beforeDestroy() { this.editor.dispose(); }, mounted() { - if (this.monaco) { + if (this.editor && monaco) { this.initMonaco(); } else { monacoLoader(['vs/editor/editor.main'], () => { @@ -32,7 +32,7 @@ export default { this.getRawFileData(this.activeFile) .then(() => { - this.editor.createInstance(this.$el); + this.editor.createInstance(this.$refs.editor); }) .then(() => this.setupEditor()) .catch(() => flash('Error setting up monaco. Please try again.')); @@ -76,9 +76,14 @@ export default { class="blob-viewer-container blob-editor-container" >
+
+
diff --git a/package.json b/package.json index 8f06ffc1b6d..3c860d5e486 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,8 @@ "vuex": "^3.0.1", "webpack": "^3.5.5", "webpack-bundle-analyzer": "^2.8.2", - "webpack-stats-plugin": "^0.1.5" + "webpack-stats-plugin": "^0.1.5", + "worker-loader": "^1.1.0" }, "devDependencies": { "@gitlab-org/gitlab-svgs": "^1.1.1", @@ -98,7 +99,6 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^2.0.4", "nodemon": "^1.11.0", - "webpack-dev-server": "^2.6.1", - "worker-loader": "^1.1.0" + "webpack-dev-server": "^2.6.1" } } diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js index 979d2185076..81158cad639 100644 --- a/spec/javascripts/repo/components/repo_editor_spec.js +++ b/spec/javascripts/repo/components/repo_editor_spec.js @@ -1,12 +1,13 @@ import Vue from 'vue'; import store from '~/repo/stores'; import repoEditor from '~/repo/components/repo_editor.vue'; +import monacoLoader from '~/repo/monaco_loader'; import { file, resetStore } from '../helpers'; describe('RepoEditor', () => { let vm; - beforeEach(() => { + beforeEach((done) => { const f = file(); const RepoEditor = Vue.extend(repoEditor); @@ -21,6 +22,10 @@ describe('RepoEditor', () => { vm.monaco = true; vm.$mount(); + + monacoLoader(['vs/editor/editor.main'], () => { + setTimeout(done, 0); + }); }); afterEach(() => { @@ -32,7 +37,6 @@ describe('RepoEditor', () => { it('renders an ide container', (done) => { Vue.nextTick(() => { expect(vm.shouldHideEditor).toBeFalsy(); - expect(vm.$el.textContent.trim()).toBe(''); done(); }); @@ -50,7 +54,7 @@ describe('RepoEditor', () => { }); it('shows activeFile html', () => { - expect(vm.$el.textContent.trim()).toBe('testing'); + expect(vm.$el.textContent).toContain('testing'); }); }); }); -- cgit v1.2.1 From 1880809d8ef9b650d1af615ebbaa590626ccf17f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 29 Nov 2017 10:26:35 +0000 Subject: added editor options to redcue EE conflicts --- app/assets/javascripts/repo/lib/editor.js | 10 ++++++++++ app/assets/javascripts/repo/lib/editor_options.js | 2 ++ spec/javascripts/repo/lib/editor_options_spec.js | 7 +++++++ 3 files changed, 19 insertions(+) create mode 100644 app/assets/javascripts/repo/lib/editor_options.js create mode 100644 spec/javascripts/repo/lib/editor_options_spec.js diff --git a/app/assets/javascripts/repo/lib/editor.js b/app/assets/javascripts/repo/lib/editor.js index e82fbfc0e9c..db499444402 100644 --- a/app/assets/javascripts/repo/lib/editor.js +++ b/app/assets/javascripts/repo/lib/editor.js @@ -2,6 +2,7 @@ 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) { @@ -49,6 +50,15 @@ export default class Editor { 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); } 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/spec/javascripts/repo/lib/editor_options_spec.js b/spec/javascripts/repo/lib/editor_options_spec.js new file mode 100644 index 00000000000..b4887d063ed --- /dev/null +++ b/spec/javascripts/repo/lib/editor_options_spec.js @@ -0,0 +1,7 @@ +import editorOptions from '~/repo/lib/editor_options'; + +describe('Multi-file editor library editor options', () => { + it('returns an array', () => { + expect(editorOptions).toEqual(jasmine.any(Array)); + }); +}); -- cgit v1.2.1 From cf26f27d98dfc2c8b4c441c3c437145ba3df5e50 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 30 Nov 2017 10:01:26 +0000 Subject: updated diff spec --- spec/javascripts/repo/lib/diff/diff_spec.js | 62 ++++++++++++++--------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js index 8a18ee89c47..3269ec5d2c9 100644 --- a/spec/javascripts/repo/lib/diff/diff_spec.js +++ b/spec/javascripts/repo/lib/diff/diff_spec.js @@ -10,71 +10,71 @@ describe('Multi-file editor library diff calculator', () => { describe('modified', () => { it('', () => { - const diff = computeDiff('123', '1234'); + const diff = computeDiff('123', '1234')[0]; - expect(diff[0].added).toBeTruthy(); - expect(diff[0].modified).toBeTruthy(); - expect(diff[0].removed).toBeUndefined(); + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeUndefined(); }); it('', () => { - const diff = computeDiff('123\n123\n123', '123\n1234\n123'); + const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0]; - expect(diff[0].added).toBeTruthy(); - expect(diff[0].modified).toBeTruthy(); - expect(diff[0].removed).toBeUndefined(); - expect(diff[0].lineNumber).toBe(2); + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeUndefined(); + expect(diff.lineNumber).toBe(2); }); }); describe('added', () => { it('', () => { - const diff = computeDiff('123', '123\n123'); + const diff = computeDiff('123', '123\n123')[0]; - expect(diff[0].added).toBeTruthy(); - expect(diff[0].modified).toBeUndefined(); - expect(diff[0].removed).toBeUndefined(); + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeUndefined(); }); it('', () => { - const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123'); + const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0]; - expect(diff[0].added).toBeTruthy(); - expect(diff[0].modified).toBeUndefined(); - expect(diff[0].removed).toBeUndefined(); - expect(diff[0].lineNumber).toBe(3); + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeUndefined(); + expect(diff.lineNumber).toBe(3); }); }); describe('removed', () => { it('', () => { - const diff = computeDiff('123', ''); + const diff = computeDiff('123', '')[0]; - expect(diff[0].added).toBeUndefined(); - expect(diff[0].modified).toBeUndefined(); - expect(diff[0].removed).toBeTruthy(); + expect(diff.added).toBeUndefined(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeTruthy(); }); it('', () => { - const diff = computeDiff('123\n123\n123', '123\n123'); + const diff = computeDiff('123\n123\n123', '123\n123')[0]; - expect(diff[0].added).toBeUndefined(); - expect(diff[0].modified).toBeTruthy(); - expect(diff[0].removed).toBeTruthy(); - expect(diff[0].lineNumber).toBe(2); + expect(diff.added).toBeUndefined(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeTruthy(); + expect(diff.lineNumber).toBe(2); }); }); it('includes line number of change', () => { - const diff = computeDiff('123', ''); + const diff = computeDiff('123', '')[0]; - expect(diff[0].lineNumber).toBe(1); + expect(diff.lineNumber).toBe(1); }); it('includes end line number of change', () => { - const diff = computeDiff('123', ''); + const diff = computeDiff('123', '')[0]; - expect(diff[0].endLineNumber).toBe(1); + expect(diff.endLineNumber).toBe(1); }); }); }); -- cgit v1.2.1