Commit 97c7c8ec authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'ide-diff-view' into 'master'

IDE open changed files in diff viewer

Closes #4568

See merge request gitlab-org/gitlab-ee!4789
parents 371de4d9 22e6e3f2
...@@ -132,13 +132,35 @@ ...@@ -132,13 +132,35 @@
.multi-file-tabs { .multi-file-tabs {
display: flex; display: flex;
overflow-x: auto;
background-color: $white-normal; background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark; box-shadow: inset 0 -1px $white-dark;
> li { > ul {
display: flex;
overflow-x: auto;
}
li {
position: relative; position: relative;
} }
.dropdown {
display: flex;
margin-left: auto;
margin-bottom: 1px;
padding: 0 $grid-size;
border-left: 1px solid $white-dark;
background-color: $white-light;
&.shadow {
box-shadow: 0 0 10px $dropdown-shadow-color;
}
.btn {
margin-top: auto;
margin-bottom: auto;
}
}
} }
.multi-file-tab { .multi-file-tab {
...@@ -207,6 +229,70 @@ ...@@ -207,6 +229,70 @@
.vertical-center { .vertical-center {
min-height: auto; min-height: auto;
} }
.monaco-editor .lines-content .cigr {
display: none;
}
.monaco-diff-editor.vs {
.editor.modified {
box-shadow: none;
}
.diagonal-fill {
display: none !important;
}
.diffOverview {
background-color: $white-light;
border-left: 1px solid $white-dark;
cursor: ns-resize;
}
.diffViewport {
display: none;
}
.char-insert {
background-color: $line-added-dark;
}
.char-delete {
background-color: $line-removed-dark;
}
.line-numbers {
color: $black-transparent;
}
.view-overlays {
.line-insert {
background-color: $line-added;
}
.line-delete {
background-color: $line-removed;
}
}
.margin {
background-color: $gray-light;
border-right: 1px solid $white-normal;
.line-insert {
border-right: 1px solid $line-added-dark;
}
.line-delete {
border-right: 1px solid $line-removed-dark;
}
}
.margin-view-overlays .insert-sign,
.margin-view-overlays .delete-sign {
opacity: .4;
}
}
} }
.multi-file-editor-holder { .multi-file-editor-holder {
......
...@@ -24,8 +24,11 @@ ...@@ -24,8 +24,11 @@
methods: { methods: {
...mapActions([ ...mapActions([
'discardFileChanges', 'discardFileChanges',
'updateViewer',
]), ]),
openFileInEditor(file) { openFileInEditor(file) {
this.updateViewer('diff');
router.push(`/project${file.url}`); router.push(`/project${file.url}`);
}, },
}, },
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
required: true,
},
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
},
};
</script>
<template>
<div
class="dropdown"
:class="{
shadow: showShadow,
}"
>
<button
type="button"
class="btn btn-primary btn-sm"
:class="{
'btn-inverted': hasChanges,
}"
data-toggle="dropdown"
>
<template v-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
{{ __('Reviewing') }}
</template>
<icon
name="angle-down"
:size="12"
css-classes="caret-down"
/>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
<li>
<a
href="#"
@click.prevent="changeMode('editor')"
:class="{
'is-active': viewer === 'editor',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('View and edit lines') }}
</span>
</a>
</li>
<li>
<a
href="#"
@click.prevent="changeMode('diff')"
:class="{
'is-active': viewer === 'diff',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the last commit') }}
</span>
</a>
</li>
</ul>
</div>
</div>
</template>
...@@ -15,6 +15,8 @@ export default { ...@@ -15,6 +15,8 @@ export default {
'leftPanelCollapsed', 'leftPanelCollapsed',
'rightPanelCollapsed', 'rightPanelCollapsed',
'panelResizing', 'panelResizing',
'viewer',
'delayViewerUpdated',
]), ]),
shouldHideEditor() { shouldHideEditor() {
return this.activeFile && this.activeFile.binary && !this.activeFile.raw; return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
...@@ -37,6 +39,9 @@ export default { ...@@ -37,6 +39,9 @@ export default {
this.editor.updateDimensions(); this.editor.updateDimensions();
} }
}, },
viewer() {
this.createEditorInstance();
},
}, },
beforeDestroy() { beforeDestroy() {
this.editor.dispose(); this.editor.dispose();
...@@ -59,6 +64,8 @@ export default { ...@@ -59,6 +64,8 @@ export default {
'setFileLanguage', 'setFileLanguage',
'setEditorPosition', 'setEditorPosition',
'setFileEOL', 'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
]), ]),
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
...@@ -67,16 +74,34 @@ export default { ...@@ -67,16 +74,34 @@ export default {
this.getRawFileData(this.activeFile) this.getRawFileData(this.activeFile)
.then(() => { .then(() => {
this.editor.createInstance(this.$refs.editor); const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
return viewerPromise;
})
.then(() => {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
}) })
.then(() => this.setupEditor())
.catch((err) => { .catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err; throw err;
}); });
}, },
createEditorInstance() {
this.editor.dispose();
this.$nextTick(() => {
if (this.viewer === 'editor') {
this.editor.createInstance(this.$refs.editor);
} else {
this.editor.createDiffInstance(this.$refs.editor);
}
this.setupEditor();
});
},
setupEditor() { setupEditor() {
if (!this.activeFile) return; if (!this.activeFile || !this.editor.instance) return;
this.model = this.editor.createModel(this.activeFile); this.model = this.editor.createModel(this.activeFile);
......
<script> <script>
import { mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
...@@ -70,6 +70,9 @@ ...@@ -70,6 +70,9 @@
} }
}, },
methods: { methods: {
...mapActions([
'updateDelayViewerUpdated',
]),
clickFile(row) { clickFile(row) {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) { if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
...@@ -78,7 +81,13 @@ ...@@ -78,7 +81,13 @@
tree: this.file, tree: this.file,
}); });
} }
this.$router.push(`/project${row.url}`);
const delayPromise = this.file.changed ?
Promise.resolve() : this.updateDelayViewerUpdated(true);
return delayPromise.then(() => {
this.$router.push(`/project${row.url}`);
});
}, },
}, },
}; };
......
<script> <script>
import { mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
export default { export default {
components: { components: {
'repo-tab': RepoTab, RepoTab,
EditorMode,
},
data() {
return {
showShadow: false,
};
}, },
computed: { computed: {
...mapGetters([
'hasChanges',
]),
...mapState([ ...mapState([
'openFiles', 'openFiles',
'viewer',
]),
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions([
'updateViewer',
]), ]),
}, },
}; };
</script> </script>
<template> <template>
<ul <div class="multi-file-tabs">
class="multi-file-tabs list-unstyled append-bottom-0" <ul
> class="list-unstyled append-bottom-0"
<repo-tab ref="tabsScroller"
v-for="tab in openFiles" >
:key="tab.key" <repo-tab
:tab="tab" v-for="tab in openFiles"
:key="tab.key"
:tab="tab"
/>
</ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
@click="updateViewer"
/> />
</ul> </div>
</template> </template>
...@@ -26,6 +26,9 @@ export default class Model { ...@@ -26,6 +26,9 @@ export default class Model {
this.events = new Map(); this.events = new Map();
this.updateContent = this.updateContent.bind(this); this.updateContent = this.updateContent.bind(this);
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent); eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
} }
...@@ -75,6 +78,7 @@ export default class Model { ...@@ -75,6 +78,7 @@ export default class Model {
this.disposable.dispose(); this.disposable.dispose();
this.events.clear(); this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent); eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
} }
} }
import eventHub from 'ee/ide/eventhub';
import Disposable from './disposable'; import Disposable from './disposable';
import Model from './model'; import Model from './model';
...@@ -25,9 +26,17 @@ export default class ModelManager { ...@@ -25,9 +26,17 @@ export default class ModelManager {
this.models.set(model.path, model); this.models.set(model.path, model);
this.disposable.add(model); this.disposable.add(model);
eventHub.$on(`editor.update.model.dispose.${file.path}`, this.removeCachedModel.bind(this, file));
return model; return model;
} }
removeCachedModel(file) {
this.models.delete(file.path);
eventHub.$off(`editor.update.model.dispose.${file.path}`, this.removeCachedModel);
}
dispose() { dispose() {
// dispose of all the models // dispose of all the models
this.disposable.dispose(); this.disposable.dispose();
......
...@@ -27,6 +27,8 @@ export default class DecorationsController { ...@@ -27,6 +27,8 @@ export default class DecorationsController {
} }
decorate(model) { decorate(model) {
if (!this.editor.instance) return;
const decorations = this.getAllDecorationsForModel(model); const decorations = this.getAllDecorationsForModel(model);
const oldDecorations = this.editorDecorations.get(model.url) || []; const oldDecorations = this.editorDecorations.get(model.url) || [];
......
...@@ -3,9 +3,16 @@ import DecorationsController from './decorations/controller'; ...@@ -3,9 +3,16 @@ import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller'; import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable'; import Disposable from './common/disposable';
import ModelManager from './common/model_manager'; import ModelManager from './common/model_manager';
import editorOptions from './editor_options'; import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
import gitlabTheme from 'ee/ide/lib/themes/gl_theme'; // eslint-disable-line import/first export const clearDomElement = el => {
if (!el || !el.firstChild) return;
while (el.firstChild) {
el.removeChild(el.firstChild);
}
};
export default class Editor { export default class Editor {
static create(monaco) { static create(monaco) {
...@@ -34,19 +41,31 @@ export default class Editor { ...@@ -34,19 +41,31 @@ export default class Editor {
createInstance(domElement) { createInstance(domElement) {
if (!this.instance) { if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.create(domElement, {
...defaultEditorOptions,
})),
(this.dirtyDiffController = new DirtyDiffController(
this.modelManager,
this.decorationsController,
)),
);
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
createDiffInstance(domElement) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add( this.disposable.add(
this.instance = this.monaco.editor.create(domElement, { (this.instance = this.monaco.editor.createDiffEditor(domElement, {
model: null, ...defaultEditorOptions,
readOnly: false, readOnly: true,
contextmenu: true, })),
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
}),
this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController,
),
); );
window.addEventListener('resize', this.debouncedUpdate, false); window.addEventListener('resize', this.debouncedUpdate, false);
...@@ -58,25 +77,39 @@ export default class Editor { ...@@ -58,25 +77,39 @@ export default class Editor {
} }
attachModel(model) { attachModel(model) {
if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
this.instance.setModel({
original: model.getOriginalModel(),
modified: model.getModel(),
});
return;
}
this.instance.setModel(model.getModel()); this.instance.setModel(model.getModel());
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model); if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model; this.currentModel = model;
this.instance.updateOptions(editorOptions.reduce((acc, obj) => { this.instance.updateOptions(
Object.keys(obj).forEach((key) => { editorOptions.reduce((acc, obj) => {
Object.assign(acc, { Object.keys(obj).forEach(key => {
[key]: obj[key](model), Object.assign(acc, {
[key]: obj[key](model),
});
}); });
}); return acc;
return acc; }, {}),
}, {})); );
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
} }
setupMonacoTheme() { setupMonacoTheme() {
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme); this.monaco.editor.defineTheme(
gitlabTheme.themeName,
gitlabTheme.monacoTheme,
);
this.monaco.editor.setTheme('gitlab'); this.monaco.editor.setTheme('gitlab');
} }
...@@ -88,12 +121,21 @@ export default class Editor { ...@@ -88,12 +121,21 @@ export default class Editor {
} }
dispose() { dispose() {
this.disposable.dispose();
window.removeEventListener('resize', this.debouncedUpdate); window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance // catch any potential errors with disposing the error
if (this.instance) { // this is mainly for tests caused by elements not existing
try {
this.disposable.dispose();
this.instance = null; this.instance = null;
} catch (e) {
this.instance = null;
if (process.env.NODE_ENV !== 'test') {
// eslint-disable-next-line no-console
console.error(e);
}
} }
} }
...@@ -113,6 +155,8 @@ export default class Editor { ...@@ -113,6 +155,8 @@ export default class Editor {
} }
onPositionChange(cb) { onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
this.disposable.add( this.disposable.add(
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)), this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
); );
......
export default [{ export const defaultEditorOptions = {
readOnly: model => !!model.file.file_lock, model: null,
}]; readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
};
export default [
{
readOnly: model => !!model.file.file_lock,
},
];
...@@ -6,6 +6,9 @@ export default { ...@@ -6,6 +6,9 @@ export default {
rules: [], rules: [],
colors: { colors: {
'editorLineNumber.foreground': '#CCCCCC', 'editorLineNumber.foreground': '#CCCCCC',
'diffEditor.insertedTextBackground': '#ddfbe6',
'diffEditor.removedTextBackground': '#f9d7dc',
'editor.selectionBackground': '#aad6f8',
}, },
}, },
}; };
...@@ -84,6 +84,14 @@ export const scrollToTab = () => { ...@@ -84,6 +84,14 @@ export const scrollToTab = () => {
}); });
}; };
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
import { normalizeHeaders } from '~/lib/utils/common_utils'; import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash'; import flash from '~/flash';
import eventHub from 'ee/ide/eventhub';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
...@@ -27,6 +28,8 @@ export const closeFile = ({ commit, state, dispatch }, file) => { ...@@ -27,6 +28,8 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
} }
dispatch('getLastCommitData'); dispatch('getLastCommitData');
eventHub.$emit(`editor.update.model.dispose.${file.path}`);
}; };
export const setFileActive = ({ commit, state, getters, dispatch }, file) => { export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
...@@ -150,4 +153,6 @@ export const discardFileChanges = ({ commit }, file) => { ...@@ -150,4 +153,6 @@ export const discardFileChanges = ({ commit }, file) => {
if (file.tempFile && file.opened) { if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, file); commit(types.TOGGLE_FILE_OPEN, file);
} }
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
}; };
...@@ -15,3 +15,5 @@ export const canEditFile = (state) => { ...@@ -15,3 +15,5 @@ export const canEditFile = (state) => {
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile); export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile); export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const hasChanges = state => !!state.changedFiles.length;
...@@ -46,3 +46,6 @@ export const SET_EDIT_MODE = 'SET_EDIT_MODE'; ...@@ -46,3 +46,6 @@ export const SET_EDIT_MODE = 'SET_EDIT_MODE';
export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE'; export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
...@@ -57,6 +57,16 @@ export default { ...@@ -57,6 +57,16 @@ export default {
lastCommitMsg, lastCommitMsg,
}); });
}, },
[types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, {
viewer,
});
},
[types.UPDATE_DELAY_VIEWER_CHANGE](state, delayViewerUpdated) {
Object.assign(state, {
delayViewerUpdated,
});
},
...projectMutations, ...projectMutations,
...fileMutations, ...fileMutations,
...treeMutations, ...treeMutations,
......
...@@ -20,4 +20,6 @@ export default () => ({ ...@@ -20,4 +20,6 @@ export default () => ({
leftPanelCollapsed: false, leftPanelCollapsed: false,
rightPanelCollapsed: false, rightPanelCollapsed: false,
panelResizing: false, panelResizing: false,
viewer: 'editor',
delayViewerUpdated: false,
}); });
...@@ -36,6 +36,7 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -36,6 +36,7 @@ describe('Multi-file editor commit sidebar list item', () => {
it('opens a closed file in the editor when clicking the file path', () => { it('opens a closed file in the editor when clicking the file path', () => {
spyOn(vm, 'openFileInEditor').and.callThrough(); spyOn(vm, 'openFileInEditor').and.callThrough();
spyOn(vm, 'updateViewer');
spyOn(router, 'push'); spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click(); vm.$el.querySelector('.multi-file-commit-list-path').click();
...@@ -44,6 +45,16 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -44,6 +45,16 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(router.push).toHaveBeenCalled(); expect(router.push).toHaveBeenCalled();
}); });
it('calls updateViewer with diff when clicking file', () => {
spyOn(vm, 'openFileInEditor').and.callThrough();
spyOn(vm, 'updateViewer');
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
expect(vm.updateViewer).toHaveBeenCalledWith('diff');
});
describe('computed', () => { describe('computed', () => {
describe('iconName', () => { describe('iconName', () => {
it('returns modified when not a tempFile', () => { it('returns modified when not a tempFile', () => {
......
...@@ -61,6 +61,34 @@ describe('RepoEditor', () => { ...@@ -61,6 +61,34 @@ describe('RepoEditor', () => {
}); });
}); });
describe('createEditorInstance', () => {
it('calls createInstance when viewer is editor', (done) => {
spyOn(vm.editor, 'createInstance');
vm.createEditorInstance();
vm.$nextTick(() => {
expect(vm.editor.createInstance).toHaveBeenCalled();
done();
});
});
it('calls createDiffInstance when viewer is diff', (done) => {
vm.$store.state.viewer = 'diff';
spyOn(vm.editor, 'createDiffInstance');
vm.createEditorInstance();
vm.$nextTick(() => {
expect(vm.editor.createDiffInstance).toHaveBeenCalled();
done();
});
});
});
describe('setupEditor', () => { describe('setupEditor', () => {
it('creates new model', () => { it('creates new model', () => {
spyOn(vm.editor, 'createModel').and.callThrough(); spyOn(vm.editor, 'createModel').and.callThrough();
......
...@@ -7,15 +7,17 @@ describe('RepoTabs', () => { ...@@ -7,15 +7,17 @@ describe('RepoTabs', () => {
const openedFiles = [file('open1'), file('open2')]; const openedFiles = [file('open1'), file('open2')];
let vm; let vm;
function createComponent() { function createComponent(el = null) {
const RepoTabs = Vue.extend(repoTabs); const RepoTabs = Vue.extend(repoTabs);
return new RepoTabs({ return new RepoTabs({
store, store,
}).$mount(); }).$mount(el);
} }
afterEach(() => { afterEach(() => {
vm.$destroy();
resetStore(vm.$store); resetStore(vm.$store);
}); });
...@@ -34,4 +36,44 @@ describe('RepoTabs', () => { ...@@ -34,4 +36,44 @@ describe('RepoTabs', () => {
done(); done();
}); });
}); });
describe('updated', () => {
it('sets showShadow as true when scroll width is larger than width', (done) => {
const el = document.createElement('div');
el.innerHTML = '<div id="test-app"></div>';
document.body.appendChild(el);
const style = document.createElement('style');
style.innerText = `
.multi-file-tabs {
width: 100px;
}
.multi-file-tabs .list-unstyled {
display: flex;
overflow-x: auto;
}
`;
document.head.appendChild(style);
vm = createComponent('#test-app');
openedFiles[0].active = true;
vm.$nextTick()
.then(() => {
expect(vm.showShadow).toBeFalsy();
vm.$store.state.openFiles = openedFiles;
})
.then(vm.$nextTick)
.then(() => {
expect(vm.showShadow).toBeTruthy();
style.remove();
el.remove();
})
.then(done)
.catch(done.fail);
});
});
}); });
/* global monaco */ /* global monaco */
import eventHub from 'ee/ide/eventhub';
import monacoLoader from 'ee/ide/monaco_loader'; import monacoLoader from 'ee/ide/monaco_loader';
import ModelManager from 'ee/ide/lib/common/model_manager'; import ModelManager from 'ee/ide/lib/common/model_manager';
import { file } from '../../helpers'; import { file } from '../../helpers';
...@@ -47,6 +48,15 @@ describe('Multi-file editor library model manager', () => { ...@@ -47,6 +48,15 @@ describe('Multi-file editor library model manager', () => {
expect(instance.models.get).toHaveBeenCalled(); expect(instance.models.get).toHaveBeenCalled();
}); });
it('adds eventHub listener', () => {
const f = file();
spyOn(eventHub, '$on').and.callThrough();
instance.addModel(f);
expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${f.path}`, jasmine.anything());
});
}); });
describe('hasCachedModel', () => { describe('hasCachedModel', () => {
...@@ -69,6 +79,30 @@ describe('Multi-file editor library model manager', () => { ...@@ -69,6 +79,30 @@ describe('Multi-file editor library model manager', () => {
}); });
}); });
describe('removeCachedModel', () => {
let f;
beforeEach(() => {
f = file();
instance.addModel(f);
});
it('clears cached model', () => {
instance.removeCachedModel(f);
expect(instance.models.size).toBe(0);
});
it('removes eventHub listener', () => {
spyOn(eventHub, '$off').and.callThrough();
instance.removeCachedModel(f);
expect(eventHub.$off).toHaveBeenCalledWith(`editor.update.model.dispose.${f.path}`, jasmine.anything());
});
});
describe('dispose', () => { describe('dispose', () => {
it('clears cached models', () => { it('clears cached models', () => {
instance.addModel(file()); instance.addModel(file());
......
/* global monaco */ /* global monaco */
import eventHub from 'ee/ide/eventhub';
import monacoLoader from 'ee/ide/monaco_loader'; import monacoLoader from 'ee/ide/monaco_loader';
import Model from 'ee/ide/lib/common/model'; import Model from 'ee/ide/lib/common/model';
import { file } from '../../helpers'; import { file } from '../../helpers';
...@@ -7,6 +8,8 @@ describe('Multi-file editor library model', () => { ...@@ -7,6 +8,8 @@ describe('Multi-file editor library model', () => {
let model; let model;
beforeEach((done) => { beforeEach((done) => {
spyOn(eventHub, '$on').and.callThrough();
monacoLoader(['vs/editor/editor.main'], () => { monacoLoader(['vs/editor/editor.main'], () => {
model = new Model(monaco, file('path')); model = new Model(monaco, file('path'));
...@@ -23,6 +26,10 @@ describe('Multi-file editor library model', () => { ...@@ -23,6 +26,10 @@ describe('Multi-file editor library model', () => {
expect(model.model).not.toBeNull(); expect(model.model).not.toBeNull();
}); });
it('adds eventHub listener', () => {
expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything());
});
describe('path', () => { describe('path', () => {
it('returns file path', () => { it('returns file path', () => {
expect(model.path).toBe('path'); expect(model.path).toBe('path');
...@@ -88,5 +95,13 @@ describe('Multi-file editor library model', () => { ...@@ -88,5 +95,13 @@ describe('Multi-file editor library model', () => {
expect(model.events.size).toBe(0); expect(model.events.size).toBe(0);
}); });
it('removes eventHub listener', () => {
spyOn(eventHub, '$off').and.callThrough();
model.dispose();
expect(eventHub.$off).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything());
});
}); });
}); });
...@@ -5,8 +5,16 @@ import { file } from '../helpers'; ...@@ -5,8 +5,16 @@ import { file } from '../helpers';
describe('Multi-file editor library', () => { describe('Multi-file editor library', () => {
let instance; let instance;
let el;
let holder;
beforeEach(done => {
el = document.createElement('div');
holder = document.createElement('div');
el.appendChild(holder);
document.body.appendChild(el);
beforeEach((done) => {
monacoLoader(['vs/editor/editor.main'], () => { monacoLoader(['vs/editor/editor.main'], () => {
instance = editor.create(monaco); instance = editor.create(monaco);
...@@ -16,6 +24,8 @@ describe('Multi-file editor library', () => { ...@@ -16,6 +24,8 @@ describe('Multi-file editor library', () => {
afterEach(() => { afterEach(() => {
instance.dispose(); instance.dispose();
el.remove();
}); });
it('creates instance of editor', () => { it('creates instance of editor', () => {
...@@ -27,33 +37,48 @@ describe('Multi-file editor library', () => { ...@@ -27,33 +37,48 @@ describe('Multi-file editor library', () => {
}); });
describe('createInstance', () => { describe('createInstance', () => {
let el;
beforeEach(() => {
el = document.createElement('div');
});
it('creates editor instance', () => { it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'create').and.callThrough(); spyOn(instance.monaco.editor, 'create').and.callThrough();
instance.createInstance(el); instance.createInstance(holder);
expect(instance.monaco.editor.create).toHaveBeenCalled(); expect(instance.monaco.editor.create).toHaveBeenCalled();
}); });
it('creates dirty diff controller', () => { it('creates dirty diff controller', () => {
instance.createInstance(el); instance.createInstance(holder);
expect(instance.dirtyDiffController).not.toBeNull(); expect(instance.dirtyDiffController).not.toBeNull();
}); });
it('creates model manager', () => { it('creates model manager', () => {
instance.createInstance(el); instance.createInstance(holder);
expect(instance.modelManager).not.toBeNull(); expect(instance.modelManager).not.toBeNull();
}); });
}); });
describe('createDiffInstance', () => {
it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough();
instance.createDiffInstance(holder);
expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith(
holder,
{
model: null,
contextmenu: true,
minimap: {
enabled: false,
},
readOnly: true,
scrollBeyondLastLine: false,
},
);
});
});
describe('createModel', () => { describe('createModel', () => {
it('calls model manager addModel', () => { it('calls model manager addModel', () => {
spyOn(instance.modelManager, 'addModel'); spyOn(instance.modelManager, 'addModel');
...@@ -87,12 +112,28 @@ describe('Multi-file editor library', () => { ...@@ -87,12 +112,28 @@ describe('Multi-file editor library', () => {
expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel()); expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
}); });
it('sets original & modified when diff editor', () => {
spyOn(instance.instance, 'getEditorType').and.returnValue(
'vs.editor.IDiffEditor',
);
spyOn(instance.instance, 'setModel');
instance.attachModel(model);
expect(instance.instance.setModel).toHaveBeenCalledWith({
original: model.getOriginalModel(),
modified: model.getModel(),
});
});
it('attaches the model to the dirty diff controller', () => { it('attaches the model to the dirty diff controller', () => {
spyOn(instance.dirtyDiffController, 'attachModel'); spyOn(instance.dirtyDiffController, 'attachModel');
instance.attachModel(model); instance.attachModel(model);
expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model); expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(
model,
);
}); });
it('re-decorates with the dirty diff controller', () => { it('re-decorates with the dirty diff controller', () => {
...@@ -100,7 +141,9 @@ describe('Multi-file editor library', () => { ...@@ -100,7 +141,9 @@ describe('Multi-file editor library', () => {
instance.attachModel(model); instance.attachModel(model);
expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model); expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(
model,
);
}); });
}); });
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import store from 'ee/ide/stores'; import store from 'ee/ide/stores';
import service from 'ee/ide/services'; import service from 'ee/ide/services';
import router from 'ee/ide/ide_router'; import router from 'ee/ide/ide_router';
import eventHub from 'ee/ide/eventhub';
import { file, resetStore } from '../../helpers'; import { file, resetStore } from '../../helpers';
describe('Multi-file store file actions', () => { describe('Multi-file store file actions', () => {
...@@ -457,6 +458,8 @@ describe('Multi-file store file actions', () => { ...@@ -457,6 +458,8 @@ describe('Multi-file store file actions', () => {
let tmpFile; let tmpFile;
beforeEach(() => { beforeEach(() => {
spyOn(eventHub, '$on');
tmpFile = file(); tmpFile = file();
tmpFile.content = 'testing'; tmpFile.content = 'testing';
......
...@@ -210,4 +210,15 @@ describe('Multi-file store actions', () => { ...@@ -210,4 +210,15 @@ describe('Multi-file store actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('updateViewer', () => {
it('updates viewer state', (done) => {
store.dispatch('updateViewer', 'diff')
.then(() => {
expect(store.state.viewer).toBe('diff');
})
.then(done)
.catch(done.fail);
});
});
}); });
...@@ -401,6 +401,8 @@ describe('IDE commit module actions', () => { ...@@ -401,6 +401,8 @@ describe('IDE commit module actions', () => {
}); });
it('redirects to new merge request page', (done) => { it('redirects to new merge request page', (done) => {
spyOn(eventHub, '$on');
store.state.commit.commitAction = '3'; store.state.commit.commitAction = '3';
store.dispatch('commit/commitChanges') store.dispatch('commit/commitChanges')
......
...@@ -106,4 +106,12 @@ describe('Multi-file store mutations', () => { ...@@ -106,4 +106,12 @@ describe('Multi-file store mutations', () => {
expect(localState.rightPanelCollapsed).toBeFalsy(); expect(localState.rightPanelCollapsed).toBeFalsy();
}); });
}); });
describe('UPDATE_VIEWER', () => {
it('sets viewer state', () => {
mutations.UPDATE_VIEWER(localState, 'diff');
expect(localState.viewer).toBe('diff');
});
});
}); });
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