From 7a456b45c490a79eacc2509f1ec3449fb7b20bfa Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 23 Sep 2024 17:35:15 +0200 Subject: [PATCH 01/12] feat(participant): export to a playground VSCODE-574 --- package.json | 25 +++++++++ src/commands/index.ts | 1 + src/editors/playgroundController.ts | 82 +++++++++++++++++++++++++++++ src/mdbExtensionController.ts | 14 +++-- 4 files changed, 117 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d1d102828..387a48c01 100644 --- a/package.json +++ b/package.json @@ -282,6 +282,10 @@ "dark": "images/dark/play.svg" } }, + { + "command": "mdb.runCodeInPlayground", + "title": "Run Code in Playground" + }, { "command": "mdb.exportToPython", "title": "MongoDB: Export To Python 3" @@ -747,6 +751,17 @@ "when": "mdb.isPlayground == true" } ], + "mdb.copilot": [ + { + "command": "mdb.runCodeInPlayground" + } + ], + "editor/context": [ + { + "submenu": "mdb.copilot", + "group": "1_main@2" + } + ], "commandPalette": [ { "command": "mdb.selectDatabaseWithParticipant", @@ -948,6 +963,10 @@ "command": "mdb.runPlayground", "when": "false" }, + { + "command": "mdb.runCodeInPlayground", + "when": "false" + }, { "command": "mdb.createIndexFromTreeView", "when": "false" @@ -994,6 +1013,12 @@ } ] }, + "submenus": [ + { + "id": "mdb.copilot", + "label": "MongoDB Copilot" + } + ], "keybindings": [ { "command": "mdb.runSelectedPlaygroundBlocks", diff --git a/src/commands/index.ts b/src/commands/index.ts index 767b99ffb..7e1517ad6 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -15,6 +15,7 @@ enum EXTENSION_COMMANDS { MDB_RUN_SELECTED_PLAYGROUND_BLOCKS = 'mdb.runSelectedPlaygroundBlocks', MDB_RUN_ALL_PLAYGROUND_BLOCKS = 'mdb.runAllPlaygroundBlocks', MDB_RUN_ALL_OR_SELECTED_PLAYGROUND_BLOCKS = 'mdb.runPlayground', + MDB_RUN_CODE_IN_PLAYGROUND = 'mdb.runCodeInPlayground', MDB_FIX_THIS_INVALID_INTERACTIVE_SYNTAX = 'mdb.fixThisInvalidInteractiveSyntax', MDB_FIX_ALL_INVALID_INTERACTIVE_SYNTAX = 'mdb.fixAllInvalidInteractiveSyntax', diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 505459191..f58097571 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -44,6 +44,7 @@ import { isPlayground, getPlaygroundExtensionForTelemetry, } from '../utils/playground'; +import type ParticipantController from '../participant/participant'; const log = createLogger('playground controller'); @@ -124,6 +125,7 @@ export default class PlaygroundController { _exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; _playgroundSelectedCodeActionProvider: PlaygroundSelectedCodeActionProvider; _telemetryService: TelemetryService; + _participantController?: ParticipantController; _isPartialRun = false; @@ -141,6 +143,7 @@ export default class PlaygroundController { playgroundResultViewProvider, exportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider, + participantController, }: { connectionController: ConnectionController; languageServerController: LanguageServerController; @@ -149,6 +152,7 @@ export default class PlaygroundController { playgroundResultViewProvider: PlaygroundResultProvider; exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; playgroundSelectedCodeActionProvider: PlaygroundSelectedCodeActionProvider; + participantController?: ParticipantController; }) { this._connectionController = connectionController; this._activeTextEditor = vscode.window.activeTextEditor; @@ -159,6 +163,7 @@ export default class PlaygroundController { this._exportToLanguageCodeLensProvider = exportToLanguageCodeLensProvider; this._playgroundSelectedCodeActionProvider = playgroundSelectedCodeActionProvider; + this._participantController = participantController; this._activeConnectionChangedHandler = () => { void this._activeConnectionChanged(); @@ -696,6 +701,83 @@ export default class PlaygroundController { return this._evaluatePlayground(codeToEvaluate); } + async runCodeInPlayground(): Promise { + const code = this._selectedText || this._getAllText(); + + if (!this._participantController?._participant) { + void vscode.window.showErrorMessage( + 'The MongoDB participant is not available.' + ); + + return Promise.resolve(false); + } + + try { + const progressResult = await vscode.window.withProgress( + { + location: ProgressLocation.Notification, + title: 'Exporting code to a playground...', + cancellable: true, + }, + async (progress, token) => { + token.onCancellationRequested(() => { + // If a user clicked the cancel button terminate all playground scripts. + this._languageServerController.cancelAll(); + + return { result: undefined }; + }); + + const abortController = new AbortController(); + token.onCancellationRequested(() => { + abortController.abort(); + }); + const messages = [ + // eslint-disable-next-line new-cap + vscode.LanguageModelChatMessage.Assistant( + 'You are MongoDB expert and can convert any proggramming language to the MongoDB Shell syntax. Take a user promt as an input string and translate it to the MongoDB Shell language.' + ), + // eslint-disable-next-line new-cap + vscode.LanguageModelChatMessage.User(code), + ]; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const util = require('util'); + console.log('messages----------------------'); + console.log(`${util.inspect(messages)}`); + console.log('----------------------'); + return await this._participantController?.getChatResponseContent({ + messages, + token, + }); + } + ); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const util = require('util'); + console.log('progressResult----------------------'); + console.log(`${util.inspect(progressResult)}`); + console.log('----------------------'); + + if (progressResult?.includes("Sorry, I can't assist with that.")) { + void vscode.window.showErrorMessage("Sorry, I can't assist with that."); + return Promise.resolve(false); + } + + if (progressResult) { + await this.createPlaygroundFromParticipantQuery({ + text: progressResult, + }); + } + + return Promise.resolve(true); + } catch (error) { + log.error( + 'Exporting code to a playground with cancel modal failed', + error + ); + return Promise.resolve(false); + } + } + async fixThisInvalidInteractiveSyntax({ documentUri, range, diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 7103183ee..e75456764 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -114,6 +114,11 @@ export default class MDBExtensionController implements vscode.Disposable { new PlaygroundSelectedCodeActionProvider(); this._playgroundDiagnosticsCodeActionProvider = new PlaygroundDiagnosticsCodeActionProvider(); + this._participantController = new ParticipantController({ + connectionController: this._connectionController, + storageController: this._storageController, + telemetryService: this._telemetryService, + }); this._playgroundController = new PlaygroundController({ connectionController: this._connectionController, languageServerController: this._languageServerController, @@ -123,11 +128,7 @@ export default class MDBExtensionController implements vscode.Disposable { exportToLanguageCodeLensProvider: this._exportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: this._playgroundSelectedCodeActionProvider, - }); - this._participantController = new ParticipantController({ - connectionController: this._connectionController, - storageController: this._storageController, - telemetryService: this._telemetryService, + participantController: this._participantController, }); this._editorsController = new EditorsController({ context, @@ -233,6 +234,9 @@ export default class MDBExtensionController implements vscode.Disposable { EXTENSION_COMMANDS.MDB_RUN_ALL_OR_SELECTED_PLAYGROUND_BLOCKS, () => this._playgroundController.runAllOrSelectedPlaygroundBlocks() ); + this.registerCommand(EXTENSION_COMMANDS.MDB_RUN_CODE_IN_PLAYGROUND, () => + this._playgroundController.runCodeInPlayground() + ); this.registerCommand( EXTENSION_COMMANDS.MDB_FIX_THIS_INVALID_INTERACTIVE_SYNTAX, From ebe0b8d5a0794134b14e7d727e46f23e2534b5f0 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 24 Sep 2024 13:07:35 +0200 Subject: [PATCH 02/12] refactor: move logic to participant controller --- src/editors/playgroundController.ts | 82 ------------------- src/mdbExtensionController.ts | 13 ++- src/participant/participant.ts | 73 +++++++++++++++++ src/participant/prompts/exportToPlayground.ts | 32 ++++++++ 4 files changed, 111 insertions(+), 89 deletions(-) create mode 100644 src/participant/prompts/exportToPlayground.ts diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index f58097571..505459191 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -44,7 +44,6 @@ import { isPlayground, getPlaygroundExtensionForTelemetry, } from '../utils/playground'; -import type ParticipantController from '../participant/participant'; const log = createLogger('playground controller'); @@ -125,7 +124,6 @@ export default class PlaygroundController { _exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; _playgroundSelectedCodeActionProvider: PlaygroundSelectedCodeActionProvider; _telemetryService: TelemetryService; - _participantController?: ParticipantController; _isPartialRun = false; @@ -143,7 +141,6 @@ export default class PlaygroundController { playgroundResultViewProvider, exportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider, - participantController, }: { connectionController: ConnectionController; languageServerController: LanguageServerController; @@ -152,7 +149,6 @@ export default class PlaygroundController { playgroundResultViewProvider: PlaygroundResultProvider; exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; playgroundSelectedCodeActionProvider: PlaygroundSelectedCodeActionProvider; - participantController?: ParticipantController; }) { this._connectionController = connectionController; this._activeTextEditor = vscode.window.activeTextEditor; @@ -163,7 +159,6 @@ export default class PlaygroundController { this._exportToLanguageCodeLensProvider = exportToLanguageCodeLensProvider; this._playgroundSelectedCodeActionProvider = playgroundSelectedCodeActionProvider; - this._participantController = participantController; this._activeConnectionChangedHandler = () => { void this._activeConnectionChanged(); @@ -701,83 +696,6 @@ export default class PlaygroundController { return this._evaluatePlayground(codeToEvaluate); } - async runCodeInPlayground(): Promise { - const code = this._selectedText || this._getAllText(); - - if (!this._participantController?._participant) { - void vscode.window.showErrorMessage( - 'The MongoDB participant is not available.' - ); - - return Promise.resolve(false); - } - - try { - const progressResult = await vscode.window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Exporting code to a playground...', - cancellable: true, - }, - async (progress, token) => { - token.onCancellationRequested(() => { - // If a user clicked the cancel button terminate all playground scripts. - this._languageServerController.cancelAll(); - - return { result: undefined }; - }); - - const abortController = new AbortController(); - token.onCancellationRequested(() => { - abortController.abort(); - }); - const messages = [ - // eslint-disable-next-line new-cap - vscode.LanguageModelChatMessage.Assistant( - 'You are MongoDB expert and can convert any proggramming language to the MongoDB Shell syntax. Take a user promt as an input string and translate it to the MongoDB Shell language.' - ), - // eslint-disable-next-line new-cap - vscode.LanguageModelChatMessage.User(code), - ]; - // eslint-disable-next-line @typescript-eslint/no-var-requires - const util = require('util'); - console.log('messages----------------------'); - console.log(`${util.inspect(messages)}`); - console.log('----------------------'); - return await this._participantController?.getChatResponseContent({ - messages, - token, - }); - } - ); - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const util = require('util'); - console.log('progressResult----------------------'); - console.log(`${util.inspect(progressResult)}`); - console.log('----------------------'); - - if (progressResult?.includes("Sorry, I can't assist with that.")) { - void vscode.window.showErrorMessage("Sorry, I can't assist with that."); - return Promise.resolve(false); - } - - if (progressResult) { - await this.createPlaygroundFromParticipantQuery({ - text: progressResult, - }); - } - - return Promise.resolve(true); - } catch (error) { - log.error( - 'Exporting code to a playground with cancel modal failed', - error - ); - return Promise.resolve(false); - } - } - async fixThisInvalidInteractiveSyntax({ documentUri, range, diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index e75456764..6ccca2653 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -114,11 +114,6 @@ export default class MDBExtensionController implements vscode.Disposable { new PlaygroundSelectedCodeActionProvider(); this._playgroundDiagnosticsCodeActionProvider = new PlaygroundDiagnosticsCodeActionProvider(); - this._participantController = new ParticipantController({ - connectionController: this._connectionController, - storageController: this._storageController, - telemetryService: this._telemetryService, - }); this._playgroundController = new PlaygroundController({ connectionController: this._connectionController, languageServerController: this._languageServerController, @@ -128,7 +123,11 @@ export default class MDBExtensionController implements vscode.Disposable { exportToLanguageCodeLensProvider: this._exportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: this._playgroundSelectedCodeActionProvider, - participantController: this._participantController, + }); + this._participantController = new ParticipantController({ + connectionController: this._connectionController, + storageController: this._storageController, + telemetryService: this._telemetryService, }); this._editorsController = new EditorsController({ context, @@ -235,7 +234,7 @@ export default class MDBExtensionController implements vscode.Disposable { () => this._playgroundController.runAllOrSelectedPlaygroundBlocks() ); this.registerCommand(EXTENSION_COMMANDS.MDB_RUN_CODE_IN_PLAYGROUND, () => - this._playgroundController.runCodeInPlayground() + this._participantController.runCodeInPlayground() ); this.registerCommand( diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 4098014e9..d6acf36c7 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -10,6 +10,7 @@ import EXTENSION_COMMANDS from '../commands'; import type { StorageController } from '../storage'; import { StorageVariables } from '../storage'; import { GenericPrompt, isPromptEmpty } from './prompts/generic'; +import { EportToPlaygroundPrompt } from './prompts/exportToPlayground'; import type { ChatResult } from './constants'; import { askToConnectChatResult, @@ -1147,6 +1148,78 @@ export default class ParticipantController { }); } + async runCodeInPlayground(): Promise { + const activeTextEditor = vscode.window.activeTextEditor; + if (!activeTextEditor) { + void vscode.window.showErrorMessage('Active editor not found.'); + return Promise.resolve(false); + } + + const sortedSelections = Array.from(activeTextEditor.selections).sort( + (a, b) => a.start.compareTo(b.start) + ); + const selectedText = sortedSelections + .map((selection) => activeTextEditor.document.getText(selection)) + .join('\n'); + + const code = + selectedText || activeTextEditor.document.getText().trim() || ''; + + if (!this._participant) { + void vscode.window.showErrorMessage( + 'The MongoDB participant is not available.' + ); + + return Promise.resolve(false); + } + + try { + const progressResult = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Exporting code to a playground...', + cancellable: true, + }, + async (progress, token) => { + const abortController = new AbortController(); + token.onCancellationRequested(() => { + abortController.abort(); + }); + const messages = EportToPlaygroundPrompt.buildMessages(code); + return await this.getChatResponseContent({ + messages, + token, + }); + } + ); + + if (progressResult?.includes("Sorry, I can't assist with that.")) { + void vscode.window.showErrorMessage("Sorry, I can't assist with that."); + return Promise.resolve(false); + } + + if (progressResult) { + const runnableContent = getRunnableContentFromString(progressResult); + if (progressResult) { + await vscode.commands.executeCommand( + EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, + { + runnableContent, + } + ); + } + } + + return Promise.resolve(true); + } catch (error) { + log.error( + 'Exporting code to a playground with cancel modal failed', + error + ); + return Promise.resolve(false); + } + } + async chatHandler( ...args: [ vscode.ChatRequest, diff --git a/src/participant/prompts/exportToPlayground.ts b/src/participant/prompts/exportToPlayground.ts new file mode 100644 index 000000000..427937bad --- /dev/null +++ b/src/participant/prompts/exportToPlayground.ts @@ -0,0 +1,32 @@ +import * as vscode from 'vscode'; + +export class EportToPlaygroundPrompt { + static getAssistantPrompt(): vscode.LanguageModelChatMessage { + const prompt = `You are a MongoDB expert. +Your task is to help the user build MongoDB queries and aggregation pipelines that perform their task. +You achieve this by converting user's code written in any proggramming language to the MongoDB Shell syntax. +Take a user promt as an input string and translate it to the MongoDB Shell language. +Keep your response concise. +You should suggest queries that are performant and correct. +Respond with markdown, suggest code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. +You can imagine the schema, collection, and database name. +Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax.`; + + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.Assistant(prompt); + } + + static getUserPrompt(prompt: string): vscode.LanguageModelChatMessage { + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.User(prompt); + } + + static buildMessages(prompt: string): vscode.LanguageModelChatMessage[] { + const messages = [ + EportToPlaygroundPrompt.getAssistantPrompt(), + EportToPlaygroundPrompt.getUserPrompt(prompt), + ]; + + return messages; + } +} From f23eb4a6c73b06eb5206336ef0d962a99224a27b Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 24 Sep 2024 15:18:46 +0200 Subject: [PATCH 03/12] test: add export to playground tests --- src/participant/participant.ts | 12 --- .../suite/participant/participant.test.ts | 101 ++++++++++++++++++ 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index d6acf36c7..732cc0e56 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1165,14 +1165,6 @@ export default class ParticipantController { const code = selectedText || activeTextEditor.document.getText().trim() || ''; - if (!this._participant) { - void vscode.window.showErrorMessage( - 'The MongoDB participant is not available.' - ); - - return Promise.resolve(false); - } - try { const progressResult = await vscode.window.withProgress( { @@ -1181,10 +1173,6 @@ export default class ParticipantController { cancellable: true, }, async (progress, token) => { - const abortController = new AbortController(); - token.onCancellationRequested(() => { - abortController.abort(); - }); const messages = EportToPlaygroundPrompt.buildMessages(code); return await this.getChatResponseContent({ messages, diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 53172b106..9151a6e2c 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -26,6 +26,8 @@ import { } from '../../../storage/storageController'; import type { LoadedConnection } from '../../../storage/connectionStorage'; import { ChatMetadataStore } from '../../../participant/chatMetadata'; +import { getFullRange } from '../suggestTestHelpers'; +import { isPlayground } from '../../../utils/playground'; // The Copilot's model in not available in tests, // therefore we need to mock its methods and returning values. @@ -1277,6 +1279,105 @@ Schema: expect(sendRequestStub).to.have.been.called; }); }); + + suite('export to playground', function () { + beforeEach(async function () { + await vscode.commands.executeCommand( + 'workbench.action.files.newUntitledFile' + ); + }); + + afterEach(async function () { + await vscode.commands.executeCommand( + 'workbench.action.closeActiveEditor' + ); + }); + + test('exports all code to a playground', async function () { + this.timeout(20000); + const editor = vscode.window.activeTextEditor; + if (!editor) { + throw new Error('Window active text editor is undefined'); + } + + const testDocumentUri = editor.document.uri; + const edit = new vscode.WorkspaceEdit(); + const code = ` + InsertOneResult result = collection.insertOne(new Document() + .append("_id", new ObjectId()) + .append("title", "Ski Bloopers") + .append("genres", Arrays.asList("Documentary", "Comedy"))); + System.out.println("Success! Inserted document id: " + result.getInsertedId()); +`; + edit.replace(testDocumentUri, getFullRange(editor.document), code); + await vscode.workspace.applyEdit(edit); + sendRequestStub.onCall(0).resolves({ + text: [ + '```javascript\n' + + 'db.collection.insertOne({\n' + + '_id: new ObjectId(),\n' + + 'title: "Ski Bloopers",' + + 'genres: ["Documentary", "Comedy"]' + + '});\n' + + 'print("Success! Inserted document id: " + result.insertedId);' + + '```', + ], + }); + await testParticipantController.runCodeInPlayground(); + const messages = sendRequestStub.firstCall.args[0]; + expect(messages[1].content).to.include('System.out.println'); + expect( + isPlayground(vscode.window.activeTextEditor?.document.uri) + ).to.be.eql(true); + expect(vscode.window.activeTextEditor?.document.getText()).to.include( + 'Inserted document id' + ); + }); + + test('exports selected lines of code to a playground', async function () { + const editor = vscode.window.activeTextEditor; + if (!editor) { + throw new Error('Window active text editor is undefined'); + } + + const testDocumentUri = editor.document.uri; + const edit = new vscode.WorkspaceEdit(); + const code = ` + InsertOneResult result = collection.insertOne(new Document() + .append("_id", new ObjectId()) + .append("title", "Ski Bloopers") + .append("genres", Arrays.asList("Documentary", "Comedy"))); + System.out.println("Success! Inserted document id: " + result.getInsertedId()); +`; + edit.replace(testDocumentUri, getFullRange(editor.document), code); + await vscode.workspace.applyEdit(edit); + const position = editor.selection.active; + const startPosition = position.with(0, 0); + const endPosition = position.with(3, 63); + const newSelection = new vscode.Selection(startPosition, endPosition); + editor.selection = newSelection; + sendRequestStub.onCall(0).resolves({ + text: [ + '```javascript\n' + + 'db.collection.insertOne({\n' + + '_id: new ObjectId(),\n' + + 'title: "Ski Bloopers",' + + 'genres: ["Documentary", "Comedy"]' + + '});\n' + + '```', + ], + }); + await testParticipantController.runCodeInPlayground(); + const messages = sendRequestStub.firstCall.args[0]; + expect(messages[1].content).to.not.include('System.out.println'); + expect( + isPlayground(vscode.window.activeTextEditor?.document.uri) + ).to.be.eql(true); + expect( + vscode.window.activeTextEditor?.document.getText() + ).to.not.include('Inserted document id'); + }); + }); }); }); From da0cd948b3d895d5bfe3c120138ff4a5ebcc09eb Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 24 Sep 2024 15:23:46 +0200 Subject: [PATCH 04/12] refactor: rename --- package.json | 6 +++--- src/commands/index.ts | 2 +- src/mdbExtensionController.ts | 4 ++-- src/participant/participant.ts | 2 +- src/test/suite/participant/participant.test.ts | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 387a48c01..9d9f2940b 100644 --- a/package.json +++ b/package.json @@ -283,7 +283,7 @@ } }, { - "command": "mdb.runCodeInPlayground", + "command": "mdb.exportCodeToPlayground", "title": "Run Code in Playground" }, { @@ -753,7 +753,7 @@ ], "mdb.copilot": [ { - "command": "mdb.runCodeInPlayground" + "command": "mdb.exportCodeToPlayground" } ], "editor/context": [ @@ -964,7 +964,7 @@ "when": "false" }, { - "command": "mdb.runCodeInPlayground", + "command": "mdb.exportCodeToPlayground", "when": "false" }, { diff --git a/src/commands/index.ts b/src/commands/index.ts index 7e1517ad6..23e01f782 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -15,7 +15,7 @@ enum EXTENSION_COMMANDS { MDB_RUN_SELECTED_PLAYGROUND_BLOCKS = 'mdb.runSelectedPlaygroundBlocks', MDB_RUN_ALL_PLAYGROUND_BLOCKS = 'mdb.runAllPlaygroundBlocks', MDB_RUN_ALL_OR_SELECTED_PLAYGROUND_BLOCKS = 'mdb.runPlayground', - MDB_RUN_CODE_IN_PLAYGROUND = 'mdb.runCodeInPlayground', + MDB_EXPORT_CODE_TO_PLAYGROUND = 'mdb.exportCodeToPlayground', MDB_FIX_THIS_INVALID_INTERACTIVE_SYNTAX = 'mdb.fixThisInvalidInteractiveSyntax', MDB_FIX_ALL_INVALID_INTERACTIVE_SYNTAX = 'mdb.fixAllInvalidInteractiveSyntax', diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 6ccca2653..ed705dd9c 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -233,8 +233,8 @@ export default class MDBExtensionController implements vscode.Disposable { EXTENSION_COMMANDS.MDB_RUN_ALL_OR_SELECTED_PLAYGROUND_BLOCKS, () => this._playgroundController.runAllOrSelectedPlaygroundBlocks() ); - this.registerCommand(EXTENSION_COMMANDS.MDB_RUN_CODE_IN_PLAYGROUND, () => - this._participantController.runCodeInPlayground() + this.registerCommand(EXTENSION_COMMANDS.MDB_EXPORT_CODE_TO_PLAYGROUND, () => + this._participantController.exportCodeToPlayground() ); this.registerCommand( diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 732cc0e56..8321bc314 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1148,7 +1148,7 @@ export default class ParticipantController { }); } - async runCodeInPlayground(): Promise { + async exportCodeToPlayground(): Promise { const activeTextEditor = vscode.window.activeTextEditor; if (!activeTextEditor) { void vscode.window.showErrorMessage('Active editor not found.'); diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 9151a6e2c..ad6dff20e 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1323,7 +1323,7 @@ Schema: '```', ], }); - await testParticipantController.runCodeInPlayground(); + await testParticipantController.exportCodeToPlayground(); const messages = sendRequestStub.firstCall.args[0]; expect(messages[1].content).to.include('System.out.println'); expect( @@ -1367,7 +1367,7 @@ Schema: '```', ], }); - await testParticipantController.runCodeInPlayground(); + await testParticipantController.exportCodeToPlayground(); const messages = sendRequestStub.firstCall.args[0]; expect(messages[1].content).to.not.include('System.out.println'); expect( From e2692deb32f88c0267d0ef559bd4aed98504bc61 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 24 Sep 2024 15:31:39 +0200 Subject: [PATCH 05/12] refactor: rename more --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9d9f2940b..8a0e71fef 100644 --- a/package.json +++ b/package.json @@ -284,7 +284,7 @@ }, { "command": "mdb.exportCodeToPlayground", - "title": "Run Code in Playground" + "title": "Export Code to Playground" }, { "command": "mdb.exportToPython", From ba5fdb682a9987e6c9ee9108c5a7350de69e8edc Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 24 Sep 2024 17:29:23 +0200 Subject: [PATCH 06/12] fix: return promise on cancelation progress --- src/editors/playgroundController.ts | 34 ++++++------- src/participant/participant.ts | 49 ++++++++++++------- src/participant/prompts/exportToPlayground.ts | 9 ++-- .../suite/participant/participant.test.ts | 13 ++--- src/test/suite/playground.test.ts | 2 +- 5 files changed, 57 insertions(+), 50 deletions(-) diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 505459191..0676e4aeb 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -492,33 +492,33 @@ export default class PlaygroundController { } try { - const progressResult = await vscode.window.withProgress( + return await vscode.window.withProgress( { location: ProgressLocation.Notification, title: 'Running MongoDB playground...', cancellable: true, }, - async (progress, token) => { - token.onCancellationRequested(() => { - // If a user clicked the cancel button terminate all playground scripts. - this._languageServerController.cancelAll(); - - return { result: undefined }; + async (progress, token): Promise => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + return new Promise(async (resolve, reject) => { + token.onCancellationRequested(() => { + // If a user clicked the cancel button terminate all playground scripts. + this._languageServerController.cancelAll(); + return resolve({ result: undefined }); + }); + + try { + // Run all playground scripts. + const result = await this._evaluate(codeToEvaluate); + return resolve(result); + } catch (error) { + return reject(error); + } }); - - // Run all playground scripts. - const result: ShellEvaluateResult = await this._evaluate( - codeToEvaluate - ); - - return result; } ); - - return progressResult; } catch (error) { log.error('Evaluating playground with cancel modal failed', error); - return { result: undefined }; } } diff --git a/src/participant/participant.ts b/src/participant/participant.ts index aa7261fdf..8c2c4e35b 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -10,7 +10,7 @@ import EXTENSION_COMMANDS from '../commands'; import type { StorageController } from '../storage'; import { StorageVariables } from '../storage'; import { GenericPrompt, isPromptEmpty } from './prompts/generic'; -import { EportToPlaygroundPrompt } from './prompts/exportToPlayground'; +import { ExportToPlaygroundPrompt } from './prompts/exportToPlayground'; import type { ChatResult } from './constants'; import { askToConnectChatResult, @@ -43,6 +43,7 @@ import { } from '../telemetry/telemetryService'; import { DocsChatbotAIService } from './docsChatbotAIService'; import type TelemetryService from '../telemetry/telemetryService'; +import formatError from '../utils/formatError'; const log = createLogger('participant'); @@ -133,7 +134,7 @@ export default class ParticipantController { return this._participant; } - handleError(err: any, command: string): never { + handleError(err: any, command: string): void { let errorCode: string | undefined; let errorName: ParticipantErrorTypes; // Making the chat request might fail because @@ -169,9 +170,6 @@ export default class ParticipantController { error_name: errorName, } ); - - // Re-throw other errors so they show up in the UI. - throw err; } /** @@ -1205,7 +1203,7 @@ export default class ParticipantController { async exportCodeToPlayground(): Promise { const activeTextEditor = vscode.window.activeTextEditor; if (!activeTextEditor) { - void vscode.window.showErrorMessage('Active editor not found.'); + await vscode.window.showErrorMessage('Active editor not found.'); return Promise.resolve(false); } @@ -1215,10 +1213,8 @@ export default class ParticipantController { const selectedText = sortedSelections .map((selection) => activeTextEditor.document.getText(selection)) .join('\n'); - const code = selectedText || activeTextEditor.document.getText().trim() || ''; - try { const progressResult = await vscode.window.withProgress( { @@ -1226,11 +1222,26 @@ export default class ParticipantController { title: 'Exporting code to a playground...', cancellable: true, }, - async (progress, token) => { - const messages = EportToPlaygroundPrompt.buildMessages(code); - return await this.getChatResponseContent({ - messages, - token, + async (progress, token): Promise => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + return new Promise(async (resolve, reject) => { + token.onCancellationRequested(() => { + void vscode.window.showInformationMessage( + 'The running export to a playground operation was canceled.' + ); + return reject(); + }); + + const messages = ExportToPlaygroundPrompt.buildMessages(code); + try { + const responseContent = await this.getChatResponseContent({ + messages, + token, + }); + return resolve(responseContent); + } catch (error) { + return reject(error); + } }); } ); @@ -1242,7 +1253,7 @@ export default class ParticipantController { if (progressResult) { const runnableContent = getRunnableContentFromString(progressResult); - if (progressResult) { + if (runnableContent) { await vscode.commands.executeCommand( EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, { @@ -1254,9 +1265,11 @@ export default class ParticipantController { return Promise.resolve(true); } catch (error) { - log.error( - 'Exporting code to a playground with cancel modal failed', - error + this.handleError(error, 'exportToPlayground'); + await vscode.window.showErrorMessage( + `An error occurred exporting to a playground: ${ + formatError(error).message + }` ); return Promise.resolve(false); } @@ -1310,6 +1323,8 @@ Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more i } } catch (e) { this.handleError(e, request.command || 'generic'); + // Re-throw other errors so they show up in the UI. + throw e; } } diff --git a/src/participant/prompts/exportToPlayground.ts b/src/participant/prompts/exportToPlayground.ts index 427937bad..769dbafbc 100644 --- a/src/participant/prompts/exportToPlayground.ts +++ b/src/participant/prompts/exportToPlayground.ts @@ -1,15 +1,14 @@ import * as vscode from 'vscode'; -export class EportToPlaygroundPrompt { +export class ExportToPlaygroundPrompt { static getAssistantPrompt(): vscode.LanguageModelChatMessage { const prompt = `You are a MongoDB expert. Your task is to help the user build MongoDB queries and aggregation pipelines that perform their task. -You achieve this by converting user's code written in any proggramming language to the MongoDB Shell syntax. +You convert user's code written in any programming language to the MongoDB Shell syntax. Take a user promt as an input string and translate it to the MongoDB Shell language. Keep your response concise. You should suggest queries that are performant and correct. Respond with markdown, suggest code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. -You can imagine the schema, collection, and database name. Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax.`; // eslint-disable-next-line new-cap @@ -23,8 +22,8 @@ Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax.`; static buildMessages(prompt: string): vscode.LanguageModelChatMessage[] { const messages = [ - EportToPlaygroundPrompt.getAssistantPrompt(), - EportToPlaygroundPrompt.getUserPrompt(prompt), + ExportToPlaygroundPrompt.getAssistantPrompt(), + ExportToPlaygroundPrompt.getUserPrompt(prompt), ]; return messages; diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index ec82942d0..054aa1e97 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1296,7 +1296,6 @@ Schema: }); test('exports all code to a playground', async function () { - this.timeout(20000); const editor = vscode.window.activeTextEditor; if (!editor) { throw new Error('Window active text editor is undefined'); @@ -1441,9 +1440,7 @@ Schema: test('reports error', function () { const err = Error('Filtered by Responsible AI Service'); - expect(() => testParticipantController.handleError(err, 'query')).throws( - 'Filtered by Responsible AI Service' - ); + testParticipantController.handleError(err, 'query'); sinon.assert.calledOnce(telemetryTrackStub); expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( @@ -1461,9 +1458,7 @@ Schema: test('reports nested error', function () { const err = new Error('Parent error'); err.cause = Error('This message is flagged as off topic: off_topic.'); - expect(() => testParticipantController.handleError(err, 'docs')).throws( - 'off_topic' - ); + testParticipantController.handleError(err, 'docs'); sinon.assert.calledOnce(telemetryTrackStub); expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( @@ -1479,9 +1474,7 @@ Schema: test('Reports error code when available', function () { // eslint-disable-next-line new-cap const err = vscode.LanguageModelError.NotFound('Model not found'); - expect(() => testParticipantController.handleError(err, 'schema')).throws( - 'Model not found' - ); + testParticipantController.handleError(err, 'schema'); sinon.assert.calledOnce(telemetryTrackStub); expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( diff --git a/src/test/suite/playground.test.ts b/src/test/suite/playground.test.ts index db142ee88..8733d28b6 100644 --- a/src/test/suite/playground.test.ts +++ b/src/test/suite/playground.test.ts @@ -87,7 +87,7 @@ suite('Playground', function () { .update('confirmRunAll', false); }); - afterEach(async () => { + afterEach(async function () { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); disposeAll(_disposables); sandbox.restore(); From 94d88c03e5fd4a3830e43562550258698661ef3e Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 24 Sep 2024 17:50:07 +0200 Subject: [PATCH 07/12] test: increase waiting time of selection to fix ubuntu ci --- src/test/suite/suggestTestHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/suite/suggestTestHelpers.ts b/src/test/suite/suggestTestHelpers.ts index 998715525..4a2336358 100644 --- a/src/test/suite/suggestTestHelpers.ts +++ b/src/test/suite/suggestTestHelpers.ts @@ -65,7 +65,7 @@ export function acceptFirstSuggestion( _disposables, async () => { await vscode.commands.executeCommand('editor.action.triggerSuggest'); - await wait(1000); + await wait(3000); await vscode.commands.executeCommand('acceptSelectedSuggestion'); } ); From 55d634efcd5bce623483bb2933c53da9103f1496 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 24 Sep 2024 21:15:53 +0200 Subject: [PATCH 08/12] refactor: address pr comments --- src/editors/playgroundController.ts | 28 +++++++------- src/participant/participant.ts | 38 +++++++++---------- src/participant/prompts/exportToPlayground.ts | 2 +- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 0676e4aeb..6a618688f 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -499,22 +499,20 @@ export default class PlaygroundController { cancellable: true, }, async (progress, token): Promise => { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - return new Promise(async (resolve, reject) => { - token.onCancellationRequested(() => { - // If a user clicked the cancel button terminate all playground scripts. - this._languageServerController.cancelAll(); - return resolve({ result: undefined }); - }); - - try { - // Run all playground scripts. - const result = await this._evaluate(codeToEvaluate); - return resolve(result); - } catch (error) { - return reject(error); - } + token.onCancellationRequested(() => { + // If the user clicks the cancel button, + // terminate all processes running on the language server. + this._languageServerController.cancelAll(); }); + + return Promise.race([ + this._evaluate(codeToEvaluate), + new Promise<{ result: undefined }>((resolve) => + token.onCancellationRequested(() => { + resolve({ result: undefined }); + }) + ), + ]); } ); } catch (error) { diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 8c2c4e35b..e0a5811ff 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1204,7 +1204,7 @@ export default class ParticipantController { const activeTextEditor = vscode.window.activeTextEditor; if (!activeTextEditor) { await vscode.window.showErrorMessage('Active editor not found.'); - return Promise.resolve(false); + return false; } const sortedSelections = Array.from(activeTextEditor.selections).sort( @@ -1223,26 +1223,24 @@ export default class ParticipantController { cancellable: true, }, async (progress, token): Promise => { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - return new Promise(async (resolve, reject) => { - token.onCancellationRequested(() => { - void vscode.window.showInformationMessage( - 'The running export to a playground operation was canceled.' - ); - return reject(); - }); - - const messages = ExportToPlaygroundPrompt.buildMessages(code); - try { - const responseContent = await this.getChatResponseContent({ - messages, - token, - }); - return resolve(responseContent); - } catch (error) { - return reject(error); - } + token.onCancellationRequested(async () => { + await vscode.window.showInformationMessage( + 'The running export to a playground operation was canceled.' + ); }); + + const messages = ExportToPlaygroundPrompt.buildMessages(code); + return Promise.race([ + this.getChatResponseContent({ + messages, + token, + }), + new Promise((resolve) => + token.onCancellationRequested(() => { + resolve(undefined); + }) + ), + ]); } ); diff --git a/src/participant/prompts/exportToPlayground.ts b/src/participant/prompts/exportToPlayground.ts index 769dbafbc..61ba82bd2 100644 --- a/src/participant/prompts/exportToPlayground.ts +++ b/src/participant/prompts/exportToPlayground.ts @@ -5,7 +5,7 @@ export class ExportToPlaygroundPrompt { const prompt = `You are a MongoDB expert. Your task is to help the user build MongoDB queries and aggregation pipelines that perform their task. You convert user's code written in any programming language to the MongoDB Shell syntax. -Take a user promt as an input string and translate it to the MongoDB Shell language. +Take a user prompt as an input string and translate it to the MongoDB Shell language. Keep your response concise. You should suggest queries that are performant and correct. Respond with markdown, suggest code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. From 49f330a8399666d74ffd09c602ddbfeca5ce4962 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 25 Sep 2024 14:29:16 +0200 Subject: [PATCH 09/12] refactor: clean up code around evaluate and cancelation tokens --- src/editors/playgroundController.ts | 61 +++++------- src/language/languageServerController.ts | 15 +-- src/language/mongoDBService.ts | 97 +++++++++++++------ src/language/worker.ts | 13 ++- src/participant/participant.ts | 6 +- src/participant/prompts/exportToPlayground.ts | 4 +- src/telemetry/telemetryService.ts | 12 ++- .../editors/playgroundController.test.ts | 4 +- .../suite/language/mongoDBService.test.ts | 4 +- src/types/playgroundType.ts | 9 +- 10 files changed, 124 insertions(+), 101 deletions(-) diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 6a618688f..6ed98563c 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -62,7 +62,7 @@ let dummySandbox; // TODO: this function was copied from the compass-export-to-language module // https://github.com/mongodb-js/compass/blob/7c4bc0789a7b66c01bb7ba63955b3b11ed40c094/packages/compass-export-to-language/src/modules/count-aggregation-stages-in-string.js // and should be updated as well when the better solution for the problem will be found. -const countAggregationStagesInString = (str: string) => { +const countAggregationStagesInString = (str: string): number => { if (!dummySandbox) { dummySandbox = vm.createContext(Object.create(null), { codeGeneration: { strings: false, wasm: false }, @@ -160,7 +160,7 @@ export default class PlaygroundController { this._playgroundSelectedCodeActionProvider = playgroundSelectedCodeActionProvider; - this._activeConnectionChangedHandler = () => { + this._activeConnectionChangedHandler = (): void => { void this._activeConnectionChanged(); }; this._connectionController.addEventListener( @@ -170,7 +170,7 @@ export default class PlaygroundController { const onDidChangeActiveTextEditor = ( editor: vscode.TextEditor | undefined - ) => { + ): void => { if (editor?.document.uri.scheme === PLAYGROUND_RESULT_SCHEME) { this._playgroundResultViewColumn = editor.viewColumn; this._playgroundResultTextDocument = editor?.document; @@ -438,7 +438,10 @@ export default class PlaygroundController { return this._createPlaygroundFileWithContent(content); } - async _evaluate(codeToEvaluate: string): Promise { + async _evaluate( + codeToEvaluate: string, + token?: vscode.CancellationToken + ): Promise { const connectionId = this._connectionController.getActiveConnectionId(); if (!connectionId) { @@ -449,13 +452,14 @@ export default class PlaygroundController { this._statusView.showMessage('Getting results...'); - let result: ShellEvaluateResult; + let result: ShellEvaluateResult = null; try { // Send a request to the language server to execute scripts from a playground. result = await this._languageServerController.evaluate({ codeToEvaluate, connectionId, filePath: vscode.window.activeTextEditor?.document.uri.fsPath, + token, }); } catch (error) { const msg = @@ -491,34 +495,21 @@ export default class PlaygroundController { ); } - try { - return await vscode.window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Running MongoDB playground...', - cancellable: true, - }, - async (progress, token): Promise => { - token.onCancellationRequested(() => { - // If the user clicks the cancel button, - // terminate all processes running on the language server. - this._languageServerController.cancelAll(); - }); - - return Promise.race([ - this._evaluate(codeToEvaluate), - new Promise<{ result: undefined }>((resolve) => - token.onCancellationRequested(() => { - resolve({ result: undefined }); - }) - ), - ]); - } - ); - } catch (error) { - log.error('Evaluating playground with cancel modal failed', error); - return { result: undefined }; - } + return await vscode.window.withProgress( + { + location: ProgressLocation.Notification, + title: 'Running MongoDB playground...', + cancellable: true, + }, + async (progress, token): Promise => { + token.onCancellationRequested(() => { + // If the user clicks the cancel button, + // terminate all processes running on the language server. + this._languageServerController.cancelAll(); + }); + return this._evaluate(codeToEvaluate, token); + } + ); } async _openInResultPane(result: PlaygroundResult): Promise { @@ -698,7 +689,7 @@ export default class PlaygroundController { documentUri, range, fix, - }: ThisDiagnosticFix) { + }: ThisDiagnosticFix): Promise { const edit = new vscode.WorkspaceEdit(); edit.replace(documentUri, range, fix); await vscode.workspace.applyEdit(edit); @@ -708,7 +699,7 @@ export default class PlaygroundController { async fixAllInvalidInteractiveSyntax({ documentUri, diagnostics, - }: AllDiagnosticFixes) { + }: AllDiagnosticFixes): Promise { const edit = new vscode.WorkspaceEdit(); for (const { range, fix } of diagnostics) { diff --git a/src/language/languageServerController.ts b/src/language/languageServerController.ts index 13c579e09..8912cf2bf 100644 --- a/src/language/languageServerController.ts +++ b/src/language/languageServerController.ts @@ -5,11 +5,7 @@ import type { LanguageClientOptions, ServerOptions, } from 'vscode-languageclient/node'; -import { - LanguageClient, - TransportKind, - CancellationTokenSource, -} from 'vscode-languageclient/node'; +import { LanguageClient, TransportKind } from 'vscode-languageclient/node'; import type { ExtensionContext } from 'vscode'; import { workspace } from 'vscode'; import util from 'util'; @@ -32,7 +28,6 @@ const log = createLogger('language server controller'); */ export default class LanguageServerController { _context: ExtensionContext; - _source?: CancellationTokenSource; _isExecutingInProgress = false; _client: LanguageClient; _currentConnectionId: string | null = null; @@ -190,20 +185,15 @@ export default class LanguageServerController { inputLength: playgroundExecuteParameters.codeToEvaluate.length, }); this._isExecutingInProgress = true; - this._consoleOutputChannel.clear(); - // Instantiate a new CancellationTokenSource object - // that generates a cancellation token for each run of a playground. - this._source = new CancellationTokenSource(); - // Send a request with a cancellation token // to the language server instance to execute scripts from a playground // and return results to the playground controller when ready. const res: ShellEvaluateResult = await this._client.sendRequest( ServerCommands.EXECUTE_CODE_FROM_PLAYGROUND, playgroundExecuteParameters, - this._source.token + playgroundExecuteParameters.token ); this._isExecutingInProgress = false; @@ -279,7 +269,6 @@ export default class LanguageServerController { // the onCancellationRequested event will be fired, // and IsCancellationRequested will return true. if (this._isExecutingInProgress) { - this._source?.cancel(); this._isExecutingInProgress = false; } } diff --git a/src/language/mongoDBService.ts b/src/language/mongoDBService.ts index 09d4c942d..05ea14a8e 100644 --- a/src/language/mongoDBService.ts +++ b/src/language/mongoDBService.ts @@ -192,7 +192,7 @@ export default class MongoDBService { }; } - async _getAndCacheDatabases() { + async _getAndCacheDatabases(): Promise { try { // Get database names for the current connection. const databases = await this._getDatabases(); @@ -205,7 +205,7 @@ export default class MongoDBService { } } - async _getAndCacheStreamProcessors() { + async _getAndCacheStreamProcessors(): Promise { try { const processors = await this._getStreamProcessors(); this._cacheStreamProcessorCompletionItems(processors); @@ -222,7 +222,7 @@ export default class MongoDBService { async evaluate( params: PlaygroundEvaluateParams, token: CancellationToken - ): Promise { + ): Promise { this.clearCachedFields(); return new Promise((resolve) => { @@ -231,14 +231,14 @@ export default class MongoDBService { ServerCommands.SHOW_ERROR_MESSAGE, "The playground's active connection does not match the extension's active connection. Please reconnect and try again." ); - return resolve(undefined); + return resolve(null); } if (!this._extensionPath) { this._connection.console.error( 'LS evaluate: extensionPath is undefined' ); - return resolve(undefined); + return resolve(null); } try { @@ -274,7 +274,7 @@ export default class MongoDBService { if (name === ServerCommands.CODE_EXECUTION_RESULT) { const { error, data } = payload as { - data?: ShellEvaluateResult; + data: ShellEvaluateResult | null; error?: any; }; if (error) { @@ -313,13 +313,13 @@ export default class MongoDBService { // Stop the worker and all JavaScript execution // in the worker thread as soon as possible. await worker.terminate(); - return resolve(undefined); + return resolve(null); }); } catch (error) { this._connection.console.error( `LS evaluate error: ${util.inspect(error)}` ); - return resolve(undefined); + return resolve(null); } }); } @@ -427,7 +427,7 @@ export default class MongoDBService { /** * Return 'db', 'sp' and 'use' completion items. */ - _cacheGlobalSymbolCompletionItems() { + _cacheGlobalSymbolCompletionItems(): void { this._globalSymbolCompletionItems = [ { label: 'db', @@ -452,7 +452,7 @@ export default class MongoDBService { /** * Create and cache Shell symbols completion items. */ - _cacheShellSymbolCompletionItems() { + _cacheShellSymbolCompletionItems(): void { const shellSymbolCompletionItems = {}; Object.keys(signatures).map((symbol) => { @@ -567,7 +567,10 @@ export default class MongoDBService { }: { operator: string; description?: string; - }) { + }): { + kind: typeof MarkupKind.Markdown; + value: string; + } { const title = operator.replace(/[$]/g, ''); const link = LINKS.aggregationDocs(title); return { @@ -584,7 +587,10 @@ export default class MongoDBService { }: { bsonType: string; description?: string; - }) { + }): { + kind: typeof MarkupKind.Markdown; + value: string; + } { const link = LINKS.bsonDocs(bsonType); return { kind: MarkupKind.Markdown, @@ -600,7 +606,10 @@ export default class MongoDBService { }: { variable: string; description?: string; - }) { + }): { + kind: typeof MarkupKind.Markdown; + value: string; + } { const title = variable.replace(/[$]/g, ''); const link = LINKS.systemVariableDocs(title); return { @@ -617,7 +626,7 @@ export default class MongoDBService { async _getCompletionValuesAndUpdateCache( currentDatabaseName: string | null, currentCollectionName: string | null - ) { + ): Promise { if (currentDatabaseName && !this._collections[currentDatabaseName]) { // Get collection names for the current database. const collections = await this._getCollections(currentDatabaseName); @@ -645,7 +654,9 @@ export default class MongoDBService { /** * If the current node is 'db.collection.aggregate([{}])'. */ - _provideStageCompletionItems(state: CompletionState) { + _provideStageCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isStage) { this._connection.console.log('VISITOR found stage operator completions'); @@ -678,7 +689,9 @@ export default class MongoDBService { * we check a playground text before the current cursor position. * If we found 'use("db")' or 'db.collection' we also suggest field names. */ - _provideQueryOperatorCompletionItems(state: CompletionState) { + _provideQueryOperatorCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if ( state.stageOperator === MATCH || (state.stageOperator === null && state.isObjectKey) @@ -718,7 +731,9 @@ export default class MongoDBService { * we check a playground text before the current cursor position. * If we found 'use("db")' or 'db.collection' we also suggest field names. */ - _provideAggregationOperatorCompletionItems(state: CompletionState) { + _provideAggregationOperatorCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.stageOperator) { const fields = this._fields[`${state.databaseName}.${state.collectionName}`] || []; @@ -766,7 +781,9 @@ export default class MongoDBService { /** * If the current node is 'db.collection.find({ _id: });'. */ - _provideIdentifierObjectValueCompletionItems(state: CompletionState) { + _provideIdentifierObjectValueCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isIdentifierObjectValue) { this._connection.console.log('VISITOR found bson completions'); return getFilteredCompletions({ meta: ['bson'] }).map((item) => { @@ -795,7 +812,9 @@ export default class MongoDBService { /** * If the current node is 'db.collection.find({ field: "" });'. */ - _provideTextObjectValueCompletionItems(state: CompletionState) { + _provideTextObjectValueCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isTextObjectValue) { const fields = this._fields[`${state.databaseName}.${state.collectionName}`]; @@ -827,7 +846,9 @@ export default class MongoDBService { /** * If the current node is 'db.collection.' or 'db["test"].'. */ - _provideCollectionSymbolCompletionItems(state: CompletionState) { + _provideCollectionSymbolCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isCollectionSymbol) { this._connection.console.log( 'VISITOR found collection symbol completions' @@ -839,7 +860,9 @@ export default class MongoDBService { /** * If the current node is 'sp.processor.' or 'sp["processor"].'. */ - _provideStreamProcessorSymbolCompletionItems(state: CompletionState) { + _provideStreamProcessorSymbolCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isStreamProcessorSymbol) { this._connection.console.log( 'VISITOR found stream processor symbol completions' @@ -851,7 +874,9 @@ export default class MongoDBService { /** * If the current node is 'db.collection.find().'. */ - _provideFindCursorCompletionItems(state: CompletionState) { + _provideFindCursorCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isFindCursor) { this._connection.console.log('VISITOR found find cursor completions'); return this._shellSymbolCompletionItems.Cursor; @@ -861,7 +886,9 @@ export default class MongoDBService { /** * If the current node is 'db.collection.aggregate().'. */ - _provideAggregationCursorCompletionItems(state: CompletionState) { + _provideAggregationCursorCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isAggregationCursor) { this._connection.console.log( 'VISITOR found aggregation cursor completions' @@ -873,7 +900,9 @@ export default class MongoDBService { /** * If the current node is 'db' or 'use'. */ - _provideGlobalSymbolCompletionItems(state: CompletionState) { + _provideGlobalSymbolCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isGlobalSymbol) { this._connection.console.log('VISITOR found global symbol completions'); return this._globalSymbolCompletionItems; @@ -894,7 +923,7 @@ export default class MongoDBService { databaseName: string; currentLineText: string; position: { line: number; character: number }; - }) { + }): CompletionItem[] { return this._collections[databaseName].map((collectionName) => { if (this._isValidPropertyName(collectionName)) { return { @@ -936,7 +965,7 @@ export default class MongoDBService { state: CompletionState, currentLineText: string, position: { line: number; character: number } - ) { + ): CompletionItem[] | undefined { // If we found 'use("db")' and the current node is 'db.'. if (state.isDbSymbol && state.databaseName) { this._connection.console.log( @@ -963,7 +992,9 @@ export default class MongoDBService { /** * If the current node is 'sp.'. */ - _provideSpSymbolCompletionItems(state: CompletionState) { + _provideSpSymbolCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isSpSymbol) { if (state.isStreamProcessorName) { this._connection.console.log( @@ -982,7 +1013,9 @@ export default class MongoDBService { /** * If the current node is 'sp.get()'. */ - _provideStreamProcessorNameCompletionItems(state: CompletionState) { + _provideStreamProcessorNameCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isStreamProcessorName) { this._connection.console.log( 'VISITOR found stream processor name completions' @@ -999,7 +1032,7 @@ export default class MongoDBService { state: CompletionState, currentLineText: string, position: { line: number; character: number } - ) { + ): CompletionItem[] | undefined { if (state.isCollectionName && state.databaseName) { this._connection.console.log('VISITOR found collection name completions'); return this._getCollectionCompletionItems({ @@ -1013,7 +1046,9 @@ export default class MongoDBService { /** * If the current node is 'use(""")'. */ - _provideDbNameCompletionItems(state: CompletionState) { + _provideDbNameCompletionItems( + state: CompletionState + ): CompletionItem[] | undefined { if (state.isUseCallExpression) { this._connection.console.log('VISITOR found database names completion'); return this._databaseCompletionItems; @@ -1094,7 +1129,7 @@ export default class MongoDBService { } // Highlight the usage of commands that only works inside interactive session. - provideDiagnostics(textFromEditor: string) { + provideDiagnostics(textFromEditor: string): Diagnostic[] { const lines = textFromEditor.split(/\r?\n/g); const diagnostics: Diagnostic[] = []; const invalidInteractiveSyntaxes = [ diff --git a/src/language/worker.ts b/src/language/worker.ts index 3a1d1605d..229b43ee3 100644 --- a/src/language/worker.ts +++ b/src/language/worker.ts @@ -2,6 +2,7 @@ import { CliServiceProvider } from '@mongosh/service-provider-server'; import { ElectronRuntime } from '@mongosh/browser-runtime-electron'; import { parentPort } from 'worker_threads'; import { ServerCommands } from './serverCommands'; +import type { Document } from 'bson'; import type { ShellEvaluateResult, @@ -16,7 +17,7 @@ interface EvaluationResult { type: string | null; } -const getContent = ({ type, printable }: EvaluationResult) => { +const getContent = ({ type, printable }: EvaluationResult): Document => { if (type === 'Cursor' || type === 'AggregationCursor') { return getEJSON(printable.documents); } @@ -26,7 +27,9 @@ const getContent = ({ type, printable }: EvaluationResult) => { : getEJSON(printable); }; -export const getLanguage = (evaluationResult: EvaluationResult) => { +export const getLanguage = ( + evaluationResult: EvaluationResult +): 'json' | 'plaintext' => { const content = getContent(evaluationResult); if (typeof content === 'object' && content !== null) { @@ -44,7 +47,7 @@ type ExecuteCodeOptions = { filePath?: string; }; -function handleEvalPrint(values: EvaluationResult[]) { +function handleEvalPrint(values: EvaluationResult[]): void { parentPort?.postMessage({ name: ServerCommands.SHOW_CONSOLE_OUTPUT, payload: values.map((v) => { @@ -65,7 +68,7 @@ export const execute = async ({ connectionOptions, filePath, }: ExecuteCodeOptions): Promise<{ - data?: ShellEvaluateResult; + data: ShellEvaluateResult | null; error?: any; }> => { const serviceProvider = await CliServiceProvider.connect( @@ -112,7 +115,7 @@ export const execute = async ({ return { data: { result } }; } catch (error) { - return { error }; + return { error, data: null }; } finally { await serviceProvider.close(true); } diff --git a/src/participant/participant.ts b/src/participant/participant.ts index e0a5811ff..2d16bc6e7 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1246,7 +1246,7 @@ export default class ParticipantController { if (progressResult?.includes("Sorry, I can't assist with that.")) { void vscode.window.showErrorMessage("Sorry, I can't assist with that."); - return Promise.resolve(false); + return false; } if (progressResult) { @@ -1261,7 +1261,7 @@ export default class ParticipantController { } } - return Promise.resolve(true); + return true; } catch (error) { this.handleError(error, 'exportToPlayground'); await vscode.window.showErrorMessage( @@ -1269,7 +1269,7 @@ export default class ParticipantController { formatError(error).message }` ); - return Promise.resolve(false); + return false; } } diff --git a/src/participant/prompts/exportToPlayground.ts b/src/participant/prompts/exportToPlayground.ts index 61ba82bd2..cefaa8d9c 100644 --- a/src/participant/prompts/exportToPlayground.ts +++ b/src/participant/prompts/exportToPlayground.ts @@ -21,11 +21,9 @@ Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax.`; } static buildMessages(prompt: string): vscode.LanguageModelChatMessage[] { - const messages = [ + return [ ExportToPlaygroundPrompt.getAssistantPrompt(), ExportToPlaygroundPrompt.getUserPrompt(prompt), ]; - - return messages; } } diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 53dd2cba5..a40f8c184 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -48,9 +48,16 @@ type DocumentEditedTelemetryEventProperties = { source: DocumentSource; }; +type AggregationExportedTelemetryEventProperties = { + language: string; + num_stages: number | null; + with_import_statements: boolean; + with_builders: boolean; + with_driver_syntax: boolean; +}; + type QueryExportedTelemetryEventProperties = { language: string; - num_stages?: number; with_import_statements: boolean; with_builders: boolean; with_driver_syntax: boolean; @@ -130,6 +137,7 @@ type TelemetryEventProperties = | ConnectionEditedTelemetryEventProperties | DocumentEditedTelemetryEventProperties | QueryExportedTelemetryEventProperties + | AggregationExportedTelemetryEventProperties | PlaygroundCreatedTelemetryEventProperties | PlaygroundSavedTelemetryEventProperties | PlaygroundLoadedTelemetryEventProperties @@ -390,7 +398,7 @@ export default class TelemetryService { } trackAggregationExported( - aggExportedProps: QueryExportedTelemetryEventProperties + aggExportedProps: AggregationExportedTelemetryEventProperties ): void { this.track(TelemetryEventTypes.AGGREGATION_EXPORTED, aggExportedProps); } diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index 7d5458161..348f40ec9 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -321,14 +321,14 @@ suite('Playground Controller Test Suite', function () { sandbox.replace( testPlaygroundController, '_evaluate', - sandbox.fake.rejects(false) + sandbox.fake.resolves(null) ); const result = await testPlaygroundController._evaluateWithCancelModal( '' ); - expect(result).to.deep.equal({ result: undefined }); + expect(result).to.deep.equal(null); }); test('playground controller loads the active editor on start', () => { diff --git a/src/test/suite/language/mongoDBService.test.ts b/src/test/suite/language/mongoDBService.test.ts index 6f7e1fe5d..c3fc37f60 100644 --- a/src/test/suite/language/mongoDBService.test.ts +++ b/src/test/suite/language/mongoDBService.test.ts @@ -73,7 +73,7 @@ suite('MongoDBService Test Suite', () => { source.token ); - expect(result).to.be.equal(undefined); + expect(result).to.be.equal(null); }); test('catches error when _getCollectionsCompletionItems is called and extension path is empty string', async () => { @@ -2729,7 +2729,7 @@ suite('MongoDBService Test Suite', () => { source.token ); - expect(result).to.equal(undefined); + expect(result).to.equal(null); }); test('evaluate multiplies commands at once', async () => { diff --git a/src/types/playgroundType.ts b/src/types/playgroundType.ts index 0e43a9f21..91080e8ad 100644 --- a/src/types/playgroundType.ts +++ b/src/types/playgroundType.ts @@ -12,16 +12,15 @@ export type PlaygroundDebug = OutputItem[] | undefined; export type PlaygroundResult = OutputItem | undefined; -export type ShellEvaluateResult = - | { - result: PlaygroundResult; - } - | undefined; +export type ShellEvaluateResult = { + result: PlaygroundResult; +} | null; export type PlaygroundEvaluateParams = { codeToEvaluate: string; connectionId: string; filePath?: string; + token?: vscode.CancellationToken; }; export interface ExportToLanguageAddons { From ea05667d842df87c8863ee9a30957e193ed39e9d Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 25 Sep 2024 19:03:52 +0200 Subject: [PATCH 10/12] refactor: more clean up --- package.json | 2 +- src/editors/playgroundController.ts | 16 ++++++------ src/language/languageServerController.ts | 5 ++-- src/language/server.ts | 3 ++- .../language/languageServerController.test.ts | 25 ++++++++++++------- .../suite/telemetry/telemetryService.test.ts | 3 ++- src/types/playgroundType.ts | 1 - 7 files changed, 33 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 8a0e71fef..47f803631 100644 --- a/package.json +++ b/package.json @@ -1016,7 +1016,7 @@ "submenus": [ { "id": "mdb.copilot", - "label": "MongoDB Copilot" + "label": "MongoDB Copilot Participant" } ], "keybindings": [ diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 6ed98563c..25c61af46 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -440,7 +440,7 @@ export default class PlaygroundController { async _evaluate( codeToEvaluate: string, - token?: vscode.CancellationToken + token: vscode.CancellationToken ): Promise { const connectionId = this._connectionController.getActiveConnectionId(); @@ -455,12 +455,14 @@ export default class PlaygroundController { let result: ShellEvaluateResult = null; try { // Send a request to the language server to execute scripts from a playground. - result = await this._languageServerController.evaluate({ - codeToEvaluate, - connectionId, - filePath: vscode.window.activeTextEditor?.document.uri.fsPath, - token, - }); + result = await this._languageServerController.evaluate( + { + codeToEvaluate, + connectionId, + filePath: vscode.window.activeTextEditor?.document.uri.fsPath, + }, + token + ); } catch (error) { const msg = 'An internal error has occurred. The playground services have been restored. This can occur when the playground runner runs out of memory.'; diff --git a/src/language/languageServerController.ts b/src/language/languageServerController.ts index 8912cf2bf..7e29135f7 100644 --- a/src/language/languageServerController.ts +++ b/src/language/languageServerController.ts @@ -177,7 +177,8 @@ export default class LanguageServerController { } async evaluate( - playgroundExecuteParameters: PlaygroundEvaluateParams + playgroundExecuteParameters: PlaygroundEvaluateParams, + token: vscode.CancellationToken ): Promise { log.info('Running a playground...', { connectionId: playgroundExecuteParameters.connectionId, @@ -193,7 +194,7 @@ export default class LanguageServerController { const res: ShellEvaluateResult = await this._client.sendRequest( ServerCommands.EXECUTE_CODE_FROM_PLAYGROUND, playgroundExecuteParameters, - playgroundExecuteParameters.token + token ); this._isExecutingInProgress = false; diff --git a/src/language/server.ts b/src/language/server.ts index 0bef401a4..b66793bb5 100644 --- a/src/language/server.ts +++ b/src/language/server.ts @@ -1,4 +1,5 @@ import type { + CancellationToken, InitializeParams, CompletionItem, TextDocumentPositionParams, @@ -161,7 +162,7 @@ connection.onDidChangeWatchedFiles((/* _change */) => { // Execute a playground. connection.onRequest( ServerCommands.EXECUTE_CODE_FROM_PLAYGROUND, - (evaluateParams: PlaygroundEvaluateParams, token) => { + (evaluateParams: PlaygroundEvaluateParams, token: CancellationToken) => { return mongoDBService.evaluate(evaluateParams, token); } ); diff --git a/src/test/suite/language/languageServerController.test.ts b/src/test/suite/language/languageServerController.test.ts index 87d09b17b..15c0ce1ef 100644 --- a/src/test/suite/language/languageServerController.test.ts +++ b/src/test/suite/language/languageServerController.test.ts @@ -107,9 +107,10 @@ suite('Language Server Controller Test Suite', () => { test('cancel a long-running script', async () => { expect(languageServerControllerStub._isExecutingInProgress).to.equal(false); - - await languageServerControllerStub.evaluate({ - codeToEvaluate: ` + const source = new vscode.CancellationTokenSource(); + await languageServerControllerStub.evaluate( + { + codeToEvaluate: ` const names = [ "flour", "butter", @@ -126,8 +127,10 @@ suite('Language Server Controller Test Suite', () => { }); currentName `, - connectionId: 'pineapple', - }); + connectionId: 'pineapple', + }, + source.token + ); languageServerControllerStub.cancelAll(); expect(languageServerControllerStub._isExecutingInProgress).to.equal(false); @@ -176,13 +179,17 @@ suite('Language Server Controller Test Suite', () => { expect(outputChannelClearStub).to.not.be.called; - await languageServerControllerStub.evaluate({ - codeToEvaluate: ` + const source = new vscode.CancellationTokenSource(); + await languageServerControllerStub.evaluate( + { + codeToEvaluate: ` print('test'); console.log({ pineapple: 'yes' }); `, - connectionId: 'pineapple', - }); + connectionId: 'pineapple', + }, + source.token + ); expect(outputChannelClearStub).to.be.calledOnce; }); diff --git a/src/test/suite/telemetry/telemetryService.test.ts b/src/test/suite/telemetry/telemetryService.test.ts index 0055f0e50..e1517239b 100644 --- a/src/test/suite/telemetry/telemetryService.test.ts +++ b/src/test/suite/telemetry/telemetryService.test.ts @@ -235,7 +235,8 @@ suite('Telemetry Controller Test Suite', () => { test('track playground code executed event', async () => { const testPlaygroundController = mdbTestExtension.testExtensionController._playgroundController; - await testPlaygroundController._evaluate('show dbs'); + const source = new vscode.CancellationTokenSource(); + await testPlaygroundController._evaluate('show dbs', source.token); sandbox.assert.calledWith( fakeSegmentAnalyticsTrack, sinon.match({ diff --git a/src/types/playgroundType.ts b/src/types/playgroundType.ts index 91080e8ad..859b8a487 100644 --- a/src/types/playgroundType.ts +++ b/src/types/playgroundType.ts @@ -20,7 +20,6 @@ export type PlaygroundEvaluateParams = { codeToEvaluate: string; connectionId: string; filePath?: string; - token?: vscode.CancellationToken; }; export interface ExportToLanguageAddons { From 5d873dfe377a8983f600ef1960cee7fe9b47bf1a Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Fri, 27 Sep 2024 16:49:00 +0200 Subject: [PATCH 11/12] refactor: more cleanup --- src/editors/playgroundController.ts | 7 +-- src/language/languageServerController.ts | 15 ------ src/participant/participant.ts | 54 ++++--------------- src/participant/prompts/exportToPlayground.ts | 48 +++++++++++++++-- src/telemetry/telemetryService.ts | 35 ++++++++++++ .../editors/playgroundController.test.ts | 14 ----- .../language/languageServerController.test.ts | 31 ----------- .../suite/participant/participant.test.ts | 15 ++++-- src/test/suite/stubs.ts | 2 - 9 files changed, 102 insertions(+), 119 deletions(-) diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 25c61af46..b5b06d964 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -503,12 +503,7 @@ export default class PlaygroundController { title: 'Running MongoDB playground...', cancellable: true, }, - async (progress, token): Promise => { - token.onCancellationRequested(() => { - // If the user clicks the cancel button, - // terminate all processes running on the language server. - this._languageServerController.cancelAll(); - }); + (progress, token): Promise => { return this._evaluate(codeToEvaluate, token); } ); diff --git a/src/language/languageServerController.ts b/src/language/languageServerController.ts index 7e29135f7..cd95afb7b 100644 --- a/src/language/languageServerController.ts +++ b/src/language/languageServerController.ts @@ -28,7 +28,6 @@ const log = createLogger('language server controller'); */ export default class LanguageServerController { _context: ExtensionContext; - _isExecutingInProgress = false; _client: LanguageClient; _currentConnectionId: string | null = null; _currentConnectionString?: string; @@ -185,7 +184,6 @@ export default class LanguageServerController { filePath: playgroundExecuteParameters.filePath, inputLength: playgroundExecuteParameters.codeToEvaluate.length, }); - this._isExecutingInProgress = true; this._consoleOutputChannel.clear(); // Send a request with a cancellation token @@ -197,8 +195,6 @@ export default class LanguageServerController { token ); - this._isExecutingInProgress = false; - log.info('Evaluate response', { namespace: res?.result?.namespace, type: res?.result?.type, @@ -263,17 +259,6 @@ export default class LanguageServerController { ); } - cancelAll(): void { - log.info('Canceling a playground...'); - // Send a request for cancellation. As a result - // the associated CancellationToken will be notified of the cancellation, - // the onCancellationRequested event will be fired, - // and IsCancellationRequested will return true. - if (this._isExecutingInProgress) { - this._isExecutingInProgress = false; - } - } - async updateCurrentSessionFields(params): Promise { await this._client.sendRequest( ServerCommands.UPDATE_CURRENT_SESSION_FIELDS, diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 2d16bc6e7..b235e08da 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -134,44 +134,6 @@ export default class ParticipantController { return this._participant; } - handleError(err: any, command: string): void { - let errorCode: string | undefined; - let errorName: ParticipantErrorTypes; - // Making the chat request might fail because - // - model does not exist - // - user consent not given - // - quote limits exceeded - if (err instanceof vscode.LanguageModelError) { - errorCode = err.code; - } - - if (err instanceof Error) { - // Unwrap the error if a cause is provided - err = err.cause || err; - } - - const message: string = err.message || err.toString(); - - if (message.includes('off_topic')) { - errorName = ParticipantErrorTypes.CHAT_MODEL_OFF_TOPIC; - } else if (message.includes('Filtered by Responsible AI Service')) { - errorName = ParticipantErrorTypes.FILTERED; - } else if (message.includes('Prompt failed validation')) { - errorName = ParticipantErrorTypes.INVALID_PROMPT; - } else { - errorName = ParticipantErrorTypes.OTHER; - } - - this._telemetryService.track( - TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED, - { - command, - error_code: errorCode, - error_name: errorName, - } - ); - } - /** * In order to get access to the model, and to write more messages to the chat after * an async event that occurs after we've already completed our response, we need @@ -1222,7 +1184,7 @@ export default class ParticipantController { title: 'Exporting code to a playground...', cancellable: true, }, - async (progress, token): Promise => { + (progress, token): Promise => { token.onCancellationRequested(async () => { await vscode.window.showInformationMessage( 'The running export to a playground operation was canceled.' @@ -1263,7 +1225,10 @@ export default class ParticipantController { return true; } catch (error) { - this.handleError(error, 'exportToPlayground'); + this._telemetryService.trackCopilotParticipantError( + error, + 'exportToPlayground' + ); await vscode.window.showErrorMessage( `An error occurred exporting to a playground: ${ formatError(error).message @@ -1319,10 +1284,13 @@ Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more i return await this.handleGenericRequest(...args); } - } catch (e) { - this.handleError(e, request.command || 'generic'); + } catch (error) { + this._telemetryService.trackCopilotParticipantError( + error, + request.command || 'generic' + ); // Re-throw other errors so they show up in the UI. - throw e; + throw error; } } diff --git a/src/participant/prompts/exportToPlayground.ts b/src/participant/prompts/exportToPlayground.ts index cefaa8d9c..660db3c3a 100644 --- a/src/participant/prompts/exportToPlayground.ts +++ b/src/participant/prompts/exportToPlayground.ts @@ -3,13 +3,51 @@ import * as vscode from 'vscode'; export class ExportToPlaygroundPrompt { static getAssistantPrompt(): vscode.LanguageModelChatMessage { const prompt = `You are a MongoDB expert. -Your task is to help the user build MongoDB queries and aggregation pipelines that perform their task. -You convert user's code written in any programming language to the MongoDB Shell syntax. +Your task is to convert user's code written in any programming language to the MongoDB mongosh shell script. + +Example: +User: +public class InsertMany { + public static void main(String[] args) { + // Replace the uri string with your MongoDB deployment's connection string + String uri = ""; + try (MongoClient mongoClient = MongoClients.create(uri)) { + MongoDatabase database = mongoClient.getDatabase("sample_mflix"); + MongoCollection collection = database.getCollection("movies"); + List movieList = Arrays.asList( + new Document().append("title", "Short Circuit 3"), + new Document().append("title", "The Lego Frozen Movie")); + try { + InsertManyResult result = collection.insertMany(movieList); + System.out.println("Inserted document ids: " + result.getInsertedIds()); + } catch (MongoException me) { + System.err.println("Unable to insert due to an error: " + me); + } + } + } +} +Response: +class InsertMany { + main(args) { + const uri = "/sample_mflix"; + // Replace the uri string with your MongoDB deployment's connection string + db = connect(uri); + + try { + const ids = db.movies.insertMany([ + { "title": "Short Circuit 3" }, + { "title": "The Lego Frozen Movie" }, + ]); + printjson(ids.insertedIds); + } catch (error) { + print(error); + } + } +} + Take a user prompt as an input string and translate it to the MongoDB Shell language. Keep your response concise. -You should suggest queries that are performant and correct. -Respond with markdown, suggest code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. -Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax.`; +Respond with markdown, suggest code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`.`; // eslint-disable-next-line new-cap return vscode.LanguageModelChatMessage.Assistant(prompt); diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index a40f8c184..8d48b7c47 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -430,4 +430,39 @@ export default class TelemetryService { trackCopilotParticipantFeedback(props: ParticipantFeedbackProperties): void { this.track(TelemetryEventTypes.PARTICIPANT_FEEDBACK, props); } + + trackCopilotParticipantError(err: any, command: string): void { + let errorCode: string | undefined; + let errorName: ParticipantErrorTypes; + // Making the chat request might fail because + // - model does not exist + // - user consent not given + // - quote limits exceeded + if (err instanceof vscode.LanguageModelError) { + errorCode = err.code; + } + + if (err instanceof Error) { + // Unwrap the error if a cause is provided + err = err.cause || err; + } + + const message: string = err.message || err.toString(); + + if (message.includes('off_topic')) { + errorName = ParticipantErrorTypes.CHAT_MODEL_OFF_TOPIC; + } else if (message.includes('Filtered by Responsible AI Service')) { + errorName = ParticipantErrorTypes.FILTERED; + } else if (message.includes('Prompt failed validation')) { + errorName = ParticipantErrorTypes.INVALID_PROMPT; + } else { + errorName = ParticipantErrorTypes.OTHER; + } + + this.track(TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED, { + command, + error_code: errorCode, + error_name: errorName, + }); + } } diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index 348f40ec9..17a509cb8 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -317,20 +317,6 @@ suite('Playground Controller Test Suite', function () { expect(showTextDocumentOptions.viewColumn).to.be.equal(-2); }); - test('close cancelation modal when a playground is canceled', async () => { - sandbox.replace( - testPlaygroundController, - '_evaluate', - sandbox.fake.resolves(null) - ); - - const result = await testPlaygroundController._evaluateWithCancelModal( - '' - ); - - expect(result).to.deep.equal(null); - }); - test('playground controller loads the active editor on start', () => { sandbox.replaceGetter( vscode.window, diff --git a/src/test/suite/language/languageServerController.test.ts b/src/test/suite/language/languageServerController.test.ts index 15c0ce1ef..45919d9b0 100644 --- a/src/test/suite/language/languageServerController.test.ts +++ b/src/test/suite/language/languageServerController.test.ts @@ -105,37 +105,6 @@ suite('Language Server Controller Test Suite', () => { sandbox.restore(); }); - test('cancel a long-running script', async () => { - expect(languageServerControllerStub._isExecutingInProgress).to.equal(false); - const source = new vscode.CancellationTokenSource(); - await languageServerControllerStub.evaluate( - { - codeToEvaluate: ` - const names = [ - "flour", - "butter", - "water", - "salt", - "onions", - "leek" - ]; - let currentName = ''; - names.forEach((name) => { - setTimeout(() => { - currentName = name; - }, 500); - }); - currentName - `, - connectionId: 'pineapple', - }, - source.token - ); - - languageServerControllerStub.cancelAll(); - expect(languageServerControllerStub._isExecutingInProgress).to.equal(false); - }); - test('the language server dependency bundle exists', async () => { const extensionPath = mdbTestExtension.extensionContextStub.extensionPath; const languageServerModuleBundlePath = path.join( diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 054aa1e97..da76eaf5b 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1440,7 +1440,10 @@ Schema: test('reports error', function () { const err = Error('Filtered by Responsible AI Service'); - testParticipantController.handleError(err, 'query'); + testParticipantController._telemetryService.trackCopilotParticipantError( + err, + 'query' + ); sinon.assert.calledOnce(telemetryTrackStub); expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( @@ -1458,7 +1461,10 @@ Schema: test('reports nested error', function () { const err = new Error('Parent error'); err.cause = Error('This message is flagged as off topic: off_topic.'); - testParticipantController.handleError(err, 'docs'); + testParticipantController._telemetryService.trackCopilotParticipantError( + err, + 'docs' + ); sinon.assert.calledOnce(telemetryTrackStub); expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( @@ -1474,7 +1480,10 @@ Schema: test('Reports error code when available', function () { // eslint-disable-next-line new-cap const err = vscode.LanguageModelError.NotFound('Model not found'); - testParticipantController.handleError(err, 'schema'); + testParticipantController._telemetryService.trackCopilotParticipantError( + err, + 'schema' + ); sinon.assert.calledOnce(telemetryTrackStub); expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( diff --git a/src/test/suite/stubs.ts b/src/test/suite/stubs.ts index bb1f09df2..11ac366d7 100644 --- a/src/test/suite/stubs.ts +++ b/src/test/suite/stubs.ts @@ -265,7 +265,6 @@ class LanguageServerControllerStub { _context: ExtensionContextStub; _storageController?: StorageController; _source?: CancellationTokenSource; - _isExecutingInProgress: boolean; _client: LanguageClient; _currentConnectionId: string | null = null; _consoleOutputChannel = @@ -331,7 +330,6 @@ class LanguageServerControllerStub { serverOptions, clientOptions ); - this._isExecutingInProgress = false; } startLanguageServer(): Promise { From 05ea3c3978761b5384bcf36e368313ed6afdff74 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Thu, 24 Oct 2024 17:58:30 +0200 Subject: [PATCH 12/12] test: use getMessageContent --- src/test/suite/participant/participant.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index c97d39723..61eeb5c6f 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1802,7 +1802,9 @@ Schema: }); await testParticipantController.exportCodeToPlayground(); const messages = sendRequestStub.firstCall.args[0]; - expect(messages[1].content).to.include('System.out.println'); + expect(getMessageContent(messages[1])).to.include( + 'System.out.println' + ); expect( isPlayground(vscode.window.activeTextEditor?.document.uri) ).to.be.eql(true);