summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/ide
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/ide')
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue18
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue25
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue12
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue15
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue19
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue6
-rw-r--r--app/assets/javascripts/ide/components/file_templates/bar.vue6
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue5
-rw-r--r--app/assets/javascripts/ide/components/ide.vue30
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue30
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue18
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue11
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue32
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue28
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue9
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue7
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue7
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue1
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue44
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue19
-rw-r--r--app/assets/javascripts/ide/constants.js6
-rw-r--r--app/assets/javascripts/ide/index.js12
-rw-r--r--app/assets/javascripts/ide/lib/editor.js6
-rw-r--r--app/assets/javascripts/ide/lib/errors.js38
-rw-r--r--app/assets/javascripts/ide/lib/languages/README.md4
-rw-r--r--app/assets/javascripts/ide/lib/languages/hcl.js192
-rw-r--r--app/assets/javascripts/ide/lib/languages/index.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/getters.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js18
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutations.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js15
-rw-r--r--app/assets/javascripts/ide/stores/utils.js23
-rw-r--r--app/assets/javascripts/ide/utils.js54
35 files changed, 542 insertions, 189 deletions
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 183816921c1..644808cb83a 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -1,8 +1,6 @@
<script>
-import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
-import { GlIcon } from '@gitlab/ui';
-import tooltip from '~/vue_shared/directives/tooltip';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { leftSidebarViews } from '../constants';
export default {
@@ -10,7 +8,7 @@ export default {
GlIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
computed: {
...mapState(['currentActivityView']),
@@ -22,9 +20,7 @@ export default {
this.updateActivityBarView(view);
- // TODO: We must use JQuery here to interact with the Bootstrap tooltip API
- // https://gitlab.com/gitlab-org/gitlab/-/issues/217577
- $(e.currentTarget).tooltip('hide');
+ this.$root.$emit('bv::hide::tooltip');
},
},
leftSidebarViews,
@@ -32,11 +28,11 @@ export default {
</script>
<template>
- <nav class="ide-activity-bar">
+ <nav class="ide-activity-bar" data-testid="left-sidebar">
<ul class="list-unstyled">
<li>
<button
- v-tooltip
+ v-gl-tooltip.right.viewport
:class="{
active: currentActivityView === $options.leftSidebarViews.edit.name,
}"
@@ -54,7 +50,7 @@ export default {
</li>
<li>
<button
- v-tooltip
+ v-gl-tooltip.right.viewport
:class="{
active: currentActivityView === $options.leftSidebarViews.review.name,
}"
@@ -71,7 +67,7 @@ export default {
</li>
<li>
<button
- v-tooltip
+ v-gl-tooltip.right.viewport
:class="{
active: currentActivityView === $options.leftSidebarViews.commit.name,
}"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index de4b0a34002..b89329c92ec 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,8 +1,8 @@
<script>
-/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
-import { sprintf, s__ } from '~/locale';
+import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
import consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
import NewMergeRequestOption from './new_merge_request_option.vue';
@@ -13,6 +13,7 @@ const { mapState: mapCommitState, mapActions: mapCommitActions } = createNamespa
export default {
components: {
+ GlSprintf,
RadioGroup,
NewMergeRequestOption,
},
@@ -20,12 +21,8 @@ export default {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
...mapCommitState(['commitAction']),
...mapGetters(['currentBranch', 'emptyRepo', 'canPushToBranch']),
- commitToCurrentBranchText() {
- return sprintf(
- s__('IDE|Commit to %{branchName} branch'),
- { branchName: `<strong class="monospace">${escape(this.currentBranchId)}</strong>` },
- false,
- );
+ currentBranchText() {
+ return escape(this.currentBranchId);
},
containsStagedChanges() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
@@ -77,11 +74,13 @@ export default {
:disabled="!canPushToBranch"
:title="$options.currentBranchPermissionsTooltip"
>
- <span
- class="ide-option-label"
- data-qa-selector="commit_to_current_branch_radio"
- v-html="commitToCurrentBranchText"
- ></span>
+ <span class="ide-option-label" data-qa-selector="commit_to_current_branch_radio">
+ <gl-sprintf :message="s__('IDE|Commit to %{branchName} branch')">
+ <template #branchName>
+ <strong class="monospace">{{ currentBranchText }}</strong>
+ </template>
+ </gl-sprintf>
+ </span>
</radio-group>
<template v-if="!emptyRepo">
<radio-group
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
index bbcb866c758..53fac09ab66 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions } from 'vuex';
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlButton } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
@@ -8,6 +8,7 @@ import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
export default {
components: {
GlModal,
+ GlButton,
FileIcon,
ChangedFileIcon,
},
@@ -52,15 +53,16 @@ export default {
</strong>
<changed-file-icon :file="activeFile" :is-centered="false" />
<div class="ml-auto">
- <button
+ <gl-button
v-if="canDiscard"
ref="discardButton"
- type="button"
- class="btn btn-remove btn-inverted gl-mr-3"
+ category="secondary"
+ variant="danger"
+ class="gl-mr-3"
@click="showDiscardModal"
>
{{ __('Discard changes') }}
- </button>
+ </gl-button>
</div>
<gl-modal
ref="discardModal"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 73c56514fce..f36fe87ccfa 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -7,7 +7,6 @@ import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
-import consts from '../../stores/modules/commit/constants';
import { createUnexpectedCommitError } from '../../lib/errors';
export default {
@@ -45,12 +44,11 @@ export default {
return this.currentActivityView === leftSidebarViews.commit.name;
},
commitErrorPrimaryAction() {
- if (!this.lastCommitError?.canCreateBranch) {
- return undefined;
- }
+ const { primaryAction } = this.lastCommitError || {};
return {
- text: __('Create new branch'),
+ button: primaryAction ? { text: primaryAction.text } : undefined,
+ callback: primaryAction?.callback?.bind(this, this.$store) || (() => {}),
};
},
},
@@ -78,9 +76,6 @@ export default {
commit() {
return this.commitChanges();
},
- forceCreateNewBranch() {
- return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit());
- },
handleCompactState() {
if (this.lastCommitMsg) {
this.isCompact = false;
@@ -188,9 +183,9 @@ export default {
ref="commitErrorModal"
modal-id="ide-commit-error-modal"
:title="lastCommitError.title"
- :action-primary="commitErrorPrimaryAction"
+ :action-primary="commitErrorPrimaryAction.button"
:action-cancel="{ text: __('Cancel') }"
- @ok="forceCreateNewBranch"
+ @ok="commitErrorPrimaryAction.callback"
>
<div v-safe-html="lastCommitError.messageHTML"></div>
</gl-modal>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index 2787b10a48b..7d08815b033 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlPopover } from '@gitlab/ui';
import { __, sprintf } from '../../../locale';
import popover from '../../../vue_shared/directives/popover';
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
@@ -10,6 +10,7 @@ export default {
},
components: {
GlIcon,
+ GlPopover,
},
props: {
text: {
@@ -58,7 +59,7 @@ export default {
},
},
popoverOptions: {
- trigger: 'hover',
+ triggers: 'hover',
placement: 'top',
content: sprintf(
__(`
@@ -83,9 +84,16 @@ export default {
<ul class="nav-links">
<li>
{{ __('Commit Message') }}
- <span v-popover="$options.popoverOptions" class="form-text text-muted gl-ml-3">
- <gl-icon name="question" />
- </span>
+ <div id="ide-commit-message-popover-container">
+ <span id="ide-commit-message-question" class="form-text text-muted gl-ml-3">
+ <gl-icon name="question" />
+ </span>
+ <gl-popover
+ target="ide-commit-message-question"
+ container="ide-commit-message-popover-container"
+ v-bind="$options.popoverOptions"
+ />
+ </div>
</li>
</ul>
</div>
@@ -108,6 +116,7 @@ export default {
:placeholder="placeholder"
:value="text"
class="note-textarea ide-commit-message-textarea"
+ data-qa-selector="ide_commit_message_field"
dir="auto"
name="commit-message"
@scroll="handleScroll"
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 732fa0786b0..dec8aa61838 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,8 +1,12 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { viewerTypes } from '../constants';
export default {
+ components: {
+ GlButton,
+ },
props: {
viewer: {
type: String,
@@ -31,7 +35,7 @@ export default {
<template>
<div class="dropdown">
- <button type="button" class="btn btn-link" data-toggle="dropdown">{{ __('Edit') }}</button>
+ <gl-button variant="link" data-toggle="dropdown">{{ __('Edit') }}</gl-button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
<li>
diff --git a/app/assets/javascripts/ide/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue
index b6a57d1b6e6..88dca2f0556 100644
--- a/app/assets/javascripts/ide/components/file_templates/bar.vue
+++ b/app/assets/javascripts/ide/components/file_templates/bar.vue
@@ -1,10 +1,12 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import Dropdown from './dropdown.vue';
export default {
components: {
Dropdown,
+ GlButton,
},
computed: {
...mapGetters(['activeFile']),
@@ -65,9 +67,9 @@ export default {
@click="selectTemplate"
/>
<transition name="fade">
- <button v-show="updateSuccess" type="button" class="btn btn-default" @click="undo">
+ <gl-button v-show="updateSuccess" category="secondary" variant="default" @click="undo">
{{ __('Undo') }}
- </button>
+ </gl-button>
</transition>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
index d80662f6ae1..cfd2555b769 100644
--- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -1,12 +1,13 @@
<script>
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
export default {
components: {
DropdownButton,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -85,7 +86,7 @@ export default {
type="search"
class="dropdown-input-field qa-dropdown-filter-input"
/>
- <i aria-hidden="true" class="fa fa-search dropdown-input-search"></i>
+ <gl-icon name="search" class="dropdown-input-search" aria-hidden="true" />
</div>
<div class="dropdown-content">
<gl-loading-icon v-if="showLoading" size="lg" />
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 1b03d9eee8b..8f23856fd6c 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -2,7 +2,18 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
+import {
+ WEBIDE_MARK_APP_START,
+ WEBIDE_MARK_FILE_FINISH,
+ WEBIDE_MARK_FILE_CLICKED,
+ WEBIDE_MARK_TREE_FINISH,
+ WEBIDE_MEASURE_TREE_FROM_REQUEST,
+ WEBIDE_MEASURE_FILE_FROM_REQUEST,
+ WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
+} from '~/performance_constants';
+import { performanceMarkAndMeasure } from '~/performance_utils';
import { modalTypes } from '../constants';
+import eventHub from '../eventhub';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue';
@@ -14,6 +25,22 @@ import ErrorMessage from './error_message.vue';
import CommitEditorHeader from './commit_sidebar/editor_header.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { measurePerformance } from '../utils';
+
+eventHub.$on(WEBIDE_MEASURE_TREE_FROM_REQUEST, () =>
+ measurePerformance(WEBIDE_MARK_TREE_FINISH, WEBIDE_MEASURE_TREE_FROM_REQUEST),
+);
+eventHub.$on(WEBIDE_MEASURE_FILE_FROM_REQUEST, () =>
+ measurePerformance(WEBIDE_MARK_FILE_FINISH, WEBIDE_MEASURE_FILE_FROM_REQUEST),
+);
+eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () =>
+ measurePerformance(
+ WEBIDE_MARK_FILE_FINISH,
+ WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
+ WEBIDE_MARK_FILE_CLICKED,
+ ),
+);
+
export default {
components: {
NewModal,
@@ -59,6 +86,9 @@ export default {
if (this.themeName)
document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`);
},
+ beforeCreate() {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_APP_START });
+ },
methods: {
...mapActions(['toggleFileFinder']),
onBeforeUnload(e = {}) {
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
index e36d0a5a5b1..7d2f0acb08c 100644
--- a/app/assets/javascripts/ide/components/ide_review.vue
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -23,26 +23,32 @@ export default {
},
},
mounted() {
- if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) {
- this.$router.push(this.getUrlForPath(this.activeFile.path), () => {
- this.updateViewer('editor');
- });
- } else if (this.activeFile && this.activeFile.deleted) {
- this.resetOpenFiles();
- }
-
- this.$nextTick(() => {
- this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff);
- });
+ this.initialize();
+ },
+ activated() {
+ this.initialize();
},
methods: {
...mapActions(['updateViewer', 'resetOpenFiles']),
+ initialize() {
+ if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) {
+ this.$router.push(this.getUrlForPath(this.activeFile.path), () => {
+ this.updateViewer(viewerTypes.edit);
+ });
+ } else if (this.activeFile && this.activeFile.deleted) {
+ this.resetOpenFiles();
+ }
+
+ this.$nextTick(() => {
+ this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff);
+ });
+ },
},
};
</script>
<template>
- <ide-tree-list :viewer-type="viewer" header-class="ide-review-header">
+ <ide-tree-list header-class="ide-review-header">
<template #header>
<div class="ide-review-button-holder">
{{ __('Review') }}
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index ed68ca5cae9..53dfc133fc8 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -7,9 +7,8 @@ import ActivityBar from './activity_bar.vue';
import RepoCommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeReview from './ide_review.vue';
-import SuccessMessage from './commit_sidebar/success_message.vue';
import IdeProjectHeader from './ide_project_header.vue';
-import { leftSidebarViews, SIDEBAR_INIT_WIDTH } from '../constants';
+import { SIDEBAR_INIT_WIDTH } from '../constants';
export default {
components: {
@@ -20,18 +19,11 @@ export default {
IdeTree,
CommitForm,
IdeReview,
- SuccessMessage,
IdeProjectHeader,
},
computed: {
...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']),
...mapGetters(['currentProject', 'someUncommittedChanges']),
- showSuccessMessage() {
- return (
- this.currentActivityView === leftSidebarViews.edit.name &&
- (this.lastCommitMsg && !this.someUncommittedChanges)
- );
- },
},
SIDEBAR_INIT_WIDTH,
};
@@ -44,7 +36,7 @@ export default {
class="multi-file-commit-panel flex-column"
>
<template v-if="loading">
- <div class="multi-file-commit-panel-inner">
+ <div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner">
<div v-for="n in 3" :key="n" class="multi-file-loading-container">
<gl-skeleton-loading />
</div>
@@ -54,9 +46,11 @@ export default {
<ide-project-header :project="currentProject" />
<div class="ide-context-body d-flex flex-fill">
<activity-bar />
- <div class="multi-file-commit-panel-inner">
+ <div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner">
<div class="multi-file-commit-panel-inner-content">
- <component :is="currentActivityView" />
+ <keep-alive>
+ <component :is="currentActivityView" />
+ </keep-alive>
</div>
<commit-form />
</div>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 146e818d654..ee292190e06 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,10 +1,9 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import IdeStatusList from './ide_status_list.vue';
import IdeStatusMr from './ide_status_mr.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
@@ -19,7 +18,7 @@ export default {
IdeStatusMr,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
mixins: [timeAgoMixin],
data() {
@@ -85,7 +84,7 @@ export default {
@click="openRightPane($options.rightSidebarViews.pipelines)"
>
<ci-icon
- v-tooltip
+ v-gl-tooltip
:status="latestPipeline.details.status"
:title="latestPipeline.details.status.text"
/>
@@ -99,7 +98,7 @@ export default {
<gl-icon name="commit" />
<a
- v-tooltip
+ v-gl-tooltip
:title="lastCommit.message"
:href="getCommitPath(lastCommit.short_id)"
class="commit-sha"
@@ -116,7 +115,7 @@ export default {
/>
{{ lastCommit.author_name }}
<time
- v-tooltip
+ v-gl-tooltip
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
data-placement="top"
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index 747d5044790..51d783df0ad 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { modalTypes } from '../constants';
+import { modalTypes, viewerTypes } from '../constants';
import IdeTreeList from './ide_tree_list.vue';
import Upload from './new_dropdown/upload.vue';
import NewEntryButton from './new_dropdown/button.vue';
@@ -18,15 +18,10 @@ export default {
...mapGetters(['currentProject', 'currentTree', 'activeFile', 'getUrlForPath']),
},
mounted() {
- if (!this.activeFile) return;
-
- if (this.activeFile.pending && !this.activeFile.deleted) {
- this.$router.push(this.getUrlForPath(this.activeFile.path), () => {
- this.updateViewer('editor');
- });
- } else if (this.activeFile.deleted) {
- this.resetOpenFiles();
- }
+ this.initialize();
+ },
+ activated() {
+ this.initialize();
},
methods: {
...mapActions(['updateViewer', 'createTempEntry', 'resetOpenFiles']),
@@ -36,12 +31,27 @@ export default {
createNewFolder() {
this.$refs.newModal.open(modalTypes.tree);
},
+ initialize() {
+ this.$nextTick(() => {
+ this.updateViewer(viewerTypes.edit);
+ });
+
+ if (!this.activeFile) return;
+
+ if (this.activeFile.pending && !this.activeFile.deleted) {
+ this.$router.push(this.getUrlForPath(this.activeFile.path), () => {
+ this.updateViewer(viewerTypes.edit);
+ });
+ } else if (this.activeFile.deleted) {
+ this.resetOpenFiles();
+ }
+ },
},
};
</script>
<template>
- <ide-tree-list viewer-type="editor">
+ <ide-tree-list>
<template #header>
{{ __('Edit') }}
<div class="ide-tree-actions ml-auto d-flex">
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 776d8459515..dd226f07fb0 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -2,6 +2,13 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import FileTree from '~/vue_shared/components/file_tree.vue';
+import {
+ WEBIDE_MARK_TREE_START,
+ WEBIDE_MEASURE_TREE_FROM_REQUEST,
+ WEBIDE_MARK_FILE_CLICKED,
+} from '~/performance_constants';
+import { performanceMarkAndMeasure } from '~/performance_utils';
+import eventHub from '../eventhub';
import IdeFileRow from './ide_file_row.vue';
import NavDropdown from './nav_dropdown.vue';
@@ -12,10 +19,6 @@ export default {
FileTree,
},
props: {
- viewerType: {
- type: String,
- required: true,
- },
headerClass: {
type: String,
required: false,
@@ -29,11 +32,19 @@ export default {
return !this.currentTree || this.currentTree.loading;
},
},
- mounted() {
- this.updateViewer(this.viewerType);
+ beforeCreate() {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_TREE_START });
+ },
+ updated() {
+ if (this.currentTree?.tree?.length) {
+ eventHub.$emit(WEBIDE_MEASURE_TREE_FROM_REQUEST);
+ }
},
methods: {
- ...mapActions(['updateViewer', 'toggleTreeOpen']),
+ ...mapActions(['toggleTreeOpen']),
+ clickedFile() {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_CLICKED });
+ },
},
IdeFileRow,
};
@@ -51,7 +62,7 @@ export default {
<nav-dropdown />
<slot name="header"></slot>
</header>
- <div class="ide-tree-body h-100">
+ <div class="ide-tree-body h-100" data-testid="ide-tree-body">
<template v-if="currentTree.tree.length">
<file-tree
v-for="file in currentTree.tree"
@@ -60,6 +71,7 @@ export default {
:level="0"
:file-row-component="$options.IdeFileRow"
@toggleTreeOpen="toggleTreeOpen"
+ @clickFile="clickedFile"
/>
</template>
<div v-else class="file-row">{{ __('No files') }}</div>
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 11033a5cc88..a5ae8bbfe9a 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -2,9 +2,8 @@
/* eslint-disable vue/no-v-html */
import { mapActions, mapState } from 'vuex';
import { throttle } from 'lodash';
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '../../../locale';
-import tooltip from '../../../vue_shared/directives/tooltip';
import ScrollButton from './detail/scroll_button.vue';
import JobDescription from './detail/description.vue';
@@ -15,7 +14,7 @@ const scrollPositions = {
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
GlIcon,
@@ -84,7 +83,7 @@ export default {
<job-description :job="detailJob" />
<div class="controllers ml-auto">
<a
- v-tooltip
+ v-gl-tooltip
:title="__('Show complete raw log')"
:href="detailJob.rawPath"
data-placement="top"
@@ -92,7 +91,7 @@ export default {
class="controllers-buttons"
target="_blank"
>
- <i aria-hidden="true" class="fa fa-file-text-o"></i>
+ <gl-icon name="doc-text" aria-hidden="true" />
</a>
<scroll-button :disabled="isScrolledToTop" direction="up" @click="scrollUp" />
<scroll-button :disabled="isScrolledToBottom" direction="down" @click="scrollDown" />
diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
index 2c679a3edc7..f4859b9f312 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
@@ -1,7 +1,6 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '../../../../locale';
-import tooltip from '../../../../vue_shared/directives/tooltip';
const directions = {
up: 'up',
@@ -10,7 +9,7 @@ const directions = {
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
GlIcon,
@@ -46,7 +45,7 @@ export default {
<template>
<div
- v-tooltip
+ v-gl-tooltip
:title="tooltipTitle"
class="controllers-buttons"
data-container="body"
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 0b643947139..6c7f084c164 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -1,12 +1,11 @@
<script>
-import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import tooltip from '../../../vue_shared/directives/tooltip';
+import { GlLoadingIcon, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import Item from './item.vue';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
GlIcon,
@@ -67,7 +66,7 @@ export default {
<ci-icon :status="stage.status" :size="24" />
<strong
ref="stageTitle"
- v-tooltip="showTooltip"
+ v-gl-tooltip="showTooltip"
:title="showTooltip ? stage.name : null"
data-container="body"
class="gl-ml-3 text-truncate"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 528475849de..5ad836f346a 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -152,6 +152,7 @@ export default {
v-model.trim="entryName"
type="text"
class="form-control"
+ data-testid="file-name-field"
data-qa-selector="file_name_field"
:placeholder="placeholder"
/>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 84ff05c9750..4a9a2a57acd 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -35,7 +35,7 @@ export default {
name: `${this.path ? `${this.path}/` : ''}${name}`,
type: 'blob',
content,
- rawPath: !isText ? target.result : '',
+ rawPath: !isText ? URL.createObjectURL(file) : '',
});
if (isText) {
@@ -44,7 +44,7 @@ export default {
reader.addEventListener('load', e => emitCreateEvent(e.target.result), { once: true });
reader.readAsText(file);
} else {
- emitCreateEvent(encodedContent);
+ emitCreateEvent(rawContent);
}
},
readFile(file) {
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 5eed57bb6c5..92b99b5c731 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -26,28 +26,34 @@ export default {
},
},
mounted() {
- const file =
- this.lastOpenedFile && this.lastOpenedFile.type !== 'tree'
- ? this.lastOpenedFile
- : this.activeFile;
-
- if (!file) return;
-
- this.openPendingTab({
- file,
- keyPrefix: file.staged ? stageKeys.staged : stageKeys.unstaged,
- })
- .then(changeViewer => {
- if (changeViewer) {
- this.updateViewer('diff');
- }
- })
- .catch(e => {
- throw e;
- });
+ this.initialize();
+ },
+ activated() {
+ this.initialize();
},
methods: {
...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
+ initialize() {
+ const file =
+ this.lastOpenedFile && this.lastOpenedFile.type !== 'tree'
+ ? this.lastOpenedFile
+ : this.activeFile;
+
+ if (!file) return;
+
+ this.openPendingTab({
+ file,
+ keyPrefix: file.staged ? stageKeys.staged : stageKeys.unstaged,
+ })
+ .then(changeViewer => {
+ if (changeViewer) {
+ this.updateViewer('diff');
+ }
+ })
+ .catch(e => {
+ throw e;
+ });
+ },
},
stageKeys,
};
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f342ce1739c..56bbb6349cd 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -5,6 +5,14 @@ import { deprecatedCreateFlash as flash } from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import {
+ WEBIDE_MARK_FILE_CLICKED,
+ WEBIDE_MARK_FILE_START,
+ WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
+ WEBIDE_MEASURE_FILE_FROM_REQUEST,
+} from '~/performance_constants';
+import { performanceMarkAndMeasure } from '~/performance_utils';
+import eventHub from '../eventhub';
+import {
leftSidebarViews,
viewerTypes,
FILE_VIEW_MODE_EDITOR,
@@ -60,7 +68,7 @@ export default {
]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() {
- return this.file && !isTextFile(this.file);
+ return this.file && !this.file.loading && !isTextFile(this.file);
},
showContentViewer() {
return (
@@ -164,6 +172,9 @@ export default {
}
},
},
+ beforeCreate() {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_START });
+ },
beforeDestroy() {
this.editor.dispose();
},
@@ -224,6 +235,7 @@ export default {
return this.getFileData({
path: this.file.path,
makeFileActive: false,
+ toggleLoading: false,
}).then(() =>
this.getRawFileData({
path: this.file.path,
@@ -289,6 +301,11 @@ export default {
});
this.$emit('editorSetup');
+ if (performance.getEntriesByName(WEBIDE_MARK_FILE_CLICKED).length) {
+ eventHub.$emit(WEBIDE_MEASURE_FILE_AFTER_INTERACTION);
+ } else {
+ eventHub.$emit(WEBIDE_MEASURE_FILE_FROM_REQUEST);
+ }
},
refreshEditorDimensions() {
if (this.showEditor) {
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 59b1969face..bdb11e6b004 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -47,9 +47,9 @@ export const diffViewerErrors = Object.freeze({
});
export const leftSidebarViews = {
- edit: { name: 'ide-tree', keepAlive: false },
- review: { name: 'ide-review', keepAlive: false },
- commit: { name: 'repo-commit-section', keepAlive: false },
+ edit: { name: 'ide-tree' },
+ review: { name: 'ide-review' },
+ commit: { name: 'repo-commit-section' },
};
export const rightSidebarViews = {
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 7c767009de5..56d48e87c18 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -73,11 +73,9 @@ export function initIde(el, options = {}) {
* @param {Objects} options - Extra options for the IDE (Used by EE).
*/
export function startIde(options) {
- document.addEventListener('DOMContentLoaded', () => {
- const ideElement = document.getElementById('ide');
- if (ideElement) {
- resetServiceWorkersPublicPath();
- initIde(ideElement, options);
- }
- });
+ const ideElement = document.getElementById('ide');
+ if (ideElement) {
+ resetServiceWorkersPublicPath();
+ initIde(ideElement, options);
+ }
}
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 2b12230c7cd..493dedcd89a 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -157,8 +157,10 @@ export default class Editor {
}
updateDimensions() {
- this.instance.layout();
- this.updateDiffView();
+ if (this.instance) {
+ this.instance.layout();
+ this.updateDiffView();
+ }
}
setPosition({ lineNumber, column }) {
diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js
index 6ae18bc8180..e62d9d1e77f 100644
--- a/app/assets/javascripts/ide/lib/errors.js
+++ b/app/assets/javascripts/ide/lib/errors.js
@@ -1,25 +1,49 @@
import { escape } from 'lodash';
import { __ } from '~/locale';
+import consts from '../stores/modules/commit/constants';
const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/;
const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/;
+const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/;
-export const createUnexpectedCommitError = () => ({
+const createNewBranchAndCommit = store =>
+ store
+ .dispatch('commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH)
+ .then(() => store.dispatch('commit/commitChanges'));
+
+export const createUnexpectedCommitError = message => ({
title: __('Unexpected error'),
- messageHTML: __('Could not commit. An unexpected error occurred.'),
- canCreateBranch: false,
+ messageHTML: escape(message) || __('Could not commit. An unexpected error occurred.'),
});
export const createCodeownersCommitError = message => ({
title: __('CODEOWNERS rule violation'),
messageHTML: escape(message),
- canCreateBranch: true,
+ primaryAction: {
+ text: __('Create new branch'),
+ callback: createNewBranchAndCommit,
+ },
});
export const createBranchChangedCommitError = message => ({
title: __('Branch changed'),
messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`,
- canCreateBranch: true,
+ primaryAction: {
+ text: __('Create new branch'),
+ callback: createNewBranchAndCommit,
+ },
+});
+
+export const branchAlreadyExistsCommitError = message => ({
+ title: __('Branch already exists'),
+ messageHTML: `${escape(message)}<br/><br/>${__(
+ 'Would you like to try auto-generating a branch name?',
+ )}`,
+ primaryAction: {
+ text: __('Create new branch'),
+ callback: store =>
+ store.dispatch('commit/addSuffixToBranchName').then(() => createNewBranchAndCommit(store)),
+ },
});
export const parseCommitError = e => {
@@ -33,7 +57,9 @@ export const parseCommitError = e => {
return createCodeownersCommitError(message);
} else if (BRANCH_CHANGED_REGEX.test(message)) {
return createBranchChangedCommitError(message);
+ } else if (BRANCH_ALREADY_EXISTS.test(message)) {
+ return branchAlreadyExistsCommitError(message);
}
- return createUnexpectedCommitError();
+ return createUnexpectedCommitError(message);
};
diff --git a/app/assets/javascripts/ide/lib/languages/README.md b/app/assets/javascripts/ide/lib/languages/README.md
index e4d1a4c7818..c4f3de00783 100644
--- a/app/assets/javascripts/ide/lib/languages/README.md
+++ b/app/assets/javascripts/ide/lib/languages/README.md
@@ -1,7 +1,7 @@
# Web IDE Languages
The Web IDE uses the [Monaco editor](https://microsoft.github.io/monaco-editor/) which uses the [Monarch library](https://microsoft.github.io/monaco-editor/monarch.html) for syntax highlighting.
-The Web IDE currently supports all langauges defined in the [monaco-languages](https://github.com/microsoft/monaco-languages/tree/master/src) repository.
+The Web IDE currently supports all languages defined in the [monaco-languages](https://github.com/microsoft/monaco-languages/tree/master/src) repository.
## Adding New Languages
@@ -14,7 +14,7 @@ Should you be willing to help us and add support to GitLab for any missing langu
2. Create a new file in this folder called `{languageName}.js`, where `{languageName}` is the name of the language you want to add support for.
3. Follow the [Monarch documentation](https://microsoft.github.io/monaco-editor/monarch.html) to add a configuration for the new language.
- Example: The [`vue.js`](./vue.js) file in the current directory adds support for Vue.js Syntax Highlighting.
-4. Add tests for the new langauge implementation in `spec/frontend/ide/lib/languages/{langaugeName}.js`.
+4. Add tests for the new language implementation in `spec/frontend/ide/lib/languages/{langaugeName}.js`.
- Example: See [`vue_spec.js`](spec/frontend/ide/lib/languages/vue_spec.js).
5. Create a [Merge Request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) with your newly added language.
diff --git a/app/assets/javascripts/ide/lib/languages/hcl.js b/app/assets/javascripts/ide/lib/languages/hcl.js
new file mode 100644
index 00000000000..4539719b1f2
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/languages/hcl.js
@@ -0,0 +1,192 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See https://github.com/microsoft/monaco-languages/blob/master/LICENSE.md
+ *--------------------------------------------------------------------------------------------*/
+
+/* eslint-disable no-useless-escape */
+/* eslint-disable @gitlab/require-i18n-strings */
+
+const conf = {
+ comments: {
+ lineComment: '//',
+ blockComment: ['/*', '*/'],
+ },
+ brackets: [['{', '}'], ['[', ']'], ['(', ')']],
+ autoClosingPairs: [
+ { open: '{', close: '}' },
+ { open: '[', close: ']' },
+ { open: '(', close: ')' },
+ { open: '"', close: '"', notIn: ['string'] },
+ ],
+ surroundingPairs: [
+ { open: '{', close: '}' },
+ { open: '[', close: ']' },
+ { open: '(', close: ')' },
+ { open: '"', close: '"' },
+ ],
+};
+
+const language = {
+ defaultToken: '',
+ tokenPostfix: '.hcl',
+
+ keywords: [
+ 'var',
+ 'local',
+ 'path',
+ 'for_each',
+ 'any',
+ 'string',
+ 'number',
+ 'bool',
+ 'true',
+ 'false',
+ 'null',
+ 'if ',
+ 'else ',
+ 'endif ',
+ 'for ',
+ 'in',
+ 'endfor',
+ ],
+
+ operators: [
+ '=',
+ '>=',
+ '<=',
+ '==',
+ '!=',
+ '+',
+ '-',
+ '*',
+ '/',
+ '%',
+ '&&',
+ '||',
+ '!',
+ '<',
+ '>',
+ '?',
+ '...',
+ ':',
+ ],
+
+ symbols: /[=><!~?:&|+\-*\/\^%]+/,
+ escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
+ terraformFunctions: /(abs|ceil|floor|log|max|min|pow|signum|chomp|format|formatlist|indent|join|lower|regex|regexall|replace|split|strrev|substr|title|trimspace|upper|chunklist|coalesce|coalescelist|compact|concat|contains|distinct|element|flatten|index|keys|length|list|lookup|map|matchkeys|merge|range|reverse|setintersection|setproduct|setunion|slice|sort|transpose|values|zipmap|base64decode|base64encode|base64gzip|csvdecode|jsondecode|jsonencode|urlencode|yamldecode|yamlencode|abspath|dirname|pathexpand|basename|file|fileexists|fileset|filebase64|templatefile|formatdate|timeadd|timestamp|base64sha256|base64sha512|bcrypt|filebase64sha256|filebase64sha512|filemd5|filemd1|filesha256|filesha512|md5|rsadecrypt|sha1|sha256|sha512|uuid|uuidv5|cidrhost|cidrnetmask|cidrsubnet|tobool|tolist|tomap|tonumber|toset|tostring)/,
+ terraformMainBlocks: /(module|data|terraform|resource|provider|variable|output|locals)/,
+ tokenizer: {
+ root: [
+ // highlight main blocks
+ [
+ /^@terraformMainBlocks([ \t]*)([\w-]+|"[\w-]+"|)([ \t]*)([\w-]+|"[\w-]+"|)([ \t]*)(\{)/,
+ ['type', '', 'string', '', 'string', '', '@brackets'],
+ ],
+ // highlight all the remaining blocks
+ [
+ /(\w+[ \t]+)([ \t]*)([\w-]+|"[\w-]+"|)([ \t]*)([\w-]+|"[\w-]+"|)([ \t]*)(\{)/,
+ ['identifier', '', 'string', '', 'string', '', '@brackets'],
+ ],
+ // highlight block
+ [
+ /(\w+[ \t]+)([ \t]*)([\w-]+|"[\w-]+"|)([ \t]*)([\w-]+|"[\w-]+"|)(=)(\{)/,
+ ['identifier', '', 'string', '', 'operator', '', '@brackets'],
+ ],
+ // terraform general highlight - shared with expressions
+ { include: '@terraform' },
+ ],
+ terraform: [
+ // highlight terraform functions
+ [/@terraformFunctions(\()/, ['type', '@brackets']],
+ // all other words are variables or keywords
+ [
+ /[a-zA-Z_]\w*-*/, // must work with variables such as foo-bar and also with negative numbers
+ {
+ cases: {
+ '@keywords': { token: 'keyword.$0' },
+ '@default': 'variable',
+ },
+ },
+ ],
+ { include: '@whitespace' },
+ { include: '@heredoc' },
+ // delimiters and operators
+ [/[{}()\[\]]/, '@brackets'],
+ [/[<>](?!@symbols)/, '@brackets'],
+ [
+ /@symbols/,
+ {
+ cases: {
+ '@operators': 'operator',
+ '@default': '',
+ },
+ },
+ ],
+ // numbers
+ [/\d*\d+[eE]([\-+]?\d+)?/, 'number.float'],
+ [/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'],
+ [/\d[\d']*/, 'number'],
+ [/\d/, 'number'],
+ [/[;,.]/, 'delimiter'], // delimiter: after number because of .\d floats
+ // strings
+ [/"/, 'string', '@string'], // this will include expressions
+ [/'/, 'invalid'],
+ ],
+ heredoc: [
+ [
+ /<<[-]*\s*["]?([\w\-]+)["]?/,
+ { token: 'string.heredoc.delimiter', next: '@heredocBody.$1' },
+ ],
+ ],
+ heredocBody: [
+ [
+ /^([\w\-]+)$/,
+ {
+ cases: {
+ '$1==$S2': [
+ {
+ token: 'string.heredoc.delimiter',
+ next: '@popall',
+ },
+ ],
+ '@default': 'string.heredoc',
+ },
+ },
+ ],
+ [/./, 'string.heredoc'],
+ ],
+ whitespace: [
+ [/[ \t\r\n]+/, ''],
+ [/\/\*/, 'comment', '@comment'],
+ [/\/\/.*$/, 'comment'],
+ [/#.*$/, 'comment'],
+ ],
+ comment: [[/[^\/*]+/, 'comment'], [/\*\//, 'comment', '@pop'], [/[\/*]/, 'comment']],
+ string: [
+ [/\$\{/, { token: 'delimiter', next: '@stringExpression' }],
+ [/[^\\"\$]+/, 'string'],
+ [/@escapes/, 'string.escape'],
+ [/\\./, 'string.escape.invalid'],
+ [/"/, 'string', '@popall'],
+ ],
+ stringInsideExpression: [
+ [/[^\\"]+/, 'string'],
+ [/@escapes/, 'string.escape'],
+ [/\\./, 'string.escape.invalid'],
+ [/"/, 'string', '@pop'],
+ ],
+ stringExpression: [
+ [/\}/, { token: 'delimiter', next: '@pop' }],
+ [/"/, 'string', '@stringInsideExpression'],
+ { include: '@terraform' },
+ ],
+ },
+};
+
+export default {
+ id: 'hcl',
+ extensions: ['.tf', '.tfvars', '.hcl'],
+ aliases: ['Terraform', 'tf', 'HCL', 'hcl'],
+ conf,
+ language,
+};
diff --git a/app/assets/javascripts/ide/lib/languages/index.js b/app/assets/javascripts/ide/lib/languages/index.js
index 0c85a1104fc..580ad820bf9 100644
--- a/app/assets/javascripts/ide/lib/languages/index.js
+++ b/app/assets/javascripts/ide/lib/languages/index.js
@@ -1,5 +1,6 @@
import vue from './vue';
+import hcl from './hcl';
-const languages = [vue];
+const languages = [vue, hcl];
export default languages;
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 3515d1fc933..a0df85540f9 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -59,7 +59,7 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
export const getFileData = (
{ state, commit, dispatch, getters },
- { path, makeFileActive = true, openFile = makeFileActive },
+ { path, makeFileActive = true, openFile = makeFileActive, toggleLoading = true },
) => {
const file = state.entries[path];
const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path);
@@ -99,7 +99,7 @@ export const getFileData = (
});
})
.finally(() => {
- commit(types.TOGGLE_LOADING, { entry: file, forceValue: false });
+ if (toggleLoading) commit(types.TOGGLE_LOADING, { entry: file, forceValue: false });
});
};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index b8304a9b68d..500ce9f32d5 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -6,6 +6,7 @@ import {
PERMISSION_CREATE_MR,
PERMISSION_PUSH_CODE,
} from '../constants';
+import { addNumericSuffix } from '~/ide/utils';
import Api from '~/api';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
@@ -167,10 +168,7 @@ export const getAvailableFileName = (state, getters) => path => {
let newPath = path;
while (getters.entryExists(newPath)) {
- newPath = newPath.replace(
- /([ _-]?)(\d*)(\..+?$|$)/,
- (_, before, number, after) => `${before || '_'}${Number(number) + 1}${after}`,
- );
+ newPath = addNumericSuffix(newPath);
}
return newPath;
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 90a6c644d17..e0d2028d2e1 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -8,6 +8,7 @@ import consts from './constants';
import { leftSidebarViews } from '../../../constants';
import eventHub from '../../../eventhub';
import { parseCommitError } from '../../../lib/errors';
+import { addNumericSuffix } from '~/ide/utils';
export const updateCommitMessage = ({ commit }, message) => {
commit(types.UPDATE_COMMIT_MESSAGE, message);
@@ -17,11 +18,8 @@ export const discardDraft = ({ commit }) => {
commit(types.UPDATE_COMMIT_MESSAGE, '');
};
-export const updateCommitAction = ({ commit, getters }, commitAction) => {
- commit(types.UPDATE_COMMIT_ACTION, {
- commitAction,
- });
- commit(types.TOGGLE_SHOULD_CREATE_MR, !getters.shouldHideNewMrOption);
+export const updateCommitAction = ({ commit }, commitAction) => {
+ commit(types.UPDATE_COMMIT_ACTION, { commitAction });
};
export const toggleShouldCreateMR = ({ commit }) => {
@@ -32,6 +30,12 @@ export const updateBranchName = ({ commit }, branchName) => {
commit(types.UPDATE_NEW_BRANCH_NAME, branchName);
};
+export const addSuffixToBranchName = ({ commit, state }) => {
+ const newBranchName = addNumericSuffix(state.newBranchName, true);
+
+ commit(types.UPDATE_NEW_BRANCH_NAME, newBranchName);
+};
+
export const setLastCommitMessage = ({ commit, rootGetters }, data) => {
const { currentProject } = rootGetters;
const commitStats = data.stats
@@ -107,7 +111,7 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
// Pull commit options out because they could change
// During some of the pre and post commit processing
- const { shouldCreateMR, isCreatingNewBranch, branchName } = getters;
+ const { shouldCreateMR, shouldHideNewMrOption, isCreatingNewBranch, branchName } = getters;
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const stageFilesPromise = rootState.stagedFiles.length
? Promise.resolve()
@@ -167,7 +171,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000);
- if (shouldCreateMR) {
+ if (shouldCreateMR && !shouldHideNewMrOption) {
const { currentProject } = rootGetters;
const targetBranch = isCreatingNewBranch
? rootState.currentBranchId
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
index 2cf6e8e6f36..c4bfad6405e 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
@@ -10,9 +10,7 @@ export default {
Object.assign(state, { commitAction });
},
[types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) {
- Object.assign(state, {
- newBranchName,
- });
+ Object.assign(state, { newBranchName });
},
[types.UPDATE_LOADING](state, submitCommitLoading) {
Object.assign(state, {
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index c90bc2a3320..a981f86fa40 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -19,19 +19,20 @@ export default {
}
},
[types.TOGGLE_FILE_OPEN](state, path) {
- Object.assign(state.entries[path], {
- opened: !state.entries[path].opened,
- });
+ const entry = state.entries[path];
- if (state.entries[path].opened) {
+ entry.opened = !entry.opened;
+ if (entry.opened && !entry.tempFile) {
+ entry.loading = true;
+ }
+
+ if (entry.opened) {
Object.assign(state, {
openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]),
});
} else {
- const file = state.entries[path];
-
Object.assign(state, {
- openFiles: state.openFiles.filter(f => f.key !== file.key),
+ openFiles: state.openFiles.filter(f => f.key !== entry.key),
});
}
},
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index d9cdc7727ad..b7ced3a271a 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -3,7 +3,7 @@ import {
relativePathToAbsolute,
isAbsolute,
isRootRelative,
- isBase64DataUrl,
+ isBlobUrl,
} from '~/lib/utils/url_utility';
export const dataStructure = () => ({
@@ -110,14 +110,19 @@ export const createCommitPayload = ({
}) => ({
branch,
commit_message: state.commitMessage || getters.preBuiltCommitMessage,
- actions: getCommitFiles(rootState.stagedFiles).map(f => ({
- action: commitActionForFile(f),
- file_path: f.path,
- previous_path: f.prevPath || undefined,
- content: f.prevPath && !f.changed ? null : f.content || undefined,
- encoding: isBase64DataUrl(f.rawPath) ? 'base64' : 'text',
- last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha,
- })),
+ actions: getCommitFiles(rootState.stagedFiles).map(f => {
+ const isBlob = isBlobUrl(f.rawPath);
+ const content = isBlob ? btoa(f.content) : f.content;
+
+ return {
+ action: commitActionForFile(f),
+ file_path: f.path,
+ previous_path: f.prevPath || undefined,
+ content: f.prevPath && !f.changed ? null : content || undefined,
+ encoding: isBlob ? 'base64' : 'text',
+ last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha,
+ };
+ }),
start_sha: newBranch ? rootGetters.lastCommit.id : undefined,
});
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index cde53e1ef00..4cf4f5e1d81 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -1,6 +1,7 @@
import { languages } from 'monaco-editor';
import { flatten, isString } from 'lodash';
import { SIDE_LEFT, SIDE_RIGHT } from './constants';
+import { performanceMarkAndMeasure } from '~/performance_utils';
const toLowerCase = x => x.toLowerCase();
@@ -42,16 +43,17 @@ const KNOWN_TYPES = [
},
];
-export function isTextFile({ name, content, mimeType = '' }) {
+export function isTextFile({ name, raw, content, mimeType = '' }) {
const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, name));
-
if (knownType) return knownType.isText;
// does the string contain ascii characters only (ranges from space to tilde, tabs and new lines)
const asciiRegex = /^[ -~\t\n\r]+$/;
+ const fileContents = raw || content;
+
// for unknown types, determine the type by evaluating the file contents
- return isString(content) && (content === '' || asciiRegex.test(content));
+ return isString(fileContents) && (fileContents === '' || asciiRegex.test(fileContents));
}
export const createPathWithExt = p => {
@@ -137,3 +139,49 @@ export function readFileAsDataURL(file) {
export function getFileEOL(content = '') {
return content.includes('\r\n') ? 'CRLF' : 'LF';
}
+
+/**
+ * Adds or increments the numeric suffix to a filename/branch name.
+ * Retains underscore or dash before the numeric suffix if it already exists.
+ *
+ * Examples:
+ * hello -> hello-1
+ * hello-2425 -> hello-2425
+ * hello.md -> hello-1.md
+ * hello_2.md -> hello_3.md
+ * hello_ -> hello_1
+ * master-patch-22432 -> master-patch-22433
+ * patch_332 -> patch_333
+ *
+ * @param {string} filename File name or branch name
+ * @param {number} [randomize] Should randomize the numeric suffix instead of auto-incrementing?
+ */
+export function addNumericSuffix(filename, randomize = false) {
+ return filename.replace(/([ _-]?)(\d*)(\..+?$|$)/, (_, before, number, after) => {
+ const n = randomize
+ ? Math.random()
+ .toString()
+ .substring(2, 7)
+ .slice(-5)
+ : Number(number) + 1;
+ return `${before || '-'}${n}${after}`;
+ });
+}
+
+export const measurePerformance = (
+ mark,
+ measureName,
+ measureStart = undefined,
+ measureEnd = mark,
+) => {
+ performanceMarkAndMeasure({
+ mark,
+ measures: [
+ {
+ name: measureName,
+ start: measureStart,
+ end: measureEnd,
+ },
+ ],
+ });
+};