From 52f5f155f38b637e3dcdce1001c05f7c771e1e42 Mon Sep 17 00:00:00 2001 From: Denys Mishunov Date: Sat, 18 Feb 2023 23:19:38 +0100 Subject: [PATCH 1/4] Added the quickfind snippet component --- .../source_editor_quickfind_widget.vue | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 app/assets/javascripts/editor/components/source_editor_quickfind_widget.vue diff --git a/app/assets/javascripts/editor/components/source_editor_quickfind_widget.vue b/app/assets/javascripts/editor/components/source_editor_quickfind_widget.vue new file mode 100644 index 00000000000000..a2668285de835d --- /dev/null +++ b/app/assets/javascripts/editor/components/source_editor_quickfind_widget.vue @@ -0,0 +1,122 @@ + + -- GitLab From 51e91a644abadda161d667ed2847caf4cee0e2f3 Mon Sep 17 00:00:00 2001 From: Denys Mishunov Date: Sat, 18 Feb 2023 23:20:14 +0100 Subject: [PATCH 2/4] Added the GraphQL query to get user's snippets --- .../repository/user_snippets.query.graphql | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/graphql/queries/repository/user_snippets.query.graphql diff --git a/app/graphql/queries/repository/user_snippets.query.graphql b/app/graphql/queries/repository/user_snippets.query.graphql new file mode 100644 index 00000000000000..0f101aacba362d --- /dev/null +++ b/app/graphql/queries/repository/user_snippets.query.graphql @@ -0,0 +1,25 @@ +query getUserSnippetsQuery { + currentUser { + __typename + snippets { + __typename + nodes { + __typename + id + title + description + blobs { + __typename + nodes { + __typename + name + binary + simpleViewer { + tooLarge + } + } + } + } + } + } +} -- GitLab From a09847f99411a67170ca03aa92ea92dff9bb1559 Mon Sep 17 00:00:00 2001 From: Denys Mishunov Date: Sat, 18 Feb 2023 23:21:45 +0100 Subject: [PATCH 3/4] Added new extension for SFE --- app/assets/javascripts/blob_edit/edit_blob.js | 2 + .../extensions/source_editor_snippets_ext.js | 199 ++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 app/assets/javascripts/editor/extensions/source_editor_snippets_ext.js diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index a3d11d90ed295b..e2575a80780aef 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import { renderGFM } from '~/behaviors/markdown/render_gfm'; import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base'; import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext'; +import { SnippetsExtension } from '~/editor/extensions/source_editor_snippets_ext'; import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext'; import SourceEditor from '~/editor/source_editor'; import { createAlert } from '~/flash'; @@ -71,6 +72,7 @@ export default class EditBlob { { definition: SourceEditorExtension }, { definition: FileTemplateExtension }, { definition: ToolbarExtension }, + { definition: SnippetsExtension }, ]); fileNameEl.addEventListener('change', () => { diff --git a/app/assets/javascripts/editor/extensions/source_editor_snippets_ext.js b/app/assets/javascripts/editor/extensions/source_editor_snippets_ext.js new file mode 100644 index 00000000000000..d46670171c14f1 --- /dev/null +++ b/app/assets/javascripts/editor/extensions/source_editor_snippets_ext.js @@ -0,0 +1,199 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import GetBlobContent from 'shared_queries/snippet/snippet_blob_content.query.graphql'; +import QuickFindWidget from '../components/source_editor_quickfind_widget.vue'; + +const EXTENSION_SNIPPET_INSERT_ACTION_ID = 'snippet-insert'; +const EXTENSION_SNIPPET_CREATE_ACTION_ID = 'snippet-create'; + +const EXTENSION_SNIPPET_INSERT_LABEL = 'Insert Snippet'; +const EXTENSION_SNIPPET_CREATE_LABEL = 'Create Snippet from selection'; + +Vue.use(VueApollo); + +class QuickFinderWidget { + constructor(codeEditor) { + this.codeEditor = codeEditor; + const wrapper = document.createElement('div'); + const content = document.createElement('div'); + wrapper.appendChild(content); + this.domNode = wrapper; + this.contentNode = content; + this.codeEditor.addOverlayWidget(this); + } + getId() { + return 'se.extension.snippets.file.widget'; + } + getDomNode() { + return this.domNode; + } + getContentNode() { + return this.contentNode; + } + getPosition() { + return { preference: 2 /* TOP_CENTER */ }; + } + dispose() { + this.codeEditor.removeOverlayWidget(this); + } +} + +export class SnippetsExtension { + /** + * A required getter returning the extension's name + * We have to provide it for every extension instead of relying on the built-in + * `name` prop because the prop does not survive the webpack's minification + * and the name mangling. + * @returns {string} + */ + static get extensionName() { + return 'SnippetsExtension'; + } + + /** + * Is called before the extension gets used by an instance, + * Use `onSetup` to setup Monaco directly: + * actions, keystrokes, update options, etc. + * Is called only once before the extension gets registered + * + * @param { Object } [instance] The Source Editor instance + * @param { Object } [setupOptions] The setupOptions object + */ + // eslint-disable-next-line class-methods-use-this,no-unused-vars + onSetup(instance, setupOptions) { + this.hasSelectionKey = instance.createContextKey('hasSelection', false); + this.finderWidget = new QuickFinderWidget(instance); + instance.onContextMenu(() => { + if(!instance.getSelection().isEmpty()) { + this.hasSelectionKey.set(true); + } else { + this.hasSelectionKey.set(false); + } + }); + this.setupInsertAction(instance); + // this.setupCreateAction(instance); + } + + setupVue(el, instance) { + const defaultClient = createDefaultClient(); + const apolloProvider = new VueApollo({ + defaultClient, + }); + const QuickFindComponent = Vue.extend(QuickFindWidget); + + this.finder = new QuickFindComponent({ + el, + apolloProvider, + }).$mount(); + this.finder.$on('snippet-selected', (snippet, path) => { + defaultClient.query({ + query: GetBlobContent, + variables: { + ids: [snippet.id], + rich: false, + paths: [path || snippet.blobs.nodes[0].name], + }, + }).then((result) => { + this.pasteContentAtCursor(result.data.snippets.nodes[0].blobs.nodes[0].rawPlainData, instance); + }); + }); + } + + pasteContentAtCursor(text, instance) { + instance.executeEdits('', [{ range: instance.getSelection(), text, forceMoveMarkers: true }]); + this.finderWidget.dispose(); + } + + setupInsertAction(instance) { + if (instance.getAction(EXTENSION_SNIPPET_INSERT_ACTION_ID)) return; + const actionBasis = { + contextMenuGroupId: 'navigation', + contextMenuOrder: 1.5, + // Method that will be executed when the action is triggered. + // @param ed The editor instance is passed in as a convenience + run(inst) { + inst.insertSnippetRequested(); + }, + }; + + instance.addAction({ + ...actionBasis, + id: EXTENSION_SNIPPET_INSERT_ACTION_ID, + label: EXTENSION_SNIPPET_INSERT_LABEL, + }); + } + + // setupCreateAction(instance) { + // if (instance.getAction(EXTENSION_SNIPPET_CREATE_ACTION_ID)) return; + // const actionBasis = { + // contextMenuGroupId: 'navigation', + // contextMenuOrder: 1.6, + // // Method that will be executed when the action is triggered. + // // @param ed The editor instance is passed in as a convenience + // run(inst) { + // inst.createSnippetRequested(); + // }, + // }; + + // instance.addAction({ + // ...actionBasis, + // id: EXTENSION_SNIPPET_CREATE_ACTION_ID, + // label: EXTENSION_SNIPPET_CREATE_LABEL, + // precondition: 'hasSelection', + // }); + // } + + // fetchSnippets() { + // return gqClient + // .query({ + // query: GetUserSnippetsQuery, + // }) + // .then(({ data }) => { + // const snippets = data.currentUser.snippets.nodes; + // const widgetNode = this.finderWidget.getDomNode(); + // const widgetContent = widgetNode.querySelector('quick-input-list'); + // const frag = new DocumentFragment(); + // snippets.forEach((snippet) => { + // const node = document.createElement('div'); + // node.classList.add('quick-input-list-entry', 'quick-input-list-separator-border'); + // node.innerHTML = '<>' + // frag.appendChild(node); + // }); + // // .innerHTML = this.snippets; + // }) + // .catch((e) => { + // debgugger; + // }); + // } + + showQuickFinderWidget(instance) { + this.finderWidget = new QuickFinderWidget(instance); + this.setupVue(this.finderWidget.getContentNode(), instance); + } + + /** + * Is called right after an extension is removed from an instance (un-used) + * Can be used for non time-critical tasks like cleanup on the Monaco level + * (removing actions, keystrokes, etc.). + * onUnuse() will be executed during the browser's idle period + * (https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) + * + * @param { Object } [instance] The Source Editor instance + */ + // eslint-disable-next-line class-methods-use-this,no-unused-vars + onUnuse(instance) {} + + /** + * The public API of the extension: these are the methods that will be exposed + * to the end user + * @returns {Object} + */ + provides() { + return { + insertSnippetRequested: (instance) => { + this.showQuickFinderWidget(instance); + }, + }; + } +} -- GitLab From 52497ef56c86832ebc273904f880e6e60e652d70 Mon Sep 17 00:00:00 2001 From: Denys Mishunov Date: Sat, 18 Feb 2023 23:22:08 +0100 Subject: [PATCH 4/4] Extended the blob content information --- app/graphql/queries/snippet/snippet_blob_content.query.graphql | 1 + 1 file changed, 1 insertion(+) diff --git a/app/graphql/queries/snippet/snippet_blob_content.query.graphql b/app/graphql/queries/snippet/snippet_blob_content.query.graphql index 0ac7d4d23a0928..4cc0b2fae113a1 100644 --- a/app/graphql/queries/snippet/snippet_blob_content.query.graphql +++ b/app/graphql/queries/snippet/snippet_blob_content.query.graphql @@ -11,6 +11,7 @@ query SnippetBlobContent($ids: [SnippetID!], $rich: Boolean!, $paths: [String!]) path richData @include(if: $rich) plainData @skip(if: $rich) + rawPlainData @skip(if: $rich) } hasUnretrievableBlobs } -- GitLab