debian-mirror-gitlab/app/assets/javascripts/ide/components/repo_editor.vue

448 lines
12 KiB
Vue
Raw Normal View History

2018-05-09 12:01:36 +05:30
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
2020-10-24 23:57:45 +05:30
import { deprecatedCreateFlash as flash } from '~/flash';
2021-03-11 19:13:27 +05:30
import { __ } from '~/locale';
2021-01-03 14:25:43 +05:30
import {
WEBIDE_MARK_FILE_CLICKED,
2021-02-22 17:27:13 +05:30
WEBIDE_MARK_REPO_EDITOR_START,
WEBIDE_MARK_REPO_EDITOR_FINISH,
WEBIDE_MEASURE_REPO_EDITOR,
2021-01-03 14:25:43 +05:30
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
2021-01-29 00:20:46 +05:30
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
2021-03-11 19:13:27 +05:30
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
2019-10-12 21:52:04 +05:30
import {
2020-03-13 15:44:24 +05:30
leftSidebarViews,
2019-10-12 21:52:04 +05:30
viewerTypes,
FILE_VIEW_MODE_EDITOR,
FILE_VIEW_MODE_PREVIEW,
} from '../constants';
2021-03-11 19:13:27 +05:30
import eventHub from '../eventhub';
2018-05-09 12:01:36 +05:30
import Editor from '../lib/editor';
2020-06-23 00:09:42 +05:30
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
2021-03-11 19:13:27 +05:30
import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
import { extractMarkdownImagesFromEntries } from '../stores/utils';
import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
import FileTemplatesBar from './file_templates/bar.vue';
2018-05-09 12:01:36 +05:30
export default {
2021-02-22 17:27:13 +05:30
name: 'RepoEditor',
2018-05-09 12:01:36 +05:30
components: {
ContentViewer,
2018-11-08 19:23:39 +05:30
DiffViewer,
2018-11-20 20:47:30 +05:30
FileTemplatesBar,
2018-05-09 12:01:36 +05:30
},
props: {
file: {
type: Object,
required: true,
},
},
2020-05-24 23:13:21 +05:30
data() {
return {
content: '',
images: {},
2020-06-23 00:09:42 +05:30
rules: {},
2020-05-24 23:13:21 +05:30
};
},
2018-05-09 12:01:36 +05:30
computed: {
2018-12-05 23:21:45 +05:30
...mapState('rightPane', {
rightPaneIsOpen: 'isOpen',
}),
2021-01-29 00:20:46 +05:30
...mapState('editor', ['fileEditors']),
2020-03-13 15:44:24 +05:30
...mapState([
'viewer',
'panelResizing',
'currentActivityView',
'renderWhitespaceInCode',
'editorTheme',
2020-05-24 23:13:21 +05:30
'entries',
2020-11-24 15:15:51 +05:30
'currentProjectId',
2020-03-13 15:44:24 +05:30
]),
2018-10-15 14:42:47 +05:30
...mapGetters([
'currentMergeRequest',
'getStagedFile',
'isEditModeActive',
'isCommitModeActive',
2020-05-24 23:13:21 +05:30
'currentBranch',
2020-11-24 15:15:51 +05:30
'getJsonSchemaForPath',
2018-10-15 14:42:47 +05:30
]),
2018-11-20 20:47:30 +05:30
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
2021-01-29 00:20:46 +05:30
fileEditor() {
return getFileEditorOrDefault(this.fileEditors, this.file.path);
},
2021-03-08 18:12:59 +05:30
isBinaryFile() {
return !isTextFile(this.file);
},
2018-05-09 12:01:36 +05:30
shouldHideEditor() {
2021-03-08 18:12:59 +05:30
return this.file && !this.file.loading && this.isBinaryFile;
2018-05-09 12:01:36 +05:30
},
2018-11-08 19:23:39 +05:30
showContentViewer() {
return (
2019-09-30 21:07:59 +05:30
(this.shouldHideEditor || this.isPreviewViewMode) &&
2018-11-08 19:23:39 +05:30
(this.viewer !== viewerTypes.mr || !this.file.mrChange)
);
},
showDiffViewer() {
return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr;
},
2019-09-30 21:07:59 +05:30
isEditorViewMode() {
2021-01-29 00:20:46 +05:30
return this.fileEditor.viewMode === FILE_VIEW_MODE_EDITOR;
2019-09-30 21:07:59 +05:30
},
isPreviewViewMode() {
2021-01-29 00:20:46 +05:30
return this.fileEditor.viewMode === FILE_VIEW_MODE_PREVIEW;
2019-09-30 21:07:59 +05:30
},
2018-05-09 12:01:36 +05:30
editTabCSS() {
return {
2019-09-30 21:07:59 +05:30
active: this.isEditorViewMode,
2018-05-09 12:01:36 +05:30
};
},
previewTabCSS() {
return {
2019-09-30 21:07:59 +05:30
active: this.isPreviewViewMode,
2018-05-09 12:01:36 +05:30
};
},
2019-09-30 21:07:59 +05:30
showEditor() {
return !this.shouldHideEditor && this.isEditorViewMode;
},
2020-03-13 15:44:24 +05:30
editorOptions() {
return {
renderWhitespace: this.renderWhitespaceInCode ? 'all' : 'none',
theme: this.editorTheme,
};
},
2020-05-24 23:13:21 +05:30
currentBranchCommit() {
return this.currentBranch?.commit.id;
},
2020-06-23 00:09:42 +05:30
previewMode() {
return viewerInformationForPath(this.file.path);
},
fileType() {
return this.previewMode?.id || '';
},
2018-05-09 12:01:36 +05:30
},
watch: {
2018-10-15 14:42:47 +05:30
file(newVal, oldVal) {
if (oldVal.pending) {
this.removePendingTab(oldVal);
}
2018-05-09 12:01:36 +05:30
// Compare key to allow for files opened in review mode to be cached differently
2018-10-15 14:42:47 +05:30
if (oldVal.key !== this.file.key) {
2018-11-08 19:23:39 +05:30
this.initEditor();
2018-10-15 14:42:47 +05:30
2020-03-13 15:44:24 +05:30
if (this.currentActivityView !== leftSidebarViews.edit.name) {
2021-01-29 00:20:46 +05:30
this.updateEditor({
2019-10-12 21:52:04 +05:30
viewMode: FILE_VIEW_MODE_EDITOR,
2018-10-15 14:42:47 +05:30
});
}
}
},
currentActivityView() {
2020-03-13 15:44:24 +05:30
if (this.currentActivityView !== leftSidebarViews.edit.name) {
2021-01-29 00:20:46 +05:30
this.updateEditor({
2019-10-12 21:52:04 +05:30
viewMode: FILE_VIEW_MODE_EDITOR,
2018-10-15 14:42:47 +05:30
});
2018-05-09 12:01:36 +05:30
}
},
viewer() {
2018-11-18 11:00:15 +05:30
if (!this.file.pending) {
this.createEditorInstance();
}
2018-05-09 12:01:36 +05:30
},
panelResizing() {
if (!this.panelResizing) {
2019-09-30 21:07:59 +05:30
this.refreshEditorDimensions();
2018-05-09 12:01:36 +05:30
}
},
2018-12-05 23:21:45 +05:30
rightPaneIsOpen() {
2019-09-30 21:07:59 +05:30
this.refreshEditorDimensions();
},
showEditor(val) {
if (val) {
// We need to wait for the editor to actually be rendered.
this.$nextTick(() => this.refreshEditorDimensions());
}
2018-11-08 19:23:39 +05:30
},
2020-05-24 23:13:21 +05:30
showContentViewer(val) {
if (!val) return;
if (this.fileType === 'markdown') {
const { content, images } = extractMarkdownImagesFromEntries(this.file, this.entries);
this.content = content;
this.images = images;
} else {
this.content = this.file.content || this.file.raw;
this.images = {};
}
},
2018-05-09 12:01:36 +05:30
},
beforeDestroy() {
this.editor.dispose();
},
mounted() {
2018-11-08 19:23:39 +05:30
if (!this.editor) {
2020-10-24 23:57:45 +05:30
this.editor = Editor.create(this.$store, this.editorOptions);
2018-05-09 12:01:36 +05:30
}
2018-11-08 19:23:39 +05:30
this.initEditor();
2020-06-23 00:09:42 +05:30
// listen in capture phase to be able to override Monaco's behaviour.
window.addEventListener('paste', this.onPaste, true);
},
destroyed() {
window.removeEventListener('paste', this.onPaste, true);
2018-05-09 12:01:36 +05:30
},
methods: {
...mapActions([
2018-11-18 11:00:15 +05:30
'getFileData',
2018-05-09 12:01:36 +05:30
'getRawFileData',
'changeFileContent',
2018-10-15 14:42:47 +05:30
'removePendingTab',
2019-09-04 21:01:54 +05:30
'triggerFilesChange',
2020-06-23 00:09:42 +05:30
'addTempImage',
2018-05-09 12:01:36 +05:30
]),
2021-01-29 00:20:46 +05:30
...mapActions('editor', ['updateFileEditor']),
2018-11-08 19:23:39 +05:30
initEditor() {
2021-02-22 17:27:13 +05:30
performanceMarkAndMeasure({ mark: WEBIDE_MARK_REPO_EDITOR_START });
2019-09-30 21:07:59 +05:30
if (this.shouldHideEditor && (this.file.content || this.file.raw)) {
return;
}
2018-05-09 12:01:36 +05:30
this.editor.clearEditor();
2020-11-24 15:15:51 +05:30
this.registerSchemaForFile();
2020-06-23 00:09:42 +05:30
Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()])
2018-05-09 12:01:36 +05:30
.then(() => {
this.createEditorInstance();
})
2021-03-08 18:12:59 +05:30
.catch((err) => {
2019-09-30 21:07:59 +05:30
flash(
__('Error setting up editor. Please try again.'),
'alert',
document,
null,
false,
true,
);
2018-05-09 12:01:36 +05:30
throw err;
});
},
2019-12-21 20:55:43 +05:30
fetchFileData() {
if (this.file.tempFile) {
return Promise.resolve();
}
return this.getFileData({
path: this.file.path,
makeFileActive: false,
2021-01-03 14:25:43 +05:30
toggleLoading: false,
2019-12-21 20:55:43 +05:30
}).then(() =>
this.getRawFileData({
path: this.file.path,
}),
);
},
2018-05-09 12:01:36 +05:30
createEditorInstance() {
2021-03-08 18:12:59 +05:30
if (this.isBinaryFile) {
return;
}
2018-05-09 12:01:36 +05:30
this.editor.dispose();
this.$nextTick(() => {
2018-10-15 14:42:47 +05:30
if (this.viewer === viewerTypes.edit) {
2018-05-09 12:01:36 +05:30
this.editor.createInstance(this.$refs.editor);
} else {
2020-06-23 00:09:42 +05:30
this.editor.createDiffInstance(this.$refs.editor);
2018-05-09 12:01:36 +05:30
}
this.setupEditor();
});
},
setupEditor() {
2020-07-28 23:09:34 +05:30
if (!this.file || !this.editor.instance || this.file.loading) return;
2018-05-09 12:01:36 +05:30
2018-10-15 14:42:47 +05:30
const head = this.getStagedFile(this.file.path);
this.model = this.editor.createModel(
this.file,
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
);
2018-05-09 12:01:36 +05:30
2018-10-15 14:42:47 +05:30
if (this.viewer === viewerTypes.mr && this.file.mrChange) {
2018-05-09 12:01:36 +05:30
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
}
2020-06-23 00:09:42 +05:30
this.model.updateOptions(this.rules);
2021-03-08 18:12:59 +05:30
this.model.onChange((model) => {
2018-05-09 12:01:36 +05:30
const { file } = model;
2020-06-23 00:09:42 +05:30
if (!file.active) return;
2018-05-09 12:01:36 +05:30
2020-06-23 00:09:42 +05:30
const monacoModel = model.getModel();
const content = monacoModel.getValue();
this.changeFileContent({ path: file.path, content });
2018-05-09 12:01:36 +05:30
});
// Handle Cursor Position
this.editor.onPositionChange((instance, e) => {
2021-01-29 00:20:46 +05:30
this.updateEditor({
2018-05-09 12:01:36 +05:30
editorRow: e.position.lineNumber,
editorColumn: e.position.column,
});
});
this.editor.setPosition({
2021-01-29 00:20:46 +05:30
lineNumber: this.fileEditor.editorRow,
column: this.fileEditor.editorColumn,
2018-05-09 12:01:36 +05:30
});
// Handle File Language
2021-01-29 00:20:46 +05:30
this.updateEditor({
2018-05-09 12:01:36 +05:30
fileLanguage: this.model.language,
});
2020-06-23 00:09:42 +05:30
this.$emit('editorSetup');
2021-01-03 14:25:43 +05:30
if (performance.getEntriesByName(WEBIDE_MARK_FILE_CLICKED).length) {
eventHub.$emit(WEBIDE_MEASURE_FILE_AFTER_INTERACTION);
} else {
2021-02-22 17:27:13 +05:30
performanceMarkAndMeasure({
mark: WEBIDE_MARK_REPO_EDITOR_FINISH,
measures: [
{
name: WEBIDE_MEASURE_REPO_EDITOR,
start: WEBIDE_MARK_REPO_EDITOR_START,
},
],
});
2021-01-03 14:25:43 +05:30
}
2018-05-09 12:01:36 +05:30
},
2019-09-30 21:07:59 +05:30
refreshEditorDimensions() {
if (this.showEditor) {
this.editor.updateDimensions();
}
},
2020-06-23 00:09:42 +05:30
fetchEditorconfigRules() {
2021-03-08 18:12:59 +05:30
return getRulesWithTraversal(this.file.path, (path) => {
2020-06-23 00:09:42 +05:30
const entry = this.entries[path];
if (!entry) return Promise.resolve(null);
const content = entry.content || entry.raw;
if (content) return Promise.resolve(content);
return this.getFileData({ path: entry.path, makeFileActive: false }).then(() =>
this.getRawFileData({ path: entry.path }),
);
2021-03-08 18:12:59 +05:30
}).then((rules) => {
2020-06-23 00:09:42 +05:30
this.rules = mapRulesToMonaco(rules);
});
},
onPaste(event) {
const editor = this.editor.instance;
const reImage = /^image\/(png|jpg|jpeg|gif)$/;
const file = event.clipboardData.files[0];
if (editor.hasTextFocus() && this.fileType === 'markdown' && reImage.test(file?.type)) {
// don't let the event be passed on to Monaco.
event.preventDefault();
event.stopImmediatePropagation();
2021-03-08 18:12:59 +05:30
return readFileAsDataURL(file).then((content) => {
2020-06-23 00:09:42 +05:30
const parentPath = getPathParent(this.file.path);
const path = `${parentPath ? `${parentPath}/` : ''}${file.name}`;
return this.addTempImage({ name: path, rawPath: content }).then(({ name: fileName }) => {
this.editor.replaceSelectedText(`![${fileName}](./${fileName})`);
});
});
}
// do nothing if no image is found in the clipboard
return Promise.resolve();
},
2020-11-24 15:15:51 +05:30
registerSchemaForFile() {
const schema = this.getJsonSchemaForPath(this.file.path);
registerSchema(schema);
},
2021-01-29 00:20:46 +05:30
updateEditor(data) {
// Looks like our model wrapper `.dispose` causes the monaco editor to emit some position changes after
// when disposing. We want to ignore these by only capturing editor changes that happen to the currently active
// file.
if (!this.file.active) {
return;
}
this.updateFileEditor({ path: this.file.path, data });
},
2018-05-09 12:01:36 +05:30
},
2018-10-15 14:42:47 +05:30
viewerTypes,
2019-10-12 21:52:04 +05:30
FILE_VIEW_MODE_EDITOR,
FILE_VIEW_MODE_PREVIEW,
2018-05-09 12:01:36 +05:30
};
</script>
<template>
2019-02-15 15:39:39 +05:30
<div id="ide" class="blob-viewer-container blob-editor-container">
2020-04-22 19:07:51 +05:30
<div v-if="!shouldHideEditor && isEditModeActive" class="ide-mode-tabs clearfix">
<ul class="nav-links float-left border-bottom-0">
2018-05-09 12:01:36 +05:30
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
2021-01-29 00:20:46 +05:30
@click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_EDITOR })"
2019-02-15 15:39:39 +05:30
>
2020-06-23 00:09:42 +05:30
{{ __('Edit') }}
2018-05-09 12:01:36 +05:30
</a>
</li>
2020-06-23 00:09:42 +05:30
<li v-if="previewMode" :class="previewTabCSS">
2018-05-09 12:01:36 +05:30
<a
href="javascript:void(0);"
role="button"
2021-01-29 00:20:46 +05:30
@click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_PREVIEW })"
2020-06-23 00:09:42 +05:30
>{{ previewMode.previewTitle }}</a
2019-02-15 15:39:39 +05:30
>
2018-05-09 12:01:36 +05:30
</li>
</ul>
</div>
2019-02-15 15:39:39 +05:30
<file-templates-bar v-if="showFileTemplatesBar(file.name)" />
2018-05-09 12:01:36 +05:30
<div
2019-09-30 21:07:59 +05:30
v-show="showEditor"
2018-05-09 12:01:36 +05:30
ref="editor"
2018-10-15 14:42:47 +05:30
:class="{
'is-readonly': isCommitModeActive,
2018-11-18 11:00:15 +05:30
'is-deleted': file.deleted,
2019-02-15 15:39:39 +05:30
'is-added': file.tempFile,
2018-10-15 14:42:47 +05:30
}"
2018-11-08 19:23:39 +05:30
class="multi-file-editor-holder"
2020-04-08 14:13:33 +05:30
data-qa-selector="editor_container"
2019-09-04 21:01:54 +05:30
@focusout="triggerFilesChange"
2019-02-15 15:39:39 +05:30
></div>
2018-05-09 12:01:36 +05:30
<content-viewer
2018-11-08 19:23:39 +05:30
v-if="showContentViewer"
2020-05-24 23:13:21 +05:30
:content="content"
:images="images"
2018-05-09 12:01:36 +05:30
:path="file.rawPath || file.path"
2019-12-26 22:10:19 +05:30
:file-path="file.path"
2018-05-09 12:01:36 +05:30
:file-size="file.size"
2020-11-24 15:15:51 +05:30
:project-path="currentProjectId"
2020-05-24 23:13:21 +05:30
:commit-sha="currentBranchCommit"
2019-07-31 22:56:46 +05:30
:type="fileType"
2019-02-15 15:39:39 +05:30
/>
2018-11-08 19:23:39 +05:30
<diff-viewer
v-if="showDiffViewer"
:diff-mode="file.mrChange.diffMode"
:new-path="file.mrChange.new_path"
:new-sha="currentMergeRequest.sha"
:old-path="file.mrChange.old_path"
:old-sha="currentMergeRequest.baseCommitSha"
2020-11-24 15:15:51 +05:30
:project-path="currentProjectId"
2019-02-15 15:39:39 +05:30
/>
2018-05-09 12:01:36 +05:30
</div>
</template>