diff options
author | Clement Ho <ClemMakesApps@gmail.com> | 2018-04-25 10:51:53 -0500 |
---|---|---|
committer | Clement Ho <ClemMakesApps@gmail.com> | 2018-04-25 10:51:53 -0500 |
commit | 56982fcf24ba4290dcca6b8c4e852d716b179748 (patch) | |
tree | eb4c8883d41b21212468a6f8f07990b61d7181d8 /app/assets/javascripts/ide | |
parent | 80766821b24cc13a6ce054f5b0c996bebccc4121 (diff) | |
parent | c5f6c811eec6bca7952f84af9e30f3fafb848352 (diff) | |
download | gitlab-ce-56982fcf24ba4290dcca6b8c4e852d716b179748.tar.gz |
Merge branch 'master' into bootstrap4
Diffstat (limited to 'app/assets/javascripts/ide')
-rw-r--r-- | app/assets/javascripts/ide/components/file_finder/index.vue | 245 | ||||
-rw-r--r-- | app/assets/javascripts/ide/components/file_finder/item.vue | 113 | ||||
-rw-r--r-- | app/assets/javascripts/ide/components/ide.vue | 114 | ||||
-rw-r--r-- | app/assets/javascripts/ide/constants.js | 5 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/editor.js | 33 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/keymap.json | 11 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/actions.js | 6 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/getters.js | 16 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/modules/commit/actions.js | 3 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/modules/commit/getters.js | 3 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/mutation_types.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/mutations.js | 5 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/mutations/file.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/state.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/ide/stores/utils.js | 1 |
15 files changed, 520 insertions, 39 deletions
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue new file mode 100644 index 00000000000..ea2b13a8b21 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -0,0 +1,245 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import VirtualList from 'vue-virtual-scroll-list'; +import Item from './item.vue'; +import router from '../../ide_router'; +import { + MAX_FILE_FINDER_RESULTS, + FILE_FINDER_ROW_HEIGHT, + FILE_FINDER_EMPTY_ROW_HEIGHT, +} from '../../constants'; +import { + UP_KEY_CODE, + DOWN_KEY_CODE, + ENTER_KEY_CODE, + ESC_KEY_CODE, +} from '../../../lib/utils/keycodes'; + +export default { + components: { + Item, + VirtualList, + }, + data() { + return { + focusedIndex: 0, + searchText: '', + mouseOver: false, + cancelMouseOver: false, + }; + }, + computed: { + ...mapGetters(['allBlobs']), + ...mapState(['fileFindVisible', 'loading']), + filteredBlobs() { + const searchText = this.searchText.trim(); + + if (searchText === '') { + return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS); + } + + return fuzzaldrinPlus + .filter(this.allBlobs, searchText, { + key: 'path', + maxResults: MAX_FILE_FINDER_RESULTS, + }) + .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); + }, + filteredBlobsLength() { + return this.filteredBlobs.length; + }, + listShowCount() { + return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1; + }, + listHeight() { + return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT; + }, + showClearInputButton() { + return this.searchText.trim() !== ''; + }, + }, + watch: { + fileFindVisible() { + this.$nextTick(() => { + if (!this.fileFindVisible) { + this.searchText = ''; + } else { + this.focusedIndex = 0; + + if (this.$refs.searchInput) { + this.$refs.searchInput.focus(); + } + } + }); + }, + searchText() { + this.focusedIndex = 0; + }, + focusedIndex() { + if (!this.mouseOver) { + this.$nextTick(() => { + const el = this.$refs.virtualScrollList.$el; + const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT; + const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT; + + if (this.focusedIndex === 0) { + // if index is the first index, scroll straight to start + el.scrollTop = 0; + } else if (this.focusedIndex === this.filteredBlobsLength - 1) { + // if index is the last index, scroll to the end + el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT; + } else if (scrollTop >= bottom + el.scrollTop) { + // if element is off the bottom of the scroll list, scroll down one item + el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT; + } else if (scrollTop < el.scrollTop) { + // if element is off the top of the scroll list, scroll up one item + el.scrollTop = scrollTop; + } + }); + } + }, + }, + methods: { + ...mapActions(['toggleFileFinder']), + clearSearchInput() { + this.searchText = ''; + + this.$nextTick(() => { + this.$refs.searchInput.focus(); + }); + }, + onKeydown(e) { + switch (e.keyCode) { + case UP_KEY_CODE: + e.preventDefault(); + this.mouseOver = false; + this.cancelMouseOver = true; + if (this.focusedIndex > 0) { + this.focusedIndex -= 1; + } else { + this.focusedIndex = this.filteredBlobsLength - 1; + } + break; + case DOWN_KEY_CODE: + e.preventDefault(); + this.mouseOver = false; + this.cancelMouseOver = true; + if (this.focusedIndex < this.filteredBlobsLength - 1) { + this.focusedIndex += 1; + } else { + this.focusedIndex = 0; + } + break; + default: + break; + } + }, + onKeyup(e) { + switch (e.keyCode) { + case ENTER_KEY_CODE: + this.openFile(this.filteredBlobs[this.focusedIndex]); + break; + case ESC_KEY_CODE: + this.toggleFileFinder(false); + break; + default: + break; + } + }, + openFile(file) { + this.toggleFileFinder(false); + router.push(`/project${file.url}`); + }, + onMouseOver(index) { + if (!this.cancelMouseOver) { + this.mouseOver = true; + this.focusedIndex = index; + } + }, + onMouseMove(index) { + this.cancelMouseOver = false; + this.onMouseOver(index); + }, + }, +}; +</script> + +<template> + <div + class="ide-file-finder-overlay" + @mousedown.self="toggleFileFinder(false)" + > + <div + class="dropdown-menu diff-file-changes ide-file-finder show" + > + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + :placeholder="__('Search files')" + autocomplete="off" + v-model="searchText" + ref="searchInput" + @keydown="onKeydown($event)" + @keyup="onKeyup($event)" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + :class="{ + hidden: showClearInputButton + }" + ></i> + <i + role="button" + :aria-label="__('Clear search input')" + class="fa fa-times dropdown-input-clear" + :class="{ + show: showClearInputButton + }" + @click="clearSearchInput" + ></i> + </div> + <div> + <virtual-list + :size="listHeight" + :remain="listShowCount" + wtag="ul" + ref="virtualScrollList" + > + <template v-if="filteredBlobsLength"> + <li + v-for="(file, index) in filteredBlobs" + :key="file.key" + > + <item + class="disable-hover" + :file="file" + :search-text="searchText" + :focused="index === focusedIndex" + :index="index" + @click="openFile" + @mouseover="onMouseOver" + @mousemove="onMouseMove" + /> + </li> + </template> + <li + v-else + class="dropdown-menu-empty-item" + > + <div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8"> + <template v-if="loading"> + {{ __('Loading...') }} + </template> + <template v-else> + {{ __('No files found.') }} + </template> + </div> + </li> + </virtual-list> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue new file mode 100644 index 00000000000..d4427420207 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_finder/item.vue @@ -0,0 +1,113 @@ +<script> +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import FileIcon from '../../../vue_shared/components/file_icon.vue'; +import ChangedFileIcon from '../changed_file_icon.vue'; + +const MAX_PATH_LENGTH = 60; + +export default { + components: { + ChangedFileIcon, + FileIcon, + }, + props: { + file: { + type: Object, + required: true, + }, + focused: { + type: Boolean, + required: true, + }, + searchText: { + type: String, + required: true, + }, + index: { + type: Number, + required: true, + }, + }, + computed: { + pathWithEllipsis() { + const path = this.file.path; + + return path.length < MAX_PATH_LENGTH + ? path + : `...${path.substr(path.length - MAX_PATH_LENGTH)}`; + }, + nameSearchTextOccurences() { + return fuzzaldrinPlus.match(this.file.name, this.searchText); + }, + pathSearchTextOccurences() { + return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText); + }, + }, + methods: { + clickRow() { + this.$emit('click', this.file); + }, + mouseOverRow() { + this.$emit('mouseover', this.index); + }, + mouseMove() { + this.$emit('mousemove', this.index); + }, + }, +}; +</script> + +<template> + <button + type="button" + class="diff-changed-file" + :class="{ + 'is-focused': focused, + }" + @click.prevent="clickRow" + @mouseover="mouseOverRow" + @mousemove="mouseMove" + > + <file-icon + :file-name="file.name" + :size="16" + css-classes="diff-file-changed-icon append-right-8" + /> + <span class="diff-changed-file-content append-right-8"> + <strong + class="diff-changed-file-name" + > + <span + v-for="(char, index) in file.name.split('')" + :key="index + char" + :class="{ + highlighted: nameSearchTextOccurences.indexOf(index) >= 0, + }" + v-text="char" + > + </span> + </strong> + <span + class="diff-changed-file-path prepend-top-5" + > + <span + v-for="(char, index) in pathWithEllipsis.split('')" + :key="index + char" + :class="{ + highlighted: pathSearchTextOccurences.indexOf(index) >= 0, + }" + v-text="char" + > + </span> + </span> + </span> + <span + v-if="file.changed || file.tempFile" + class="diff-changed-stats" + > + <changed-file-icon + :file="file" + /> + </span> + </button> +</template> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 5e96d3976ea..c63c130500e 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,55 +1,91 @@ <script> -import { mapState, mapGetters } from 'vuex'; -import ideSidebar from './ide_side_bar.vue'; -import ideContextbar from './ide_context_bar.vue'; -import repoTabs from './repo_tabs.vue'; -import ideStatusBar from './ide_status_bar.vue'; -import repoEditor from './repo_editor.vue'; + import { mapActions, mapState, mapGetters } from 'vuex'; + import Mousetrap from 'mousetrap'; + import ideSidebar from './ide_side_bar.vue'; + import ideContextbar from './ide_context_bar.vue'; + import repoTabs from './repo_tabs.vue'; + import ideStatusBar from './ide_status_bar.vue'; + import repoEditor from './repo_editor.vue'; + import FindFile from './file_finder/index.vue'; -export default { - components: { - ideSidebar, - ideContextbar, - repoTabs, - ideStatusBar, - repoEditor, - }, - props: { - emptyStateSvgPath: { - type: String, - required: true, + const originalStopCallback = Mousetrap.stopCallback; + + export default { + components: { + ideSidebar, + ideContextbar, + repoTabs, + ideStatusBar, + repoEditor, + FindFile, }, - noChangesStateSvgPath: { - type: String, - required: true, + props: { + emptyStateSvgPath: { + type: String, + required: true, + }, + noChangesStateSvgPath: { + type: String, + required: true, + }, + committedStateSvgPath: { + type: String, + required: true, + }, }, - committedStateSvgPath: { - type: String, - required: true, + computed: { + ...mapState([ + 'changedFiles', + 'openFiles', + 'viewer', + 'currentMergeRequestId', + 'fileFindVisible', + ]), + ...mapGetters(['activeFile', 'hasChanges']), }, - }, - computed: { - ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']), - ...mapGetters(['activeFile', 'hasChanges']), - }, - mounted() { - const returnValue = 'Are you sure you want to lose unsaved changes?'; - window.onbeforeunload = e => { - if (!this.changedFiles.length) return undefined; + mounted() { + const returnValue = 'Are you sure you want to lose unsaved changes?'; + window.onbeforeunload = e => { + if (!this.changedFiles.length) return undefined; + + Object.assign(e, { + returnValue, + }); + return returnValue; + }; - Object.assign(e, { - returnValue, + Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => { + if (e.preventDefault) { + e.preventDefault(); + } + + this.toggleFileFinder(!this.fileFindVisible); }); - return returnValue; - }; - }, -}; + + Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo); + }, + methods: { + ...mapActions(['toggleFileFinder']), + mousetrapStopCallback(e, el, combo) { + if (combo === 't' && el.classList.contains('dropdown-input-field')) { + return true; + } else if (combo === 'command+p' || combo === 'ctrl+p') { + return false; + } + + return originalStopCallback(e, el, combo); + }, + }, + }; </script> <template> <div class="ide-view" > + <find-file + v-show="fileFindVisible" + /> <ide-sidebar /> <div class="multi-file-edit-pane" diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index b60d042e0be..b06da9f95d1 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -1,3 +1,8 @@ // Fuzzy file finder +export const MAX_FILE_FINDER_RESULTS = 40; +export const FILE_FINDER_ROW_HEIGHT = 55; +export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; + +// Commit message textarea export const MAX_TITLE_LENGTH = 50; export const MAX_BODY_LENGTH = 72; diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 2d3ee7d4f48..b65d9c68a0b 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,10 +1,12 @@ import _ from 'underscore'; +import store from '../stores'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; import ModelManager from './common/model_manager'; import editorOptions, { defaultEditorOptions } from './editor_options'; import gitlabTheme from './themes/gl_theme'; +import keymap from './keymap.json'; export const clearDomElement = el => { 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 00000000000..131abfebbed --- /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 cecb4d215ba..cbe43f5f7f2 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -137,7 +137,13 @@ 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'; export * from './actions/merge_request'; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 8518d2f6f06..ec1ea155aee 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -42,4 +42,20 @@ 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); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index b26512e213a..119debaf5f3 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -185,3 +185,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = commit(types.UPDATE_LOADING, false); }); }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 9c3905a0b0d..d01060201f2 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -27,3 +27,6 @@ export const branchName = (state, getters, rootState) => { return rootState.currentBranchId; }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index f5f95b755c8..c7f08449d03 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 fbe342f9126..2a6c136aeed 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 dd7dcba8ac7..c3041c77199 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 34975ac3144..3470bb9aec0 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 8a222da14c0..86612d845e0 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 => { |