{
if (!el || !el.firstChild) return;
@@ -53,6 +55,8 @@ export default class Editor {
)),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
@@ -73,6 +77,8 @@ export default class Editor {
})),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
@@ -189,4 +195,31 @@ export default class Editor {
static renderSideBySide(domElement) {
return domElement.offsetWidth >= 700;
}
+
+ addCommands() {
+ const getKeyCode = key => {
+ const monacoKeyMod = key.indexOf('KEY_') === 0;
+
+ return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
+ };
+
+ keymap.forEach(command => {
+ const keybindings = command.bindings.map(binding => {
+ const keys = binding.split('+');
+
+ // eslint-disable-next-line no-bitwise
+ return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
+ });
+
+ this.instance.addAction({
+ id: command.id,
+ label: command.label,
+ keybindings,
+ run() {
+ store.dispatch(command.action.name, command.action.params);
+ return null;
+ },
+ });
+ });
+ }
}
diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json
new file mode 100644
index 0000000000000000000000000000000000000000..131abfebbed90054f7346ad7b0059b65e6153c9b
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/keymap.json
@@ -0,0 +1,11 @@
+[
+ {
+ "id": "file-finder",
+ "label": "File finder",
+ "bindings": ["CtrlCmd+KEY_P"],
+ "action": {
+ "name": "toggleFileFinder",
+ "params": true
+ }
+ }
+]
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index cecb4d215bab2c4c816dd794d1ab355aee2087d8..7158e94b6c320ddfa613cfaf347a6a743da61a6e 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -137,6 +137,9 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
+export const toggleFileFinder = ({ commit }, fileFindVisible) =>
+ commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 8518d2f6f0690c228b46c5d2d2f66626c6709fdf..535c4d6f9028f540306d55935cf7de67469ffc06 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -42,4 +42,17 @@ export const collapseButtonTooltip = state =>
export const hasMergeRequest = state => !!state.currentMergeRequestId;
+export const allBlobs = state =>
+ Object.keys(state.entries)
+ .reduce((acc, key) => {
+ const entry = state.entries[key];
+
+ if (entry.type === 'blob') {
+ acc.push(entry);
+ }
+
+ return acc;
+ }, [])
+ .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index f5f95b755c8494f1a59bdc66d1936d7c3b83fbc0..c7f08449d03494a4070c311dea362516eb7409c6 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -58,3 +58,5 @@ export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
+
+export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index fbe342f91267e968e9ecbfd9bb10bf3cea242296..2a6c136aeeda3251bcb24549f8800cfdeb395d9f 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -100,6 +100,11 @@ export default {
delayViewerUpdated,
});
},
+ [types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
+ Object.assign(state, {
+ fileFindVisible,
+ });
+ },
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
const changedFile = state.changedFiles.find(f => f.path === file.path);
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index dd7dcba8ac70b31a3f0e5fe8762431790fcbab99..c3041c77199ee9a7e03b180e10edd1b222a48066 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -4,6 +4,7 @@ export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], {
active,
+ lastOpenedAt: new Date().getTime(),
});
if (active && !state.entries[path].pending) {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 34975ac3144dc08bd4252aa493a6a03773d5bbdd..3470bb9aec0ef8d32f7c00eb4c40039352970e7a 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -18,4 +18,5 @@ export default () => ({
entries: {},
viewer: 'editor',
delayViewerUpdated: false,
+ fileFindVisible: false,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 8a222da14c085786f40e77106ea5b550f5017beb..86612d845e03864f4c19b5f89b0468db4bf6514d 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -42,6 +42,7 @@ export const dataStructure = () => ({
viewMode: 'edit',
previewMode: null,
size: 0,
+ lastOpenedAt: 0,
});
export const decorateData = entity => {
diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js
new file mode 100644
index 0000000000000000000000000000000000000000..5e0f9b612a2aa248d02aa9b5f118b0452e207d74
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/keycodes.js
@@ -0,0 +1,4 @@
+export const UP_KEY_CODE = 38;
+export const DOWN_KEY_CODE = 40;
+export const ENTER_KEY_CODE = 13;
+export const ESC_KEY_CODE = 27;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index d0dda50a8353a3aed861c8641f3356ba53dc12db..e058a0b35b72361bf8bfa901bb98132628d047d4 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -472,6 +472,7 @@ img.emoji {
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
+.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cc5fac6816d976eb58d4b7ccf504dc49eae0572a..664aade7375b1883b72c010a89178ba6b7994283 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -43,7 +43,7 @@
border-color: $gray-darkest;
}
- [data-toggle="dropdown"] {
+ [data-toggle='dropdown'] {
outline: 0;
}
}
@@ -172,7 +172,11 @@
color: $brand-danger;
}
- &:hover,
+ &.disable-hover {
+ text-decoration: none;
+ }
+
+ &:not(.disable-hover):hover,
&:active,
&:focus,
&.is-focused {
@@ -508,17 +512,16 @@
}
&.is-indeterminate::before {
- content: "\f068";
+ content: '\f068';
}
&.is-active::before {
- content: "\f00c";
+ content: '\f00c';
}
}
}
}
-
.dropdown-title {
position: relative;
padding: 2px 25px 10px;
@@ -724,7 +727,6 @@
}
}
-
.dropdown-menu-due-date {
.dropdown-content {
max-height: 230px;
@@ -854,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
.projects-list-frequent-container,
- .projects-list-search-container, {
+ .projects-list-search-container {
padding: 8px 0;
overflow-y: auto;
+
+ li.section-empty.section-failure {
+ color: $callout-danger-color;
+ }
}
.section-header,
@@ -867,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
font-size: $gl-font-size;
}
- .projects-list-frequent-container,
- .projects-list-search-container {
- li.section-empty.section-failure {
- color: $callout-danger-color;
- }
- }
-
.search-input-container {
position: relative;
padding: 4px $gl-padding;
@@ -905,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
.projects-list-item-container {
- .project-item-avatar-container
- .project-item-metadata-container {
+ .project-item-avatar-container .project-item-metadata-container {
float: left;
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 450ef7d6b7e123a4c9c2dd1669d8f0e903f466bf..6d77260b658eed6f0db8974c08cbd59fa09f65df 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -17,6 +17,7 @@
}
.ide-view {
+ position: relative;
display: flex;
height: calc(100vh - #{$header-height});
margin-top: 0;
@@ -876,6 +877,26 @@
font-weight: $gl-font-weight-bold;
}
+.ide-file-finder-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 100;
+}
+
+.ide-file-finder {
+ top: 10px;
+ left: 50%;
+ transform: translateX(-50%);
+
+ .highlighted {
+ color: $blue-500;
+ font-weight: $gl-font-weight-bold;
+ }
+}
+
.ide-commit-message-field {
height: 200px;
background-color: $white-light;
diff --git a/changelogs/unreleased/ide-file-finder.yml b/changelogs/unreleased/ide-file-finder.yml
new file mode 100644
index 0000000000000000000000000000000000000000..252dd3a30c49bbf3d1fb16f5d81affda5a4cf3f0
--- /dev/null
+++ b/changelogs/unreleased/ide-file-finder.yml
@@ -0,0 +1,5 @@
+---
+title: Added fuzzy file finder to web IDE
+merge_request:
+author:
+type: added
diff --git a/package.json b/package.json
index 45bea12fd9bcfd9daef5f16d9a9479fab2e5c750..6f92600cfa51eab181b4895dd7f6379bbd1c8b11 100644
--- a/package.json
+++ b/package.json
@@ -88,6 +88,7 @@
"vue-resource": "^1.3.5",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.13",
+ "vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1",
"webpack": "^3.11.0",
"webpack-bundle-analyzer": "^2.10.0",
diff --git a/spec/javascripts/ide/components/file_finder/index_spec.js b/spec/javascripts/ide/components/file_finder/index_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..4f208e946d20bc4121d56c2a55c8f7a5b4b73230
--- /dev/null
+++ b/spec/javascripts/ide/components/file_finder/index_spec.js
@@ -0,0 +1,308 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import FindFileComponent from '~/ide/components/file_finder/index.vue';
+import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../../helpers';
+import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE File finder item spec', () => {
+ const Component = Vue.extend(FindFileComponent);
+ let vm;
+
+ beforeEach(done => {
+ setFixtures('
');
+
+ vm = mountComponentWithStore(Component, {
+ store,
+ el: '#app',
+ props: {
+ index: 0,
+ },
+ });
+
+ setTimeout(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('with entries', () => {
+ beforeEach(done => {
+ Vue.set(vm.$store.state.entries, 'folder', {
+ ...file('folder'),
+ path: 'folder',
+ type: 'folder',
+ });
+
+ Vue.set(vm.$store.state.entries, 'index.js', {
+ ...file('index.js'),
+ path: 'index.js',
+ type: 'blob',
+ url: '/index.jsurl',
+ });
+
+ Vue.set(vm.$store.state.entries, 'component.js', {
+ ...file('component.js'),
+ path: 'component.js',
+ type: 'blob',
+ });
+
+ setTimeout(done);
+ });
+
+ it('renders list of blobs', () => {
+ expect(vm.$el.textContent).toContain('index.js');
+ expect(vm.$el.textContent).toContain('component.js');
+ expect(vm.$el.textContent).not.toContain('folder');
+ });
+
+ it('filters entries', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('index.js');
+ expect(vm.$el.textContent).not.toContain('component.js');
+
+ done();
+ });
+ });
+
+ it('shows clear button when searchText is not empty', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.dropdown-input-clear').classList).toContain('show');
+ expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
+
+ done();
+ });
+ });
+
+ it('clear button resets searchText', done => {
+ vm.searchText = 'index';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.dropdown-input-clear').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.searchText).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('clear button focues search input', done => {
+ spyOn(vm.$refs.searchInput, 'focus');
+ vm.searchText = 'index';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.dropdown-input-clear').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('listShowCount', () => {
+ it('returns 1 when no filtered entries exist', done => {
+ vm.searchText = 'testing 123';
+
+ vm.$nextTick(() => {
+ expect(vm.listShowCount).toBe(1);
+
+ done();
+ });
+ });
+
+ it('returns entries length when not filtered', () => {
+ expect(vm.listShowCount).toBe(2);
+ });
+ });
+
+ describe('listHeight', () => {
+ it('returns 55 when entries exist', () => {
+ expect(vm.listHeight).toBe(55);
+ });
+
+ it('returns 33 when entries dont exist', done => {
+ vm.searchText = 'testing 123';
+
+ vm.$nextTick(() => {
+ expect(vm.listHeight).toBe(33);
+
+ done();
+ });
+ });
+ });
+
+ describe('filteredBlobsLength', () => {
+ it('returns length of filtered blobs', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.filteredBlobsLength).toBe(1);
+
+ done();
+ });
+ });
+ });
+
+ describe('watches', () => {
+ describe('searchText', () => {
+ it('resets focusedIndex when updated', done => {
+ vm.focusedIndex = 1;
+ vm.searchText = 'test';
+
+ vm.$nextTick(() => {
+ expect(vm.focusedIndex).toBe(0);
+
+ done();
+ });
+ });
+ });
+
+ describe('fileFindVisible', () => {
+ it('returns searchText when false', done => {
+ vm.searchText = 'test';
+ vm.$store.state.fileFindVisible = true;
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$store.state.fileFindVisible = false;
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.searchText).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('openFile', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ spyOn(vm, 'toggleFileFinder');
+ });
+
+ it('closes file finder', () => {
+ vm.openFile(vm.$store.state.entries['index.js']);
+
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ });
+
+ it('pushes to router', () => {
+ vm.openFile(vm.$store.state.entries['index.js']);
+
+ expect(router.push).toHaveBeenCalledWith('/project/index.jsurl');
+ });
+ });
+
+ describe('onKeyup', () => {
+ it('opens file on enter key', done => {
+ const event = new CustomEvent('keyup');
+ event.keyCode = ENTER_KEY_CODE;
+
+ spyOn(vm, 'openFile');
+
+ vm.$refs.searchInput.dispatchEvent(event);
+
+ vm.$nextTick(() => {
+ expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']);
+
+ done();
+ });
+ });
+
+ it('closes file finder on esc key', done => {
+ const event = new CustomEvent('keyup');
+ event.keyCode = ESC_KEY_CODE;
+
+ spyOn(vm, 'toggleFileFinder');
+
+ vm.$refs.searchInput.dispatchEvent(event);
+
+ vm.$nextTick(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('onKeyDown', () => {
+ let el;
+
+ beforeEach(() => {
+ el = vm.$refs.searchInput;
+ });
+
+ describe('up key', () => {
+ const event = new CustomEvent('keydown');
+ event.keyCode = UP_KEY_CODE;
+
+ it('resets to last index when at top', () => {
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(1);
+ });
+
+ it('minus 1 from focusedIndex', () => {
+ vm.focusedIndex = 1;
+
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(0);
+ });
+ });
+
+ describe('down key', () => {
+ const event = new CustomEvent('keydown');
+ event.keyCode = DOWN_KEY_CODE;
+
+ it('resets to first index when at bottom', () => {
+ vm.focusedIndex = 1;
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(0);
+ });
+
+ it('adds 1 to focusedIndex', () => {
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(1);
+ });
+ });
+ });
+ });
+
+ describe('without entries', () => {
+ it('renders loading text when loading', done => {
+ store.state.loading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('Loading...');
+
+ done();
+ });
+ });
+
+ it('renders no files text', () => {
+ expect(vm.$el.textContent).toContain('No files found.');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/file_finder/item_spec.js b/spec/javascripts/ide/components/file_finder/item_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0f1116c6912c5127e7ef7bc8feb93802c9154d45
--- /dev/null
+++ b/spec/javascripts/ide/components/file_finder/item_spec.js
@@ -0,0 +1,140 @@
+import Vue from 'vue';
+import ItemComponent from '~/ide/components/file_finder/item.vue';
+import { file } from '../../helpers';
+import createComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE File finder item spec', () => {
+ const Component = Vue.extend(ItemComponent);
+ let vm;
+ let localFile;
+
+ beforeEach(() => {
+ localFile = {
+ ...file(),
+ name: 'test file',
+ path: 'test/file',
+ };
+
+ vm = createComponent(Component, {
+ file: localFile,
+ focused: true,
+ searchText: '',
+ index: 0,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file name & path', () => {
+ expect(vm.$el.textContent).toContain('test file');
+ expect(vm.$el.textContent).toContain('test/file');
+ });
+
+ describe('focused', () => {
+ it('adds is-focused class', () => {
+ expect(vm.$el.classList).toContain('is-focused');
+ });
+
+ it('does not have is-focused class when not focused', done => {
+ vm.focused = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).not.toContain('is-focused');
+
+ done();
+ });
+ });
+ });
+
+ describe('changed file icon', () => {
+ it('does not render when not a changed or temp file', () => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
+ });
+
+ it('renders when a changed file', done => {
+ vm.file.changed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('renders when a temp file', done => {
+ vm.file.tempFile = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ it('emits event when clicked', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click', vm.file);
+ });
+
+ describe('path', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-path');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('adds ellipsis to long text', done => {
+ vm.file.path = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
+ done();
+ });
+ });
+ });
+
+ describe('name', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-name');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('does not add ellipsis to long text', done => {
+ vm.file.name = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
index 5bd890094ccea6787051eed0ba42a32ecda468c8..7bfcfc905727b6196b993432fd9aa35fd1dabca5 100644
--- a/spec/javascripts/ide/components/ide_spec.js
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import Mousetrap from 'mousetrap';
import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
@@ -38,4 +39,68 @@ describe('ide component', () => {
done();
});
});
+
+ describe('file finder', () => {
+ beforeEach(done => {
+ spyOn(vm, 'toggleFileFinder');
+
+ vm.$store.state.fileFindVisible = true;
+
+ vm.$nextTick(done);
+ });
+
+ it('calls toggleFileFinder on `t` key press', done => {
+ Mousetrap.trigger('t');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls toggleFileFinder on `command+p` key press', done => {
+ Mousetrap.trigger('command+p');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls toggleFileFinder on `ctrl+p` key press', done => {
+ Mousetrap.trigger('ctrl+p');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('always allows `command+p` to trigger toggleFileFinder', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'),
+ ).toBe(false);
+ });
+
+ it('always allows `ctrl+p` to trigger toggleFileFinder', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'),
+ ).toBe(false);
+ });
+
+ it('onlys handles `t` when focused in input-field', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
+ ).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 22a7441ba92eb388d9dbf40cff16c6c296d66093..f848f13d4291a267f731684631f8a458ade659b7 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -340,4 +340,17 @@ describe('Multi-file store actions', () => {
.catch(done.fail);
});
});
+
+ describe('toggleFileFinder', () => {
+ it('commits TOGGLE_FILE_FINDER', done => {
+ testAction(
+ actions.toggleFileFinder,
+ true,
+ null,
+ [{ type: 'TOGGLE_FILE_FINDER', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index 8d04b83928ca7a30475637784c40f7d539ae06ae..b6b4dd2872983c71b742cbc4b53a9fdce8cdc7dd 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -64,4 +64,24 @@ describe('IDE store getters', () => {
expect(getters.currentMergeRequest(localState)).toBeNull();
});
});
+
+ describe('allBlobs', () => {
+ beforeEach(() => {
+ Object.assign(localState.entries, {
+ index: { type: 'blob', name: 'index', lastOpenedAt: 0 },
+ app: { type: 'blob', name: 'blob', lastOpenedAt: 0 },
+ folder: { type: 'folder', name: 'folder', lastOpenedAt: 0 },
+ });
+ });
+
+ it('returns only blobs', () => {
+ expect(getters.allBlobs(localState).length).toBe(2);
+ });
+
+ it('returns list sorted by lastOpenedAt', () => {
+ localState.entries.app.lastOpenedAt = new Date().getTime();
+
+ expect(getters.allBlobs(localState)[0].name).toBe('blob');
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 26e7ed4535e08204e5f11664b0d6b742aa230454..575039e755e44c227501ae36fb6792903079b88e 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -86,4 +86,12 @@ describe('Multi-file store mutations', () => {
expect(localState.viewer).toBe('diff');
});
});
+
+ describe('TOGGLE_FILE_FINDER', () => {
+ it('updates fileFindVisible', () => {
+ mutations.TOGGLE_FILE_FINDER(localState, true);
+
+ expect(localState.fileFindVisible).toBe(true);
+ });
+ });
});
diff --git a/yarn.lock b/yarn.lock
index f05278cfde5c8ae9dcd922a7438cdd70f1b2d509..aa560556dccc3e64fabd68de19db7a35dba593c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8474,6 +8474,10 @@ vue-template-es2015-compiler@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
+vue-virtual-scroll-list@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.2.5.tgz#bcbd010f7cdb035eba8958ebf807c6214d9a167a"
+
vue@^2.5.13:
version "2.5.13"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.13.tgz#95bd31e20efcf7a7f39239c9aa6787ce8cf578e1"