Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(playground): add Generate Query with Copilot code lens in playgrounds VSCODE-650 #881

Merged
merged 13 commits into from
Nov 28, 2024
2 changes: 2 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ enum EXTENSION_COMMANDS {

// Chat participant.
OPEN_PARTICIPANT_CODE_IN_PLAYGROUND = 'mdb.openParticipantCodeInPlayground',
SEND_MESSAGE_TO_PARTICIPANT = 'mdb.sendMessageToParticipant',
SEND_MESSAGE_TO_PARTICIPANT_FROM_INPUT = 'mdb.sendMessageToParticipantFromInput',
RUN_PARTICIPANT_CODE = 'mdb.runParticipantCode',
CONNECT_WITH_PARTICIPANT = 'mdb.connectWithParticipant',
SELECT_DATABASE_WITH_PARTICIPANT = 'mdb.selectDatabaseWithParticipant',
Expand Down
10 changes: 5 additions & 5 deletions src/editors/activeConnectionCodeLensProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default class ActiveConnectionCodeLensProvider
this._onDidChangeCodeLenses.fire();
});

this._activeConnectionChangedHandler = () => {
this._activeConnectionChangedHandler = (): void => {
this._onDidChangeCodeLenses.fire();
};
this._connectionController.addEventListener(
Expand All @@ -50,10 +50,10 @@ export default class ActiveConnectionCodeLensProvider
? getDBFromConnectionString(connectionString)
: null;
message = defaultDB
? `Currently connected to ${this._connectionController.getActiveConnectionName()} with default database ${defaultDB}. Click here to change connection.`
: `Currently connected to ${this._connectionController.getActiveConnectionName()}. Click here to change connection.`;
? `Connected to ${this._connectionController.getActiveConnectionName()} with default database ${defaultDB}`
: `Connected to ${this._connectionController.getActiveConnectionName()}`;
} else {
message = 'Disconnected. Click here to connect.';
message = 'Connect';
}

codeLens.command = {
Expand All @@ -65,7 +65,7 @@ export default class ActiveConnectionCodeLensProvider
return [codeLens];
}

deactivate() {
deactivate(): void {
this._connectionController.removeEventListener(
DataServiceEventTypes.ACTIVE_CONNECTION_CHANGED,
this._activeConnectionChangedHandler
Expand Down
9 changes: 9 additions & 0 deletions src/editors/editorsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type PlaygroundResultProvider from './playgroundResultProvider';
import { PLAYGROUND_RESULT_SCHEME } from './playgroundResultProvider';
import { StatusView } from '../views';
import type TelemetryService from '../telemetry/telemetryService';
import type { QueryWithCopilotCodeLensProvider } from './queryWithCopilotCodeLensProvider';

const log = createLogger('editors controller');

Expand Down Expand Up @@ -102,6 +103,7 @@ export default class EditorsController {
_exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider;
_editDocumentCodeLensProvider: EditDocumentCodeLensProvider;
_collectionDocumentsCodeLensProvider: CollectionDocumentsCodeLensProvider;
_queryWithCopilotCodeLensProvider: QueryWithCopilotCodeLensProvider;

constructor({
context,
Expand All @@ -115,6 +117,7 @@ export default class EditorsController {
playgroundSelectionCodeActionProvider,
playgroundDiagnosticsCodeActionProvider,
editDocumentCodeLensProvider,
queryWithCopilotCodeLensProvider,
}: {
context: vscode.ExtensionContext;
connectionController: ConnectionController;
Expand All @@ -127,6 +130,7 @@ export default class EditorsController {
playgroundSelectionCodeActionProvider: PlaygroundSelectionCodeActionProvider;
playgroundDiagnosticsCodeActionProvider: PlaygroundDiagnosticsCodeActionProvider;
editDocumentCodeLensProvider: EditDocumentCodeLensProvider;
queryWithCopilotCodeLensProvider: QueryWithCopilotCodeLensProvider;
}) {
this._connectionController = connectionController;
this._playgroundController = playgroundController;
Expand Down Expand Up @@ -160,6 +164,7 @@ export default class EditorsController {
playgroundSelectionCodeActionProvider;
this._playgroundDiagnosticsCodeActionProvider =
playgroundDiagnosticsCodeActionProvider;
this._queryWithCopilotCodeLensProvider = queryWithCopilotCodeLensProvider;

vscode.workspace.onDidCloseTextDocument((e) => {
const uriParams = new URLSearchParams(e.uri.query);
Expand Down Expand Up @@ -410,6 +415,10 @@ export default class EditorsController {
)
);
this._context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
{ language: 'javascript' },
this._queryWithCopilotCodeLensProvider
),
vscode.languages.registerCodeLensProvider(
{ language: 'javascript' },
this._activeConnectionCodeLensProvider
Expand Down
46 changes: 46 additions & 0 deletions src/editors/queryWithCopilotCodeLensProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as vscode from 'vscode';
import EXTENSION_COMMANDS from '../commands';
import type { SendMessageToParticipantFromInputOptions } from '../participant/participantTypes';
import { isPlayground } from '../utils/playground';
import { COPILOT_EXTENSION_ID } from '../participant/constants';

export class QueryWithCopilotCodeLensProvider
implements vscode.CodeLensProvider
{
constructor() {}

readonly onDidChangeCodeLenses: vscode.Event<void> =
vscode.extensions.onDidChange;

provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] {
if (!isPlayground(document.uri)) {
return [];
}

// We can only detect whether a user has the Copilot extension active
// but not whether it has an active subscription.
const hasCopilotChatActive =
vscode.extensions.getExtension(COPILOT_EXTENSION_ID)?.isActive === true;

if (!hasCopilotChatActive) {
return [];
}

const options: SendMessageToParticipantFromInputOptions = {
prompt: 'Describe the query you would like to generate.',
gagik marked this conversation as resolved.
Show resolved Hide resolved
placeHolder:
'e.g. Find the document in sample_mflix.users with the name of Kayden Washington',
messagePrefix: '/query',
isNewChat: true,
source: 'query with copilot codelens',
};

return [
new vscode.CodeLens(new vscode.Range(0, 0, 0, 0), {
title: '✨ Generate query with MongoDB Copilot',
command: EXTENSION_COMMANDS.SEND_MESSAGE_TO_PARTICIPANT_FROM_INPUT,
arguments: [options],
}),
];
}
}
25 changes: 25 additions & 0 deletions src/mdbExtensionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ import type {
} from './participant/participant';
import ParticipantController from './participant/participant';
import type { OpenSchemaCommandArgs } from './participant/prompts/schema';
import { QueryWithCopilotCodeLensProvider } from './editors/queryWithCopilotCodeLensProvider';
import type {
SendMessageToParticipantOptions,
SendMessageToParticipantFromInputOptions,
} from './participant/participantTypes';

// This class is the top-level controller for our extension.
// Commands which the extensions handles are defined in the function `activate`.
Expand All @@ -67,6 +72,7 @@ export default class MDBExtensionController implements vscode.Disposable {
_telemetryService: TelemetryService;
_languageServerController: LanguageServerController;
_webviewController: WebviewController;
_queryWithCopilotCodeLensProvider: QueryWithCopilotCodeLensProvider;
_playgroundResultProvider: PlaygroundResultProvider;
_activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider;
_editDocumentCodeLensProvider: EditDocumentCodeLensProvider;
Expand Down Expand Up @@ -107,6 +113,8 @@ export default class MDBExtensionController implements vscode.Disposable {
this._connectionController,
this._editDocumentCodeLensProvider
);
this._queryWithCopilotCodeLensProvider =
new QueryWithCopilotCodeLensProvider();
this._activeConnectionCodeLensProvider =
new ActiveConnectionCodeLensProvider(this._connectionController);
this._exportToLanguageCodeLensProvider =
Expand Down Expand Up @@ -145,6 +153,7 @@ export default class MDBExtensionController implements vscode.Disposable {
playgroundDiagnosticsCodeActionProvider:
this._playgroundDiagnosticsCodeActionProvider,
editDocumentCodeLensProvider: this._editDocumentCodeLensProvider,
queryWithCopilotCodeLensProvider: this._queryWithCopilotCodeLensProvider,
});
this._webviewController = new WebviewController({
connectionController: this._connectionController,
Expand Down Expand Up @@ -306,6 +315,22 @@ export default class MDBExtensionController implements vscode.Disposable {
});
}
);
this.registerParticipantCommand(
EXTENSION_COMMANDS.SEND_MESSAGE_TO_PARTICIPANT,
async (options: SendMessageToParticipantOptions) => {
await this._participantController.sendMessageToParticipant(options);
return true;
}
);
this.registerParticipantCommand(
EXTENSION_COMMANDS.SEND_MESSAGE_TO_PARTICIPANT_FROM_INPUT,
async (options: SendMessageToParticipantFromInputOptions) => {
await this._participantController.sendMessageToParticipantFromInput(
options
);
return true;
}
);
this.registerParticipantCommand(
EXTENSION_COMMANDS.RUN_PARTICIPANT_CODE,
({ runnableContent }: RunParticipantCodeCommandArgs) => {
Expand Down
1 change: 1 addition & 0 deletions src/participant/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,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 type ParticipantResponseType =
| 'query'
Expand Down
70 changes: 58 additions & 12 deletions src/participant/participant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ import { ParticipantErrorTypes } from './participantErrorTypes';
import type PlaygroundResultProvider from '../editors/playgroundResultProvider';
import { isExportToLanguageResult } from '../types/playgroundType';
import { PromptHistory } from './prompts/promptHistory';
import type {
SendMessageToParticipantOptions,
SendMessageToParticipantFromInputOptions,
} from './participantTypes';
import { DEFAULT_EXPORT_TO_LANGUAGE_DRIVER_SYNTAX } from '../editors/exportToLanguageCodeLensProvider';
import { EXPORT_TO_LANGUAGE_ALIASES } from '../editors/playgroundSelectionCodeActionProvider';

Expand Down Expand Up @@ -130,9 +134,51 @@ export default class ParticipantController {
* in the chat. To work around this, we can write a message as the user, which will
* trigger the chat handler and give us access to the model.
*/
writeChatMessageAsUser(message: string): Thenable<unknown> {
async sendMessageToParticipant(
gagik marked this conversation as resolved.
Show resolved Hide resolved
options: SendMessageToParticipantOptions
): Promise<unknown> {
const { message, isNewChat = false, isPartialQuery = false } = options;

if (isNewChat) {
await vscode.commands.executeCommand('workbench.action.chat.newChat');
await vscode.commands.executeCommand(
'workbench.action.chat.clearHistory'
);
}

return vscode.commands.executeCommand('workbench.action.chat.open', {
query: `@MongoDB ${message}`,
isPartialQuery,
});
}

async sendMessageToParticipantFromInput(
options: SendMessageToParticipantFromInputOptions
): Promise<unknown> {
const {
messagePrefix = '',
isNewChat = false,
isPartialQuery = false,
source,
...inputBoxOptions
} = options;

this._telemetryService.trackCopilotParticipantSubmittedFromInputBox({
source,
});

const message = await vscode.window.showInputBox({
...inputBoxOptions,
});

if (message === undefined || message.trim() === '') {
return Promise.resolve();
}

return this.sendMessageToParticipant({
message: `${messagePrefix ? `${messagePrefix} ` : ''}${message}`,
isNewChat,
isPartialQuery,
});
}

Expand Down Expand Up @@ -460,9 +506,9 @@ export default class ParticipantController {

const connectionName = this._connectionController.getActiveConnectionName();

return this.writeChatMessageAsUser(
`${command ? `${command} ` : ''}${connectionName}`
) as Promise<boolean>;
return this.sendMessageToParticipant({
message: `${command ? `${command} ` : ''}${connectionName}`,
}) as Promise<boolean>;
}

getConnectionsTree(command: ParticipantCommand): vscode.MarkdownString[] {
Expand Down Expand Up @@ -501,7 +547,7 @@ export default class ParticipantController {
const dataService = this._connectionController.getActiveDataService();
if (!dataService) {
// Run a blank command to get the user to connect first.
void this.writeChatMessageAsUser(command);
void this.sendMessageToParticipant({ message: command });
return [];
}

Expand Down Expand Up @@ -549,9 +595,9 @@ export default class ParticipantController {
databaseName: databaseName,
});

return this.writeChatMessageAsUser(
`${command} ${databaseName}`
) as Promise<boolean>;
return this.sendMessageToParticipant({
message: `${command} ${databaseName}`,
}) as Promise<boolean>;
}

async getCollectionQuickPicks({
Expand All @@ -564,7 +610,7 @@ export default class ParticipantController {
const dataService = this._connectionController.getActiveDataService();
if (!dataService) {
// Run a blank command to get the user to connect first.
void this.writeChatMessageAsUser(command);
void this.sendMessageToParticipant({ message: command });
return [];
}

Expand Down Expand Up @@ -625,9 +671,9 @@ export default class ParticipantController {
databaseName: databaseName,
collectionName: collectionName,
});
return this.writeChatMessageAsUser(
`${command} ${collectionName}`
) as Promise<boolean>;
return this.sendMessageToParticipant({
message: `${command} ${collectionName}`,
}) as Promise<boolean>;
}

renderDatabasesTree({
Expand Down
13 changes: 13 additions & 0 deletions src/participant/participantTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type * as vscode from 'vscode';

export type SendMessageToParticipantOptions = {
message: string;
isNewChat?: boolean;
isPartialQuery?: boolean;
};

export type SendMessageToParticipantFromInputOptions = {
messagePrefix?: string;
source?: 'query with copilot codelens';
gagik marked this conversation as resolved.
Show resolved Hide resolved
} & Omit<SendMessageToParticipantOptions, 'message'> &
vscode.InputBoxOptions;
12 changes: 12 additions & 0 deletions src/telemetry/telemetryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ export type CopilotIntroductionProperties = {
is_copilot_active: boolean;
};

export type ParticipantOpenedFromInputBoxProperties = {
source?: 'query with copilot codelens';
gagik marked this conversation as resolved.
Show resolved Hide resolved
};

export function chatResultFeedbackKindToTelemetryValue(
kind: vscode.ChatResultFeedbackKind
): TelemetryFeedbackKind {
Expand Down Expand Up @@ -161,6 +165,7 @@ type TelemetryEventProperties =
| ParticipantFeedbackProperties
| ParticipantResponseFailedProperties
| ParticipantPromptProperties
| ParticipantOpenedFromInputBoxProperties
| ParticipantResponseProperties
| CopilotIntroductionProperties;

Expand All @@ -186,6 +191,7 @@ export enum TelemetryEventTypes {
PARTICIPANT_RESPONSE_FAILED = 'Participant Response Failed',
PARTICIPANT_PROMPT_SUBMITTED = 'Participant Prompt Submitted',
PARTICIPANT_RESPONSE_GENERATED = 'Participant Response Generated',
PARTICIPANT_SUBMITTED_FROM_INPUT_BOX = 'Participant Submitted From Input Box',
COPILOT_INTRODUCTION_CLICKED = 'Copilot Introduction Clicked',
COPILOT_INTRODUCTION_DISMISSED = 'Copilot Introduction Dismissed',
}
Expand Down Expand Up @@ -440,6 +446,12 @@ export default class TelemetryService {
this.track(TelemetryEventTypes.PARTICIPANT_FEEDBACK, props);
}

trackCopilotParticipantSubmittedFromInputBox(
props: ParticipantOpenedFromInputBoxProperties
): void {
this.track(TelemetryEventTypes.PARTICIPANT_SUBMITTED_FROM_INPUT_BOX, props);
}

trackCopilotParticipantError(err: any, command: string): void {
let errorCode: string | undefined;
let errorName: ParticipantErrorTypes;
Expand Down
Loading
Loading