From fabbc8cf15c767476d07eef76fe7a3a2c4eb529f Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Fri, 6 Dec 2024 16:32:17 +0100 Subject: [PATCH] feat(tree-explorer): add buttons to ask Copilot and create playgrounds from tree view VSCODE-651 (#890) Co-authored-by: Alena Khineika --- fonts/mongodb-icons.woff | Bin 1244 -> 1908 bytes images/icons/playground.svg | 1 + images/{dark => icons}/plus-circle.svg | 0 package.json | 72 +++++++++++++----- scripts/generate-icon-font.ts | 15 +++- src/commands/index.ts | 2 + src/editors/playgroundController.ts | 32 +++++++- .../playgroundSelectionCodeActionProvider.ts | 3 +- src/mdbExtensionController.ts | 34 ++++++++- src/participant/constants.ts | 1 + src/participant/participant.ts | 33 +++++++- ...laygroundFromCollectionTreeItemTemplate.ts | 15 ++++ .../playgroundFromDatabaseTreeItemTemplate.ts | 11 +++ src/templates/templateHelpers.ts | 10 +++ .../suite/participant/participant.test.ts | 69 +++++++++++++++++ 15 files changed, 268 insertions(+), 30 deletions(-) create mode 100644 images/icons/playground.svg rename images/{dark => icons}/plus-circle.svg (100%) create mode 100644 src/templates/playgroundFromCollectionTreeItemTemplate.ts create mode 100644 src/templates/playgroundFromDatabaseTreeItemTemplate.ts create mode 100644 src/templates/templateHelpers.ts diff --git a/fonts/mongodb-icons.woff b/fonts/mongodb-icons.woff index c12f6799b9969a9997c3059adb93b3cd4cb1513a..45d6e125479765cb70c11cb4a09c3ea3c01bf018 100644 GIT binary patch delta 1580 zcmXYxeK^x=7{`AzW0;YbD61Jlaxxs`oD@2ZT1vl-L<7OsAQHg*7jD1fya3>e@-3L?it39vT;>n{MH)UPsK8U}69O$%dp>iAO0=6ab)phU+Y093gMFoTIP_d!ev4gfUajRH7e^XRu(80qb=z zhtxn008r`VwZbge(1Y2&PNLl&53%6JC?TxzFvbW|pqK{5Q(&(hzR*s%xgTY|v?hU` z1RtjI6|PBvsoRjz(7_VM^MA#ryXx)L%HkQLa~;=j?`%=pMy9+`smxNVKTg;_ij;7> ziu+!yOe#3qhz&8@CBx$iw^4@$167>|gYN=2pLp6#Q2505rd2KMp%(dA?^vFnxvZ)0 z5K=hiEPEsoWfxix2l!))^TRrQrZroWE0(KtG<0X9Z#w3$e)e&>P@44CCiAh{698Zd z$sp825sJkR#o|NVE!@+m3CiyFR>U5=zDN*^sjK<|h%lZGtgg=vhb1^UIGb9cbk>3wN`OPfg?IW$RKhCJA$*>z6XU4mrR4^ACwb!JZEOi@jHEk(DkrvdjGx}Md8>h`;+oZXrZ?qAP4g)PWWrKT z)s9wYrX{L0%s;@M8Cl4H(%q=;i3276DQ)8h1-cl=;qid9g7mQfI$io^@?N#c9g_i>JAbxezCnLlo}6qj@ttwkhb6+~f0!3+^lWkWAxL*;}un&6PFOVB@kUnz)|7 zxTvV_i^ek>H|zpObVt?YBuom*rN9PcfuwwcBWhmUjofYzwB{ z=h{9GOLnMk>yX8X?JX-G7oB$&IA%V@sVq`^r-q-@m2qC#*r!Huf6QqV2m&*xOq195 zKaaC1Q_(z7tR(m%=u6X6Me1g$0_{V}(zSVp>v$lTmY;&*V=7%SfI$!rLBryDqY58!G&dMEzwa4M zn(}n(&9*nFeNovG_<3qy_kLbymFPTSsTLQyF(kg6SuxOgvfRRmUENXaqe!ZWz$3Bu Z-Tln+8e>szVg=A* zW&@y@JP<1|N;0VBCFZ6A#R7m1vH=?GpZa|6nu7ddpd(m%fNK1JxU}~0Dwc{l$q6Y5 z2@jGC5;i0!6cv8vOnh+KQ|DyzgrtPT28IbXMzamojcq43G9P0;Rll(zlTV?K>9P+` z4o{Uskc7rzu9h60Gdyn`3I+OwJvKAFZSE*wekvi!vT>Dj=;0Fwco@7ku-ye&l9-S- z=fHte2M!!Kdw^MG<^mITfz1s&)|}yqaCDfJ&2^Zu?MJu|kCTI*a2XwyL?x_h85E zYaUNxdn2O0JYn@5B~|vhr%QJHC_0?~wXY}1dv?ZV)eq)Y%YMGo_Fhv|wy3QBiu&Gv zO<%VZuGw^LBbW8@s{7VqpC3#~DiO)%;{GPTET{1etGJWQ5eA^EWzQVsYBmsHxiB{} zsbgE$0$#Z)tt-jACb571%fCJ*IpgkSVXxD%X^|YOi|M&HMJJk5BGo z;P~}$&GpM|JR$2}JoR~}a{rEsyWNJEyY)T#w`2RhW}BOE-fd9a;JluL_mWB9g%wNw zFxF1uP=dxK!=od!JFOr8%-v^HV9a5d!2E}&$zf7+Bj*uDhIQF2wm`>$<6Iyqp<(hL zo*teR4eSi8g$zz0aRw$u4K*eP2G%bOst~>?o4yYRgZwvU&I&nbVo3xhjx+{FHHBwP zPHb)e6YQ87cw!kHHt%M&XY{K*Va?TGAi!{O?vLvc9_Jp;RA~{(ESo*yv-XV$%d-cB z)&Ez@$l5Y13mbE5OV5k#OIc-kHL2=Yx^8>FNNsP+!W6C6ZV%y|jP1cfj~N&MB(qJ) diff --git a/images/icons/playground.svg b/images/icons/playground.svg new file mode 100644 index 000000000..f38f28a70 --- /dev/null +++ b/images/icons/playground.svg @@ -0,0 +1 @@ + diff --git a/images/dark/plus-circle.svg b/images/icons/plus-circle.svg similarity index 100% rename from images/dark/plus-circle.svg rename to images/icons/plus-circle.svg diff --git a/package.json b/package.json index f147a1d2a..f4676cdbe 100644 --- a/package.json +++ b/package.json @@ -255,6 +255,11 @@ "dark": "images/dark/add.svg" } }, + { + "command": "mdb.createNewPlaygroundFromTreeItem", + "title": "Create MongoDB Playground", + "icon": "$(mdb-playground)" + }, { "command": "mdb.changeActiveConnection", "title": "MongoDB: Change Active Connection" @@ -330,10 +335,7 @@ { "command": "mdb.addDatabase", "title": "Add Database...", - "icon": { - "light": "images/light/plus-circle.svg", - "dark": "images/dark/plus-circle.svg" - } + "icon": "$(mdb-plus-circle)" }, { "command": "mdb.searchForDocuments", @@ -371,13 +373,15 @@ "command": "mdb.refreshDatabase", "title": "Refresh" }, + { + "command": "mdb.askCopilotFromTreeItem", + "title": "Ask MongoDB Copilot", + "icon": "$(copilot)" + }, { "command": "mdb.addCollection", "title": "Add Collection...", - "icon": { - "light": "images/light/plus-circle.svg", - "dark": "images/dark/plus-circle.svg" - } + "icon": "$(mdb-plus-circle)" }, { "command": "mdb.viewCollectionDocuments", @@ -422,10 +426,7 @@ { "command": "mdb.createIndexFromTreeView", "title": "Create New Index...", - "icon": { - "light": "images/light/plus-circle.svg", - "dark": "images/dark/plus-circle.svg" - } + "icon": "$(mdb-plus-circle)" }, { "command": "mdb.insertObjectIdToEditor", @@ -454,10 +455,7 @@ { "command": "mdb.addStreamProcessor", "title": "Add StreamProcessor...", - "icon": { - "light": "images/light/plus-circle.svg", - "dark": "images/dark/plus-circle.svg" - } + "icon": "$(mdb-plus-circle)" }, { "command": "mdb.startStreamProcessor", @@ -587,7 +585,7 @@ { "command": "mdb.addCollection", "when": "view == mongoDBConnectionExplorer && viewItem == databaseTreeItem", - "group": "inline" + "group": "inline@3" }, { "command": "mdb.addCollection", @@ -604,10 +602,30 @@ "when": "view == mongoDBConnectionExplorer && viewItem == databaseTreeItem", "group": "2@1" }, + { + "command": "mdb.askCopilotFromTreeItem", + "when": "mdb.isCopilotActive == true && view == mongoDBConnectionExplorer && (viewItem == databaseTreeItem || viewItem == collectionTreeItem)", + "group": "inline@1" + }, + { + "command": "mdb.askCopilotFromTreeItem", + "when": "mdb.isCopilotActive == true && view == mongoDBConnectionExplorer && (viewItem == databaseTreeItem || viewItem == collectionTreeItem)", + "group": "3@1" + }, + { + "command": "mdb.createNewPlaygroundFromTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == databaseTreeItem || viewItem == collectionTreeItem)", + "group": "inline@2" + }, + { + "command": "mdb.createNewPlaygroundFromTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == databaseTreeItem || viewItem == collectionTreeItem)", + "group": "3@2" + }, { "command": "mdb.dropDatabase", "when": "view == mongoDBConnectionExplorer && viewItem == databaseTreeItem", - "group": "3@1" + "group": "4@1" }, { "command": "mdb.viewCollectionDocuments", @@ -1157,19 +1175,33 @@ } }, "icons": { - "mdb-connection-active": { + "mdb-playground": { "description": "MongoDB Icon", "default": { "fontPath": "./fonts/mongodb-icons.woff", "fontCharacter": "\\ea01" } }, - "mdb-connection-inactive": { + "mdb-plus-circle": { "description": "MongoDB Icon", "default": { "fontPath": "./fonts/mongodb-icons.woff", "fontCharacter": "\\ea02" } + }, + "mdb-connection-active": { + "description": "MongoDB Icon", + "default": { + "fontPath": "./fonts/mongodb-icons.woff", + "fontCharacter": "\\ea03" + } + }, + "mdb-connection-inactive": { + "description": "MongoDB Icon", + "default": { + "fontPath": "./fonts/mongodb-icons.woff", + "fontCharacter": "\\ea04" + } } } }, diff --git a/scripts/generate-icon-font.ts b/scripts/generate-icon-font.ts index 089dbf8d1..79ad286c9 100644 --- a/scripts/generate-icon-font.ts +++ b/scripts/generate-icon-font.ts @@ -4,7 +4,12 @@ import { GlyphData } from 'webfont/dist/src/types'; import prettier from 'prettier'; /** Icons to include in the generated icon font */ -const INCLUDED_ICONS = ['connection-active', 'connection-inactive']; +const INCLUDED_ICONS = [ + 'light/connection-active', + 'light/connection-inactive', + 'playground', + 'plus-circle', +]; /** * Generates an icon font from the included icons and outputs package.json @@ -13,7 +18,13 @@ const INCLUDED_ICONS = ['connection-active', 'connection-inactive']; */ async function main(): Promise { const font = await webfont({ - files: INCLUDED_ICONS.map((icon) => `./images/light/${icon}.svg`), + files: INCLUDED_ICONS.map((icon) => { + // Legacy support for icons inside light and dark folders. + if (icon.startsWith('light/')) { + return `./images/${icon}.svg`; + } + return `./images/icons/${icon}.svg`; + }), fontName: 'MongoDB Icons', formats: ['woff'], normalize: true, diff --git a/src/commands/index.ts b/src/commands/index.ts index a5fe02aa5..deca325c6 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -39,6 +39,7 @@ enum EXTENSION_COMMANDS { MDB_OPEN_PLAYGROUND_FROM_TREE_VIEW = 'mdb.openPlaygroundFromTreeView', MDB_CONNECT_TO_CONNECTION_TREE_VIEW = 'mdb.connectToConnectionTreeItem', MDB_CREATE_PLAYGROUND_FROM_TREE_VIEW = 'mdb.createNewPlaygroundFromTreeView', + MDB_CREATE_PLAYGROUND_FROM_TREE_ITEM = 'mdb.createNewPlaygroundFromTreeItem', MDB_DISCONNECT_FROM_CONNECTION_TREE_VIEW = 'mdb.disconnectFromConnectionTreeItem', MDB_EDIT_CONNECTION = 'mdb.editConnection', MDB_REFRESH_CONNECTION = 'mdb.refreshConnection', @@ -75,6 +76,7 @@ enum EXTENSION_COMMANDS { OPEN_PARTICIPANT_CODE_IN_PLAYGROUND = 'mdb.openParticipantCodeInPlayground', SEND_MESSAGE_TO_PARTICIPANT = 'mdb.sendMessageToParticipant', SEND_MESSAGE_TO_PARTICIPANT_FROM_INPUT = 'mdb.sendMessageToParticipantFromInput', + ASK_COPILOT_FROM_TREE_ITEM = 'mdb.askCopilotFromTreeItem', RUN_PARTICIPANT_CODE = 'mdb.runParticipantCode', CONNECT_WITH_PARTICIPANT = 'mdb.connectWithParticipant', SELECT_DATABASE_WITH_PARTICIPANT = 'mdb.selectDatabaseWithParticipant', diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index da4632294..7f2c2969d 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -8,6 +8,7 @@ import type ConnectionController from '../connectionController'; import { DataServiceEventTypes } from '../connectionController'; import { createLogger } from '../logging'; import type { ConnectionTreeItem } from '../explorer'; +import { CollectionTreeItem } from '../explorer'; import { DatabaseTreeItem } from '../explorer'; import formatError from '../utils/formatError'; import type { LanguageServerController } from '../language'; @@ -41,6 +42,8 @@ import { getPlaygroundExtensionForTelemetry, } from '../utils/playground'; import type ExportToLanguageCodeLensProvider from './exportToLanguageCodeLensProvider'; +import { playgroundFromDatabaseTreeItemTemplate } from '../templates/playgroundFromDatabaseTreeItemTemplate'; +import { playgroundFromCollectionTreeItemTemplate } from '../templates/playgroundFromCollectionTreeItemTemplate'; const log = createLogger('playground controller'); @@ -316,13 +319,36 @@ export default class PlaygroundController { return this._createPlaygroundFileWithContent(content); } + async createPlaygroundFromTreeItem( + treeItem: DatabaseTreeItem | CollectionTreeItem + ): Promise { + let content = ''; + if (treeItem instanceof DatabaseTreeItem) { + content = playgroundFromDatabaseTreeItemTemplate(treeItem.databaseName); + this._telemetryService.trackPlaygroundCreated('fromDatabaseTreeItem'); + } else if (treeItem instanceof CollectionTreeItem) { + content = playgroundFromCollectionTreeItemTemplate( + treeItem.databaseName, + treeItem.collectionName + ); + this._telemetryService.trackPlaygroundCreated('fromCollectionTreeItem'); + } + + return this._createPlaygroundFileWithContent(content); + } + async createPlayground(): Promise { const useDefaultTemplate = !!vscode.workspace .getConfiguration('mdb') .get('useDefaultTemplateForPlayground'); - const isStreams = this._connectionController.isConnectedToAtlasStreams(); - const template = isStreams ? playgroundStreamsTemplate : playgroundTemplate; - const content = useDefaultTemplate ? template : ''; + let content = ''; + if (useDefaultTemplate) { + const isStreams = this._connectionController.isConnectedToAtlasStreams(); + const template = isStreams + ? playgroundStreamsTemplate + : playgroundTemplate; + content = template; + } this._telemetryService.trackPlaygroundCreated('crud'); return this._createPlaygroundFileWithContent(content); diff --git a/src/editors/playgroundSelectionCodeActionProvider.ts b/src/editors/playgroundSelectionCodeActionProvider.ts index 578e68ce4..d943ddaba 100644 --- a/src/editors/playgroundSelectionCodeActionProvider.ts +++ b/src/editors/playgroundSelectionCodeActionProvider.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import EXTENSION_COMMANDS from '../commands'; import { isPlayground, getSelectedText } from '../utils/playground'; +import { COPILOT_CHAT_EXTENSION_ID } from '../participant/constants'; export const EXPORT_TO_LANGUAGE_ALIASES = [ { id: 'csharp', alias: 'C#' }, @@ -42,7 +43,7 @@ export default class PlaygroundSelectionCodeActionProvider provideCodeActions(): vscode.CodeAction[] | undefined { const editor = vscode.window.activeTextEditor; - const copilot = vscode.extensions.getExtension('github.copilot-chat'); + const copilot = vscode.extensions.getExtension(COPILOT_CHAT_EXTENSION_ID); let codeActions: vscode.CodeAction[] = [ this.createCodeAction({ title: 'Run selected playground blocks', diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 43213a662..c7c5f139e 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -51,6 +51,10 @@ import type { SendMessageToParticipantOptions, SendMessageToParticipantFromInputOptions, } from './participant/participantTypes'; +import { + COPILOT_CHAT_EXTENSION_ID, + COPILOT_EXTENSION_ID, +} from './participant/constants'; // This class is the top-level controller for our extension. // Commands which the extensions handles are defined in the function `activate`. @@ -177,12 +181,26 @@ export default class MDBExtensionController implements vscode.Disposable { // ------ In-app notifications ------ // void this.showCopilotIntroductionForEstablishedUsers(); - const copilot = vscode.extensions.getExtension('GitHub.copilot'); + const copilot = vscode.extensions.getExtension(COPILOT_EXTENSION_ID); void vscode.commands.executeCommand( 'setContext', 'mdb.isCopilotActive', copilot?.isActive ); + + // TODO: This is a workaround related to https://github.com/microsoft/vscode/issues/234426 + // If the extension was found but is not activated, there is a chance that the MongoDB extension + // was activated before the Copilot one, so we check again after a delay. + if (copilot && !copilot?.isActive) { + setTimeout(() => { + const copilot = vscode.extensions.getExtension(COPILOT_EXTENSION_ID); + void vscode.commands.executeCommand( + 'setContext', + 'mdb.isCopilotActive', + copilot?.isActive === true + ); + }, 3000); + } } registerCommands = (): void => { @@ -330,6 +348,13 @@ export default class MDBExtensionController implements vscode.Disposable { return true; } ); + this.registerParticipantCommand( + EXTENSION_COMMANDS.ASK_COPILOT_FROM_TREE_ITEM, + async (treeItem: DatabaseTreeItem | CollectionTreeItem) => { + await this._participantController.askCopilotFromTreeItem(treeItem); + return true; + } + ); this.registerParticipantCommand( EXTENSION_COMMANDS.RUN_PARTICIPANT_CODE, ({ runnableContent }: RunParticipantCodeCommandArgs) => { @@ -742,6 +767,11 @@ export default class MDBExtensionController implements vscode.Disposable { EXTENSION_COMMANDS.MDB_CREATE_PLAYGROUND_FROM_TREE_VIEW, () => this._playgroundController.createPlayground() ); + this.registerCommand( + EXTENSION_COMMANDS.MDB_CREATE_PLAYGROUND_FROM_TREE_ITEM, + (treeItem: DatabaseTreeItem | CollectionTreeItem) => + this._playgroundController.createPlaygroundFromTreeItem(treeItem) + ); this.registerCommand( EXTENSION_COMMANDS.MDB_REFRESH_PLAYGROUNDS_FROM_TREE_VIEW, () => this._playgroundsExplorer.refresh() @@ -972,7 +1002,7 @@ export default class MDBExtensionController implements vscode.Disposable { } ); - const copilot = vscode.extensions.getExtension('github.copilot-chat'); + const copilot = vscode.extensions.getExtension(COPILOT_CHAT_EXTENSION_ID); if (result?.title === action) { await this._participantController.sendMessageToParticipant({ message: '', diff --git a/src/participant/constants.ts b/src/participant/constants.ts index 1aeec079e..86511f823 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -4,6 +4,7 @@ import { ChatMetadataStore } from './chatMetadata'; export const CHAT_PARTICIPANT_ID = 'mongodb.participant'; export const CHAT_PARTICIPANT_MODEL = 'gpt-4o'; export const COPILOT_EXTENSION_ID = 'GitHub.copilot'; +export const COPILOT_CHAT_EXTENSION_ID = 'GitHub.copilot-chat'; export type ParticipantResponseType = | 'query' diff --git a/src/participant/participant.ts b/src/participant/participant.ts index f39cec2bc..5d98dd543 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -55,6 +55,7 @@ import type { } from './participantTypes'; import { DEFAULT_EXPORT_TO_LANGUAGE_DRIVER_SYNTAX } from '../editors/exportToLanguageCodeLensProvider'; import { EXPORT_TO_LANGUAGE_ALIASES } from '../editors/playgroundSelectionCodeActionProvider'; +import { CollectionTreeItem, DatabaseTreeItem } from '../explorer'; const log = createLogger('participant'); @@ -137,7 +138,12 @@ export default class ParticipantController { async sendMessageToParticipant( options: SendMessageToParticipantOptions ): Promise { - const { message, isNewChat = false, isPartialQuery = false } = options; + const { + message, + isNewChat = false, + isPartialQuery = false, + ...otherOptions + } = options; if (isNewChat) { await vscode.commands.executeCommand('workbench.action.chat.newChat'); @@ -146,7 +152,8 @@ export default class ParticipantController { ); } - return vscode.commands.executeCommand('workbench.action.chat.open', { + return await vscode.commands.executeCommand('workbench.action.chat.open', { + ...otherOptions, query: `@MongoDB ${message}`, isPartialQuery, }); @@ -182,6 +189,28 @@ export default class ParticipantController { }); } + async askCopilotFromTreeItem( + treeItem: DatabaseTreeItem | CollectionTreeItem + ): Promise { + if (treeItem instanceof DatabaseTreeItem) { + const { databaseName } = treeItem; + + await this.sendMessageToParticipant({ + message: `I want to ask questions about the \`${databaseName}\` database.`, + isNewChat: true, + }); + } else if (treeItem instanceof CollectionTreeItem) { + const { databaseName, collectionName } = treeItem; + + await this.sendMessageToParticipant({ + message: `I want to ask questions about the \`${databaseName}\` database's \`${collectionName}\` collection.`, + isNewChat: true, + }); + } else { + throw new Error('Unsupported tree item type'); + } + } + async _getChatResponse({ modelInput, token, diff --git a/src/templates/playgroundFromCollectionTreeItemTemplate.ts b/src/templates/playgroundFromCollectionTreeItemTemplate.ts new file mode 100644 index 000000000..35c335887 --- /dev/null +++ b/src/templates/playgroundFromCollectionTreeItemTemplate.ts @@ -0,0 +1,15 @@ +import { createTemplate } from './templateHelpers'; + +export const playgroundFromCollectionTreeItemTemplate = createTemplate( + (databaseName, collectionName) => `// MongoDB Playground +// Use Ctrl+Space inside a snippet or a string literal to trigger completions. + +// The current database to use. +use(${databaseName}); + +// Find a document in a collection. +db.getCollection(${collectionName}).findOne({ + +}); +` +); diff --git a/src/templates/playgroundFromDatabaseTreeItemTemplate.ts b/src/templates/playgroundFromDatabaseTreeItemTemplate.ts new file mode 100644 index 000000000..0a5c57941 --- /dev/null +++ b/src/templates/playgroundFromDatabaseTreeItemTemplate.ts @@ -0,0 +1,11 @@ +import { createTemplate } from './templateHelpers'; + +export const playgroundFromDatabaseTreeItemTemplate = createTemplate( + (currentDatabase) => `// MongoDB Playground +// Use Ctrl+Space inside a snippet or a string literal to trigger completions. + +// The current database to use. +use(${currentDatabase}); + +` +); diff --git a/src/templates/templateHelpers.ts b/src/templates/templateHelpers.ts new file mode 100644 index 000000000..185464a32 --- /dev/null +++ b/src/templates/templateHelpers.ts @@ -0,0 +1,10 @@ +/** Wraps a template function and escapes given string arguments. */ +export function createTemplate string>( + templateBuilder: T +): (...args: Parameters) => string { + return (...args: Parameters) => { + const escapedArgs = args.map((arg) => JSON.stringify(arg)); + + return templateBuilder(...escapedArgs); + }; +} diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index ce89bf105..1263f0e6e 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -42,6 +42,8 @@ import { } from './participantHelpers'; import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensProvider'; import PlaygroundResultProvider from '../../../editors/playgroundResultProvider'; +import { CollectionTreeItem, DatabaseTreeItem } from '../../../explorer'; +import type { SendMessageToParticipantOptions } from '../../../participant/participantTypes'; // The Copilot's model in not available in tests, // therefore we need to mock its methods and returning values. @@ -1802,6 +1804,73 @@ Schema: }); }); + suite('opened from tree view', function () { + let sendMessageToParticipantStub: SinonStub< + [options: SendMessageToParticipantOptions], + Promise + >; + + beforeEach(function () { + sendMessageToParticipantStub = sinon.stub( + testParticipantController, + 'sendMessageToParticipant' + ); + }); + + suite('with a database item', function () { + const mockDatabaseItem = Object.assign( + Object.create(DatabaseTreeItem.prototype), + { + databaseName: 'testDb', + } as DatabaseTreeItem + ); + + test('opens the chat and sends a message to set database context', async function () { + expect(sendMessageToParticipantStub).not.called; + + await testParticipantController.askCopilotFromTreeItem( + mockDatabaseItem + ); + + expect(sendMessageToParticipantStub).has.callCount(1); + + expect(sendMessageToParticipantStub.getCall(0).args).deep.equals([ + { + message: `I want to ask questions about the \`${mockDatabaseItem.databaseName}\` database.`, + isNewChat: true, + }, + ]); + }); + }); + + suite('with a collection item', function () { + const mockCollectionItem = Object.assign( + Object.create(CollectionTreeItem.prototype), + { + databaseName: 'testDb', + collectionName: 'testColl', + } as CollectionTreeItem + ); + + test('opens the chat and sends a message to set database and collection context', async function () { + expect(sendMessageToParticipantStub).not.called; + + await testParticipantController.askCopilotFromTreeItem( + mockCollectionItem + ); + + expect(sendMessageToParticipantStub).has.callCount(1); + + expect(sendMessageToParticipantStub.getCall(0).args).deep.equals([ + { + message: `I want to ask questions about the \`${mockCollectionItem.databaseName}\` database's \`${mockCollectionItem.collectionName}\` collection.`, + isNewChat: true, + }, + ]); + }); + }); + }); + suite('determining the namespace', function () { ['query', 'schema'].forEach(function (command) { suite(`${command} command`, function () {