diff options
Diffstat (limited to 'app/assets/javascripts/ide/lib')
-rw-r--r-- | app/assets/javascripts/ide/lib/common/disposable.js | 14 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/common/model.js | 64 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/common/model_manager.js | 32 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/decorations/controller.js | 43 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/diff/controller.js | 71 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/diff/diff.js | 30 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/diff/diff_worker.js | 10 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/editor.js | 109 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/editor_options.js | 2 |
9 files changed, 375 insertions, 0 deletions
diff --git a/app/assets/javascripts/ide/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js new file mode 100644 index 00000000000..84b29bdb600 --- /dev/null +++ b/app/assets/javascripts/ide/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/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js new file mode 100644 index 00000000000..14d9fe4771e --- /dev/null +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -0,0 +1,64 @@ +/* global monaco */ +import Disposable from './disposable'; + +export default class Model { + constructor(monaco, file) { + this.monaco = monaco; + this.disposable = new Disposable(); + this.file = file; + this.content = file.content !== '' ? file.content : file.raw; + + this.disposable.add( + this.originalModel = this.monaco.editor.createModel( + this.file.raw, + undefined, + new this.monaco.Uri(null, null, `original/${this.file.path}`), + ), + this.model = this.monaco.editor.createModel( + this.content, + undefined, + new this.monaco.Uri(null, null, this.file.path), + ), + ); + + this.events = new Map(); + } + + get url() { + return this.model.uri.toString(); + } + + get language() { + return this.model.getModeId(); + } + + get eol() { + return this.model.getEOL() === '\n' ? 'LF' : 'CRLF'; + } + + get path() { + return this.file.path; + } + + getModel() { + return this.model; + } + + getOriginalModel() { + return this.originalModel; + } + + onChange(cb) { + this.events.set( + this.path, + this.disposable.add( + this.model.onDidChangeContent(e => cb(this.model, e)), + ), + ); + } + + dispose() { + this.disposable.dispose(); + this.events.clear(); + } +} diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js new file mode 100644 index 00000000000..fd462252795 --- /dev/null +++ b/app/assets/javascripts/ide/lib/common/model_manager.js @@ -0,0 +1,32 @@ +import Disposable from './disposable'; +import Model from './model'; + +export default class ModelManager { + constructor(monaco) { + this.monaco = monaco; + this.disposable = new Disposable(); + this.models = new Map(); + } + + hasCachedModel(path) { + return this.models.has(path); + } + + addModel(file) { + if (this.hasCachedModel(file.path)) { + return this.models.get(file.path); + } + + const model = new Model(this.monaco, file); + this.models.set(model.path, model); + this.disposable.add(model); + + return model; + } + + dispose() { + // dispose of all the models + this.disposable.dispose(); + this.models.clear(); + } +} diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js new file mode 100644 index 00000000000..0954b7973c4 --- /dev/null +++ b/app/assets/javascripts/ide/lib/decorations/controller.js @@ -0,0 +1,43 @@ +export default class DecorationsController { + constructor(editor) { + this.editor = editor; + this.decorations = new Map(); + this.editorDecorations = new Map(); + } + + getAllDecorationsForModel(model) { + if (!this.decorations.has(model.url)) return []; + + const modelDecorations = this.decorations.get(model.url); + const decorations = []; + + modelDecorations.forEach(val => decorations.push(...val)); + + return decorations; + } + + addDecorations(model, decorationsKey, decorations) { + const decorationMap = this.decorations.get(model.url) || new Map(); + + decorationMap.set(decorationsKey, decorations); + + this.decorations.set(model.url, decorationMap); + + this.decorate(model); + } + + decorate(model) { + const decorations = this.getAllDecorationsForModel(model); + const oldDecorations = this.editorDecorations.get(model.url) || []; + + this.editorDecorations.set( + model.url, + this.editor.instance.deltaDecorations(oldDecorations, decorations), + ); + } + + dispose() { + this.decorations.clear(); + this.editorDecorations.clear(); + } +} diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js new file mode 100644 index 00000000000..dc0b1c95e59 --- /dev/null +++ b/app/assets/javascripts/ide/lib/diff/controller.js @@ -0,0 +1,71 @@ +/* global monaco */ +import { throttle } from 'underscore'; +import DirtyDiffWorker from './diff_worker'; +import Disposable from '../common/disposable'; + +export const getDiffChangeType = (change) => { + if (change.modified) { + return 'modified'; + } else if (change.added) { + return 'added'; + } else if (change.removed) { + return 'removed'; + } + + return ''; +}; + +export const getDecorator = change => ({ + range: new monaco.Range( + change.lineNumber, + 1, + change.endLineNumber, + 1, + ), + options: { + isWholeLine: true, + linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`, + }, +}); + +export default class DirtyDiffController { + constructor(modelManager, decorationsController) { + this.disposable = new Disposable(); + this.editorSimpleWorker = null; + this.modelManager = modelManager; + this.decorationsController = decorationsController; + this.dirtyDiffWorker = new DirtyDiffWorker(); + this.throttledComputeDiff = throttle(this.computeDiff, 250); + this.decorate = this.decorate.bind(this); + + this.dirtyDiffWorker.addEventListener('message', this.decorate); + } + + attachModel(model) { + model.onChange(() => this.throttledComputeDiff(model)); + } + + computeDiff(model) { + this.dirtyDiffWorker.postMessage({ + path: model.path, + originalContent: model.getOriginalModel().getValue(), + newContent: model.getModel().getValue(), + }); + } + + reDecorate(model) { + this.decorationsController.decorate(model); + } + + decorate({ data }) { + const decorations = data.changes.map(change => getDecorator(change)); + this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations); + } + + dispose() { + this.disposable.dispose(); + + this.dirtyDiffWorker.removeEventListener('message', this.decorate); + this.dirtyDiffWorker.terminate(); + } +} diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js new file mode 100644 index 00000000000..0e37f5c4704 --- /dev/null +++ b/app/assets/javascripts/ide/lib/diff/diff.js @@ -0,0 +1,30 @@ +import { diffLines } from 'diff'; + +// eslint-disable-next-line import/prefer-default-export +export const computeDiff = (originalContent, newContent) => { + const changes = diffLines(originalContent, newContent); + + let lineNumber = 1; + return changes.reduce((acc, change) => { + const findOnLine = acc.find(c => c.lineNumber === lineNumber); + + if (findOnLine) { + Object.assign(findOnLine, change, { + modified: true, + endLineNumber: (lineNumber + change.count) - 1, + }); + } else if ('added' in change || 'removed' in change) { + acc.push(Object.assign({}, change, { + lineNumber, + modified: undefined, + endLineNumber: (lineNumber + change.count) - 1, + })); + } + + if (!change.removed) { + lineNumber += change.count; + } + + return acc; + }, []); +}; diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js new file mode 100644 index 00000000000..e74c4046330 --- /dev/null +++ b/app/assets/javascripts/ide/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/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js new file mode 100644 index 00000000000..51e202b9348 --- /dev/null +++ b/app/assets/javascripts/ide/lib/editor.js @@ -0,0 +1,109 @@ +import DecorationsController from './decorations/controller'; +import DirtyDiffController from './diff/controller'; +import Disposable from './common/disposable'; +import ModelManager from './common/model_manager'; +import editorOptions from './editor_options'; + +export default class Editor { + static create(monaco) { + this.editorInstance = new Editor(monaco); + + return this.editorInstance; + } + + constructor(monaco) { + this.monaco = monaco; + this.currentModel = null; + this.instance = null; + this.dirtyDiffController = null; + this.disposable = new Disposable(); + + this.disposable.add( + this.modelManager = new ModelManager(this.monaco), + this.decorationsController = new DecorationsController(this), + ); + + this.debouncedUpdate = _.debounce(() => { + this.updateDimensions(); + }, 200); + window.addEventListener('resize', this.debouncedUpdate, false); + } + + createInstance(domElement) { + if (!this.instance) { + this.disposable.add( + this.instance = this.monaco.editor.create(domElement, { + model: null, + readOnly: false, + contextmenu: true, + scrollBeyondLastLine: false, + minimap: { + enabled: false, + }, + }), + this.dirtyDiffController = new DirtyDiffController( + this.modelManager, this.decorationsController, + ), + ); + } + } + + createModel(file) { + return this.modelManager.addModel(file); + } + + attachModel(model) { + this.instance.setModel(model.getModel()); + this.dirtyDiffController.attachModel(model); + + this.currentModel = model; + + this.instance.updateOptions(editorOptions.reduce((acc, obj) => { + Object.keys(obj).forEach((key) => { + Object.assign(acc, { + [key]: obj[key](model), + }); + }); + return acc; + }, {})); + + this.dirtyDiffController.reDecorate(model); + } + + clearEditor() { + if (this.instance) { + this.instance.setModel(null); + } + } + + dispose() { + this.disposable.dispose(); + window.removeEventListener('resize', this.debouncedUpdate); + + // dispose main monaco instance + if (this.instance) { + this.instance = null; + } + } + + updateDimensions() { + this.instance.layout(); + } + + setPosition({ lineNumber, column }) { + this.instance.revealPositionInCenter({ + lineNumber, + column, + }); + this.instance.setPosition({ + lineNumber, + column, + }); + } + + onPositionChange(cb) { + this.disposable.add( + this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)), + ); + } +} diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js new file mode 100644 index 00000000000..701affc466e --- /dev/null +++ b/app/assets/javascripts/ide/lib/editor_options.js @@ -0,0 +1,2 @@ +export default [{ +}]; |