diff --git a/fonts/mongodb-icons.woff b/fonts/mongodb-icons.woff
index c12f6799b..45d6e1254 100644
Binary files a/fonts/mongodb-icons.woff and b/fonts/mongodb-icons.woff differ
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 () {