diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-06 12:10:29 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-06 12:10:29 +0000 |
commit | 5564275a0b378298dc6281599cbfe71a937109ff (patch) | |
tree | a468e1e60046356410219c35c23a8a428c5e2c5e /app/assets/javascripts/code_navigation | |
parent | d87918510a866a5fcbbc2f899ad65c6938ebf5f5 (diff) | |
download | gitlab-ce-5564275a0b378298dc6281599cbfe71a937109ff.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/code_navigation')
9 files changed, 268 insertions, 0 deletions
diff --git a/app/assets/javascripts/code_navigation/components/app.vue b/app/assets/javascripts/code_navigation/components/app.vue new file mode 100644 index 00000000000..0e5f1f0485d --- /dev/null +++ b/app/assets/javascripts/code_navigation/components/app.vue @@ -0,0 +1,43 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import Popover from './popover.vue'; + +export default { + components: { + Popover, + }, + computed: { + ...mapState(['currentDefinition', 'currentDefinitionPosition']), + }, + mounted() { + this.blobViewer = document.querySelector('.blob-viewer'); + + this.addGlobalEventListeners(); + this.fetchData(); + }, + beforeDestroy() { + this.removeGlobalEventListeners(); + }, + methods: { + ...mapActions(['fetchData', 'showDefinition']), + addGlobalEventListeners() { + if (this.blobViewer) { + this.blobViewer.addEventListener('click', this.showDefinition); + } + }, + removeGlobalEventListeners() { + if (this.blobViewer) { + this.blobViewer.removeEventListener('click', this.showDefinition); + } + }, + }, +}; +</script> + +<template> + <popover + v-if="currentDefinition" + :position="currentDefinitionPosition" + :data="currentDefinition" + /> +</template> diff --git a/app/assets/javascripts/code_navigation/components/popover.vue b/app/assets/javascripts/code_navigation/components/popover.vue new file mode 100644 index 00000000000..d5bbe430fcd --- /dev/null +++ b/app/assets/javascripts/code_navigation/components/popover.vue @@ -0,0 +1,76 @@ +<script> +import { GlButton } from '@gitlab/ui'; + +export default { + components: { + GlButton, + }, + props: { + position: { + type: Object, + required: true, + }, + data: { + type: Object, + required: true, + }, + }, + data() { + return { + offsetLeft: 0, + }; + }, + computed: { + positionStyles() { + return { + left: `${this.position.x - this.offsetLeft}px`, + top: `${this.position.y + this.position.height}px`, + }; + }, + }, + watch: { + position: { + handler() { + this.$nextTick(() => this.updateOffsetLeft()); + }, + deep: true, + immediate: true, + }, + }, + methods: { + updateOffsetLeft() { + this.offsetLeft = Math.max( + 0, + this.$el.offsetLeft + this.$el.offsetWidth - window.innerWidth + 20, + ); + }, + }, + colorScheme: gon?.user_color_scheme, +}; +</script> + +<template> + <div + :style="positionStyles" + class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show" + > + <div :style="{ left: `${offsetLeft}px` }" class="arrow"></div> + <div v-for="(hover, index) in data.hover" :key="index" class="border-bottom"> + <pre + v-if="hover.language" + ref="code-output" + :class="$options.colorScheme" + class="border-0 bg-transparent m-0 code highlight" + v-html="hover.value" + ></pre> + <p v-else ref="doc-output" class="p-3 m-0"> + {{ hover.value }} + </p> + </div> + <div v-if="data.definition_url" class="popover-body"> + <gl-button :href="data.definition_url" target="_blank" class="w-100" variant="default"> + {{ __('Go to definition') }} + </gl-button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/code_navigation/index.js b/app/assets/javascripts/code_navigation/index.js new file mode 100644 index 00000000000..2222c986dfe --- /dev/null +++ b/app/assets/javascripts/code_navigation/index.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import store from './store'; +import App from './components/app.vue'; + +Vue.use(Vuex); + +export default () => { + const el = document.getElementById('js-code-navigation'); + + store.dispatch('setInitialData', el.dataset); + + return new Vue({ + el, + store, + render(h) { + return h(App); + }, + }); +}; diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js new file mode 100644 index 00000000000..10483abfb23 --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/actions.js @@ -0,0 +1,62 @@ +import api from '~/api'; +import { __ } from '~/locale'; +import createFlash from '~/flash'; +import * as types from './mutation_types'; +import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils'; + +export default { + setInitialData({ commit }, data) { + commit(types.SET_INITIAL_DATA, data); + }, + requestDataError({ commit }) { + commit(types.REQUEST_DATA_ERROR); + createFlash(__('An error occurred loading code navigation')); + }, + fetchData({ commit, dispatch, state }) { + commit(types.REQUEST_DATA); + + api + .lsifData(state.projectPath, state.commitId, state.path) + .then(({ data }) => { + const normalizedData = data.reduce((acc, d) => { + if (d.hover) { + acc[`${d.start_line}:${d.start_char}`] = d; + addInteractionClass(d); + } + return acc; + }, {}); + + commit(types.REQUEST_DATA_SUCCESS, normalizedData); + }) + .catch(() => dispatch('requestDataError')); + }, + showDefinition({ commit, state }, { target: el }) { + let definition; + let position; + + if (!state.data) return; + + const isCurrentElementPopoverOpen = el.classList.contains('hll'); + + if (getCurrentHoverElement()) { + getCurrentHoverElement().classList.remove('hll'); + } + + if (el.classList.contains('js-code-navigation') && !isCurrentElementPopoverOpen) { + const { lineIndex, charIndex } = el.dataset; + + position = { + x: el.offsetLeft, + y: el.offsetTop, + height: el.offsetHeight, + }; + definition = state.data[`${lineIndex}:${charIndex}`]; + + el.classList.add('hll'); + + setCurrentHoverElement(el); + } + + commit(types.SET_CURRENT_DEFINITION, { definition, position }); + }, +}; diff --git a/app/assets/javascripts/code_navigation/store/index.js b/app/assets/javascripts/code_navigation/store/index.js new file mode 100644 index 00000000000..fe48f3ac7f5 --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/index.js @@ -0,0 +1,10 @@ +import Vuex from 'vuex'; +import createState from './state'; +import actions from './actions'; +import mutations from './mutations'; + +export default new Vuex.Store({ + actions, + mutations, + state: createState(), +}); diff --git a/app/assets/javascripts/code_navigation/store/mutation_types.js b/app/assets/javascripts/code_navigation/store/mutation_types.js new file mode 100644 index 00000000000..29a2897a6fd --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/mutation_types.js @@ -0,0 +1,5 @@ +export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; +export const REQUEST_DATA = 'REQUEST_DATA'; +export const REQUEST_DATA_SUCCESS = 'REQUEST_DATA_SUCCESS'; +export const REQUEST_DATA_ERROR = 'REQUEST_DATA_ERROR'; +export const SET_CURRENT_DEFINITION = 'SET_CURRENT_DEFINITION'; diff --git a/app/assets/javascripts/code_navigation/store/mutations.js b/app/assets/javascripts/code_navigation/store/mutations.js new file mode 100644 index 00000000000..bb833a5adbc --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/mutations.js @@ -0,0 +1,23 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_INITIAL_DATA](state, { projectPath, commitId, blobPath }) { + state.projectPath = projectPath; + state.commitId = commitId; + state.blobPath = blobPath; + }, + [types.REQUEST_DATA](state) { + state.loading = true; + }, + [types.REQUEST_DATA_SUCCESS](state, data) { + state.loading = false; + state.data = data; + }, + [types.REQUEST_DATA_ERROR](state) { + state.loading = false; + }, + [types.SET_CURRENT_DEFINITION](state, { definition, position }) { + state.currentDefinition = definition; + state.currentDefinitionPosition = position; + }, +}; diff --git a/app/assets/javascripts/code_navigation/store/state.js b/app/assets/javascripts/code_navigation/store/state.js new file mode 100644 index 00000000000..a7b3b289db4 --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/state.js @@ -0,0 +1,9 @@ +export default () => ({ + projectPath: null, + commitId: null, + blobPath: null, + loading: false, + data: null, + currentDefinition: null, + currentDefinitionPosition: null, +}); diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js new file mode 100644 index 00000000000..2dee0de6501 --- /dev/null +++ b/app/assets/javascripts/code_navigation/utils/index.js @@ -0,0 +1,20 @@ +export const cachedData = new Map(); + +export const getCurrentHoverElement = () => cachedData.get('current'); +export const setCurrentHoverElement = el => cachedData.set('current', el); + +export const addInteractionClass = d => { + let charCount = 0; + const line = document.getElementById(`LC${d.start_line + 1}`); + const el = [...line.childNodes].find(({ textContent }) => { + if (charCount === d.start_char) return true; + charCount += textContent.length; + return false; + }); + + if (el) { + el.setAttribute('data-char-index', d.start_char); + el.setAttribute('data-line-index', d.start_line); + el.classList.add('cursor-pointer', 'code-navigation', 'js-code-navigation'); + } +}; |