summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/code_navigation
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-06 12:10:29 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-06 12:10:29 +0000
commit5564275a0b378298dc6281599cbfe71a937109ff (patch)
treea468e1e60046356410219c35c23a8a428c5e2c5e /app/assets/javascripts/code_navigation
parentd87918510a866a5fcbbc2f899ad65c6938ebf5f5 (diff)
downloadgitlab-ce-5564275a0b378298dc6281599cbfe71a937109ff.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/code_navigation')
-rw-r--r--app/assets/javascripts/code_navigation/components/app.vue43
-rw-r--r--app/assets/javascripts/code_navigation/components/popover.vue76
-rw-r--r--app/assets/javascripts/code_navigation/index.js20
-rw-r--r--app/assets/javascripts/code_navigation/store/actions.js62
-rw-r--r--app/assets/javascripts/code_navigation/store/index.js10
-rw-r--r--app/assets/javascripts/code_navigation/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/code_navigation/store/mutations.js23
-rw-r--r--app/assets/javascripts/code_navigation/store/state.js9
-rw-r--r--app/assets/javascripts/code_navigation/utils/index.js20
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');
+ }
+};