Commit c90b520d authored by Phil Hughes's avatar Phil Hughes

created editor library to manage all things editor

[ci skip]
parent 37864fb0
...@@ -3,21 +3,17 @@ ...@@ -3,21 +3,17 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import flash from '../../flash'; import flash from '../../flash';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import editor from '../lib/editor';
export default { export default {
destroyed() { destroyed() {
if (this.monacoInstance) { editor.dispose();
this.monacoInstance.destroy();
}
}, },
mounted() { mounted() {
if (this.monaco) { if (this.monaco) {
this.initMonaco(); this.initMonaco();
} else { } else {
monacoLoader(['vs/editor/editor.main', 'vs/editor/common/diff/diffComputer'], (_, { DiffComputer }) => { monacoLoader(['vs/editor/editor.main'], () => {
this.monaco = monaco;
this.DiffComputer = DiffComputer;
this.initMonaco(); this.initMonaco();
}); });
} }
...@@ -30,84 +26,25 @@ export default { ...@@ -30,84 +26,25 @@ export default {
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
if (this.monacoInstance) { editor.clearEditor();
this.monacoInstance.setModel(null);
}
this.getRawFileData(this.activeFile) this.getRawFileData(this.activeFile)
.then(() => { .then(() => {
if (!this.monacoInstance) { editor.createInstance(this.$el);
this.monacoInstance = this.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
this.languages = this.monaco.languages.getLanguages();
}
this.setupEditor();
}) })
.then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.')); .catch(() => flash('Error setting up monaco. Please try again.'));
}, },
setupEditor() { setupEditor() {
if (!this.activeFile) return; 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); const model = editor.createModel(this.activeFile);
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)}`,
},
})),
);
editor.attachModel(model);
model.onChange((m) => {
this.changeFileContent({ this.changeFileContent({
file: this.activeFile, file: this.activeFile,
content: this.monacoInstance.getValue(), content: m.getValue(),
}); });
}); });
}, },
......
/* 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();
}
}
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();
/* 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();
}
}
/* 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;
}
}
/* 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();
...@@ -16,6 +16,10 @@ export default { ...@@ -16,6 +16,10 @@ export default {
return Promise.resolve(file.content); return Promise.resolve(file.content);
} }
if (file.raw) {
return Promise.resolve(file.raw);
}
return Vue.http.get(file.rawPath, { params: { format: 'json' } }) return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text()); .then(res => res.text());
}, },
......
...@@ -70,6 +70,7 @@ ...@@ -70,6 +70,7 @@
.line-numbers { .line-numbers {
cursor: pointer; cursor: pointer;
min-width: initial;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
...@@ -309,16 +310,29 @@ ...@@ -309,16 +310,29 @@
left: 0 !important; left: 0 !important;
&-modified { &-modified {
background-color: rgb(19, 117, 150); background-color: $blue-500;
} }
&-added { &-added {
background-color: rgb(89, 119, 11); background-color: $green-600;
} }
&-removed { &-removed {
height: 4px!important; height: 0!important;
width: 0!important;
bottom: -2px; 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);
}
} }
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment