diff --git a/package-lock.json b/package-lock.json index 99434067e..a7a0a4c01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@mongosh/shell-api": "^1.0.4", "analytics-node": "^5.0.0", "bson": "^4.4.1", + "bson-transpilers": "^1.2.0", "classnames": "^2.3.1", "debug": "^4.3.2", "dotenv": "^8.2.0", @@ -76,6 +77,7 @@ "chai-json-schema": "^1.5.1", "chalk": "^4.1.2", "cli-ux": "^5.6.3", + "context-map-webpack-plugin": "^0.1.0", "cross-env": "^7.0.3", "css-loader": "^3.4.2", "depcheck": "^1.4.2", @@ -3396,6 +3398,11 @@ "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", "dev": true }, + "node_modules/antlr4": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.7.2.tgz", + "integrity": "sha512-vZA1xYufXLe3LX+ja9rIVxjRmILb1x3k7KYZHltRbfJtXjJ1DlFIqt+CbPYmghx0EuzY9DajiDw+MdyEt1qAsQ==" + }, "node_modules/anymatch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", @@ -3481,7 +3488,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -4388,6 +4394,17 @@ "node": ">=6.9.0" } }, + "node_modules/bson-transpilers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bson-transpilers/-/bson-transpilers-1.2.0.tgz", + "integrity": "sha512-6JJWpPWbrJ4ebtpqie+AdTcT6owlWl/It5RELLbLlNKqGuzcp2H2kjjLCvDfUefob4zdLlTdm6cb330s2lX//w==", + "dependencies": { + "antlr4": "4.7.2", + "bson": "^4.4.1", + "context-eval": "^0.1.0", + "js-yaml": "^3.13.1" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -5758,6 +5775,20 @@ "node": ">= 0.6" } }, + "node_modules/context-eval": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/context-eval/-/context-eval-0.1.0.tgz", + "integrity": "sha1-P8pxfX3wI6l4XwjGWkGJo93wP6s=" + }, + "node_modules/context-map-webpack-plugin": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/context-map-webpack-plugin/-/context-map-webpack-plugin-0.1.0.tgz", + "integrity": "sha512-iq0u4ChgErhMk+l/3va5xJvjoVf14sA+5mcsBWcmWA3B8Rt31/F+PbbbAl5LteiIyWJWy2WxLPUxp08Yc0HxRg==", + "dev": true, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, "node_modules/convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", @@ -8258,7 +8289,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -13633,7 +13663,6 @@ "version": "3.14.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -21124,8 +21153,7 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "node_modules/ssh2": { "version": "0.8.9", @@ -27130,6 +27158,11 @@ "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", "dev": true }, + "antlr4": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.7.2.tgz", + "integrity": "sha512-vZA1xYufXLe3LX+ja9rIVxjRmILb1x3k7KYZHltRbfJtXjJ1DlFIqt+CbPYmghx0EuzY9DajiDw+MdyEt1qAsQ==" + }, "anymatch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", @@ -27210,7 +27243,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -27997,6 +28029,17 @@ "buffer": "^5.6.0" } }, + "bson-transpilers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bson-transpilers/-/bson-transpilers-1.2.0.tgz", + "integrity": "sha512-6JJWpPWbrJ4ebtpqie+AdTcT6owlWl/It5RELLbLlNKqGuzcp2H2kjjLCvDfUefob4zdLlTdm6cb330s2lX//w==", + "requires": { + "antlr4": "4.7.2", + "bson": "^4.4.1", + "context-eval": "^0.1.0", + "js-yaml": "^3.13.1" + } + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -29108,6 +29151,17 @@ "safe-buffer": "5.1.2" } }, + "context-eval": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/context-eval/-/context-eval-0.1.0.tgz", + "integrity": "sha1-P8pxfX3wI6l4XwjGWkGJo93wP6s=" + }, + "context-map-webpack-plugin": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/context-map-webpack-plugin/-/context-map-webpack-plugin-0.1.0.tgz", + "integrity": "sha512-iq0u4ChgErhMk+l/3va5xJvjoVf14sA+5mcsBWcmWA3B8Rt31/F+PbbbAl5LteiIyWJWy2WxLPUxp08Yc0HxRg==", + "dev": true + }, "convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", @@ -31209,8 +31263,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.3.1", @@ -35696,7 +35749,6 @@ "version": "3.14.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -42011,8 +42063,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "ssh2": { "version": "0.8.9", diff --git a/package.json b/package.json index 471818545..51a2eaeb4 100644 --- a/package.json +++ b/package.json @@ -226,6 +226,10 @@ "command": "mdb.changeActiveConnection", "title": "MongoDB: Change Active Connection" }, + { + "command": "mdb.changeExportToLanguageAddons", + "title": "MongoDB: Change Export To Language Addons" + }, { "command": "mdb.runSelectedPlaygroundBlocks", "title": "MongoDB: Run Selected Lines From Playground" @@ -246,6 +250,22 @@ "dark": "images/dark/play.svg" } }, + { + "command": "mdb.exportToPython", + "title": "MongoDB: Export To Python 3" + }, + { + "command": "mdb.exportToJava", + "title": "MongoDB: Export To Java" + }, + { + "command": "mdb.exportToCsharp", + "title": "MongoDB: Export To C#" + }, + { + "command": "mdb.exportToNode", + "title": "MongoDB: Export To Node" + }, { "command": "mdb.addConnection", "title": "Add MongoDB Connection", @@ -613,6 +633,10 @@ "command": "mdb.changeActiveConnection", "when": "false" }, + { + "command": "mdb.changeExportToLanguageAddons", + "when": "false" + }, { "command": "mdb.copyConnectionString", "when": "false" @@ -876,6 +900,7 @@ "@mongosh/shell-api": "^1.0.4", "analytics-node": "^5.0.0", "bson": "^4.4.1", + "bson-transpilers": "^1.2.0", "classnames": "^2.3.1", "debug": "^4.3.2", "dotenv": "^8.2.0", @@ -928,6 +953,7 @@ "chai-json-schema": "^1.5.1", "chalk": "^4.1.2", "cli-ux": "^5.6.3", + "context-map-webpack-plugin": "^0.1.0", "cross-env": "^7.0.3", "css-loader": "^3.4.2", "depcheck": "^1.4.2", diff --git a/src/commands/index.ts b/src/commands/index.ts index 61e21e752..60c06fee2 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -14,6 +14,12 @@ enum EXTENSION_COMMANDS { MDB_RUN_ALL_PLAYGROUND_BLOCKS = 'mdb.runAllPlaygroundBlocks', MDB_RUN_ALL_OR_SELECTED_PLAYGROUND_BLOCKS = 'mdb.runPlayground', + MDB_EXPORT_TO_PYTHON = 'mdb.exportToPython', + MDB_EXPORT_TO_JAVA = 'mdb.exportToJava', + MDB_EXPORT_TO_CSHARP = 'mdb.exportToCsharp', + MDB_EXPORT_TO_NODE = 'mdb.exportToNode', + MDB_CHANGE_EXPORT_TO_LANGUAGE_ADDONS = 'mdb.changeExportToLanguageAddons', + MDB_OPEN_MONGODB_DOCUMENT_FROM_CODE_LENS = 'mdb.openMongoDBDocumentFromCodeLens', MDB_OPEN_MONGODB_DOCUMENT_FROM_TREE = 'mdb.openMongoDBDocumentFromTree', MDB_SAVE_MONGODB_DOCUMENT = 'mdb.saveMongoDBDocument', diff --git a/src/editors/codeActionProvider.ts b/src/editors/codeActionProvider.ts index b209ddbce..884fc202a 100644 --- a/src/editors/codeActionProvider.ts +++ b/src/editors/codeActionProvider.ts @@ -1,29 +1,78 @@ import * as vscode from 'vscode'; + import EXTENSION_COMMANDS from '../commands'; -import PlaygroundController from './playgroundController'; +import { ExportToLanguageMode } from '../types/playgroundType'; export default class CodeActionProvider implements vscode.CodeActionProvider { - _playgroundController: PlaygroundController; + _onDidChangeCodeCodeAction: vscode.EventEmitter = new vscode.EventEmitter(); + selection?: vscode.Selection; + mode?: ExportToLanguageMode; static readonly providedCodeActionKinds = [vscode.CodeActionKind.QuickFix]; - constructor(playgroundController: PlaygroundController) { - this._playgroundController = playgroundController; + constructor() { + vscode.workspace.onDidChangeConfiguration(() => { + this._onDidChangeCodeCodeAction.fire(); + }); + } + + readonly onDidChangeCodeLenses: vscode.Event = this + ._onDidChangeCodeCodeAction.event; + + refresh({ selection, mode }: { selection?: vscode.Selection, mode?: ExportToLanguageMode }): void { + this.selection = selection; + this.mode = mode; + this._onDidChangeCodeCodeAction.fire(); } provideCodeActions(): vscode.CodeAction[] | undefined { - if (!this._playgroundController._selectedText) { + if (!this.selection) { return; } - const commandAction = new vscode.CodeAction('Run selected playground blocks', vscode.CodeActionKind.Empty); - - commandAction.command = { + const codeActions: vscode.CodeAction[] = []; + const runSelectedPlaygroundBlockCommand = new vscode.CodeAction('Run selected playground blocks', vscode.CodeActionKind.Empty); + runSelectedPlaygroundBlockCommand.command = { command: EXTENSION_COMMANDS.MDB_RUN_SELECTED_PLAYGROUND_BLOCKS, title: 'Run selected playground blocks', tooltip: 'Run selected playground blocks' }; + codeActions.push(runSelectedPlaygroundBlockCommand); + + if (this.mode === ExportToLanguageMode.QUERY || this.mode === ExportToLanguageMode.AGGREGATION) { + const exportToPythonCommand = new vscode.CodeAction('Export To Python 3', vscode.CodeActionKind.Empty); + exportToPythonCommand.command = { + command: EXTENSION_COMMANDS.MDB_EXPORT_TO_PYTHON, + title: 'Export To Python 3', + tooltip: 'Export To Python 3' + }; + codeActions.push(exportToPythonCommand); + + const exportToJavaCommand = new vscode.CodeAction('Export To Java', vscode.CodeActionKind.Empty); + exportToJavaCommand.command = { + command: EXTENSION_COMMANDS.MDB_EXPORT_TO_JAVA, + title: 'Export To Java', + tooltip: 'Export To Java' + }; + codeActions.push(exportToJavaCommand); + + const exportToCsharpCommand = new vscode.CodeAction('Export To C#', vscode.CodeActionKind.Empty); + exportToCsharpCommand.command = { + command: EXTENSION_COMMANDS.MDB_EXPORT_TO_CSHARP, + title: 'Export To C#', + tooltip: 'Export To C#' + }; + codeActions.push(exportToCsharpCommand); + + const exportToJSCommand = new vscode.CodeAction('Export To Node', vscode.CodeActionKind.Empty); + exportToJSCommand.command = { + command: EXTENSION_COMMANDS.MDB_EXPORT_TO_NODE, + title: 'Export To Node', + tooltip: 'Export To Node' + }; + codeActions.push(exportToJSCommand); + } - return [commandAction]; + return codeActions; } } diff --git a/src/editors/editorsController.ts b/src/editors/editorsController.ts index 5669fe78d..5ec426067 100644 --- a/src/editors/editorsController.ts +++ b/src/editors/editorsController.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { EJSON } from 'bson'; import ActiveConnectionCodeLensProvider from './activeConnectionCodeLensProvider'; +import ExportToLanguageCodeLensProvider from './exportToLanguageCodeLensProvider'; import CodeActionProvider from './codeActionProvider'; import ConnectionController from '../connectionController'; import CollectionDocumentsCodeLensProvider from './collectionDocumentsCodeLensProvider'; @@ -95,6 +96,7 @@ export default class EditorsController { _telemetryService: TelemetryService; _playgroundResultViewProvider: PlaygroundResultProvider; _activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider; + _exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; _editDocumentCodeLensProvider: EditDocumentCodeLensProvider; _collectionDocumentsCodeLensProvider: CollectionDocumentsCodeLensProvider; @@ -106,6 +108,7 @@ export default class EditorsController { telemetryService: TelemetryService, playgroundResultViewProvider: PlaygroundResultProvider, activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider, + exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider, codeActionProvider: CodeActionProvider, editDocumentCodeLensProvider: EditDocumentCodeLensProvider ) { @@ -135,6 +138,7 @@ export default class EditorsController { ); this._playgroundResultViewProvider = playgroundResultViewProvider; this._activeConnectionCodeLensProvider = activeConnectionCodeLensProvider; + this._exportToLanguageCodeLensProvider = exportToLanguageCodeLensProvider; this._collectionDocumentsCodeLensProvider = new CollectionDocumentsCodeLensProvider( this._collectionDocumentsOperationsStore ); @@ -399,6 +403,14 @@ export default class EditorsController { this._activeConnectionCodeLensProvider ) ); + this._context.subscriptions.push( + vscode.languages.registerCodeLensProvider( + { + scheme: PLAYGROUND_RESULT_SCHEME + }, + this._exportToLanguageCodeLensProvider + ) + ); this._context.subscriptions.push( vscode.languages.registerCodeLensProvider( { diff --git a/src/editors/exportToLanguageCodeLensProvider.ts b/src/editors/exportToLanguageCodeLensProvider.ts new file mode 100644 index 000000000..5ae951138 --- /dev/null +++ b/src/editors/exportToLanguageCodeLensProvider.ts @@ -0,0 +1,83 @@ +import * as vscode from 'vscode'; + +import EXTENSION_COMMANDS from '../commands'; +import { + ExportToLanguageMode, + ExportToLanguageAddons, + ExportToLanguages +} from '../types/playgroundType'; + +export default class ExportToLanguageCodeLensProvider +implements vscode.CodeLensProvider { + _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); + _exportToLanguageAddons: ExportToLanguageAddons; + + readonly onDidChangeCodeLenses: vscode.Event = this + ._onDidChangeCodeLenses.event; + + constructor() { + this._exportToLanguageAddons = { + importStatements: false, + driverSyntax: false, + builders: false, + language: 'shell' + }; + + vscode.workspace.onDidChangeConfiguration(() => { + this._onDidChangeCodeLenses.fire(); + }); + } + + refresh(exportToLanguageAddons: ExportToLanguageAddons): void { + this._exportToLanguageAddons = exportToLanguageAddons; + this._onDidChangeCodeLenses.fire(); + } + + createCodeLens(): vscode.CodeLens { + return new vscode.CodeLens(new vscode.Range(0, 0, 0, 0)); + } + + provideCodeLenses(): vscode.CodeLens[] { + const importStatementsCodeLens = this.createCodeLens(); + const driverSyntaxCodeLens = this.createCodeLens(); + const buildersCodeLens = this.createCodeLens(); + const exportToLanguageCodeLenses: vscode.CodeLens[] = []; + + importStatementsCodeLens.command = { + title: this._exportToLanguageAddons.importStatements ? 'Exclude Import Statements' : 'Include Import Statements', + command: EXTENSION_COMMANDS.MDB_CHANGE_EXPORT_TO_LANGUAGE_ADDONS, + arguments: [{ + ...this._exportToLanguageAddons, + importStatements: !this._exportToLanguageAddons.importStatements + }] + }; + exportToLanguageCodeLenses.push(importStatementsCodeLens); + + driverSyntaxCodeLens.command = { + title: this._exportToLanguageAddons.driverSyntax ? 'Exclude Driver Syntax' : 'Include Driver Syntax', + command: EXTENSION_COMMANDS.MDB_CHANGE_EXPORT_TO_LANGUAGE_ADDONS, + arguments: [{ + ...this._exportToLanguageAddons, + driverSyntax: !this._exportToLanguageAddons.driverSyntax + }] + }; + exportToLanguageCodeLenses.push(driverSyntaxCodeLens); + + if ( + this._exportToLanguageAddons.language === ExportToLanguages.JAVA && + this._exportToLanguageAddons.mode === ExportToLanguageMode.QUERY + ) { + buildersCodeLens.command = { + title: this._exportToLanguageAddons.builders ? 'Use Raw Query' : 'Use Builders', + command: EXTENSION_COMMANDS.MDB_CHANGE_EXPORT_TO_LANGUAGE_ADDONS, + arguments: [{ + ...this._exportToLanguageAddons, + builders: !this._exportToLanguageAddons.builders + }] + }; + exportToLanguageCodeLenses.push(buildersCodeLens); + } + + return exportToLanguageCodeLenses; + } +} diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index d5d4bef82..76585928d 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -1,5 +1,4 @@ import * as vscode from 'vscode'; -import { EJSON } from 'bson'; import ActiveConnectionCodeLensProvider from './activeConnectionCodeLensProvider'; import ConnectionController, { @@ -8,11 +7,17 @@ import ConnectionController, { import { createLogger } from '../logging'; import { ExplorerController, ConnectionTreeItem, DatabaseTreeItem } from '../explorer'; import { LanguageServerController } from '../language'; +import ExportToLanguageCodeLensProvider from './exportToLanguageCodeLensProvider'; import { OutputChannel, ProgressLocation, TextEditor } from 'vscode'; import playgroundCreateIndexTemplate from '../templates/playgroundCreateIndexTemplate'; import playgroundCreateCollectionTemplate from '../templates/playgroundCreateCollectionTemplate'; import playgroundCreateCollectionWithTSTemplate from '../templates/playgroundCreateCollectionWithTSTemplate'; -import type { PlaygroundResult, ShellExecuteAllResult } from '../types/playgroundType'; +import { + PlaygroundResult, + ShellExecuteAllResult, + ExportToLanguageAddons, + ExportToLanguageNamespace +} from '../types/playgroundType'; import PlaygroundResultProvider, { PLAYGROUND_RESULT_SCHEME, PLAYGROUND_RESULT_URI @@ -22,7 +27,9 @@ import playgroundTemplate from '../templates/playgroundTemplate'; import { StatusView } from '../views'; import TelemetryService from '../telemetry/telemetryService'; import { ConnectionOptions } from '../types/connectionOptionsType'; +import CodeActionProvider from './codeActionProvider'; +const transpiler = require('bson-transpilers'); const log = createLogger('playground controller'); const getSSLFilePathsFromConnectionModel = ( @@ -65,6 +72,8 @@ export default class PlaygroundController { _statusView: StatusView; _playgroundResultViewProvider: PlaygroundResultProvider; _explorerController: ExplorerController; + _exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; + _codeActionProvider: CodeActionProvider; constructor( context: vscode.ExtensionContext, @@ -74,6 +83,8 @@ export default class PlaygroundController { statusView: StatusView, playgroundResultViewProvider: PlaygroundResultProvider, activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider, + exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider, + codeActionProvider: CodeActionProvider, explorerController: ExplorerController ) { this._context = context; @@ -87,6 +98,8 @@ export default class PlaygroundController { 'Playground output' ); this._activeConnectionCodeLensProvider = activeConnectionCodeLensProvider; + this._exportToLanguageCodeLensProvider = exportToLanguageCodeLensProvider; + this._codeActionProvider = codeActionProvider; this._explorerController = explorerController; this._connectionController.addEventListener( @@ -114,7 +127,7 @@ export default class PlaygroundController { onDidChangeActiveTextEditor(vscode.window.activeTextEditor); vscode.window.onDidChangeTextEditorSelection( - (changeEvent: vscode.TextEditorSelectionChangeEvent) => { + async (changeEvent: vscode.TextEditorSelectionChangeEvent) => { if ( changeEvent?.textEditor?.document?.languageId === 'mongodb' ) { @@ -125,6 +138,13 @@ export default class PlaygroundController { this._selectedText = sortedSelections .map((item) => this._getSelectedText(item)) .join('\n'); + + const mode = await this._languageServerController.getExportToLanguageMode({ + textFromEditor: this._getAllText(), + selection: sortedSelections[0] + }); + + this._codeActionProvider.refresh({ selection: sortedSelections[0], mode }); } } ); @@ -342,14 +362,6 @@ export default class PlaygroundController { } } - _getDocumentLanguage(content?: EJSON.SerializableTypes): string { - if (typeof content === 'object' && content !== null) { - return 'json'; - } - - return 'plaintext'; - } - async _openPlaygroundResult(): Promise { this._playgroundResultViewProvider.setPlaygroundResult( this._playgroundResult @@ -364,7 +376,7 @@ export default class PlaygroundController { await this._showResultAsVirtualDocument(); if (this._playgroundResultTextDocument) { - const language = this._getDocumentLanguage(this._playgroundResult?.content); + const language = this._playgroundResult?.language || 'plaintext'; await vscode.languages.setTextDocumentLanguage( this._playgroundResultTextDocument, @@ -533,6 +545,103 @@ export default class PlaygroundController { } } + changeExportToLanguageAddons(exportToLanguageAddons: ExportToLanguageAddons): Promise { + this._exportToLanguageCodeLensProvider.refresh(exportToLanguageAddons); + + return this._transpile(); + } + + async exportToLanguage(language: string): Promise { + this._exportToLanguageCodeLensProvider.refresh({ + ...this._exportToLanguageCodeLensProvider._exportToLanguageAddons, + textFromEditor: this._getAllText(), + selectedText: this._selectedText, + selection: this._codeActionProvider.selection, + language, + mode: this._codeActionProvider.mode + }); + + return this._transpile(); + } + + async _transpile(): Promise { + const { + textFromEditor, + selectedText, + selection, + importStatements, + driverSyntax, + builders, + language + } = this._exportToLanguageCodeLensProvider._exportToLanguageAddons; + + log.info(`Start export to ${language} language`); + + if (!textFromEditor || !selection) { + void vscode.window.showInformationMessage( + 'Please select one or more lines in the playground.' + ); + + return true; + } + + try { + let transpiledExpression = ''; + let imports = ''; + let namespace: ExportToLanguageNamespace = { + databaseName: null, + collectionName: null + }; + + if (driverSyntax) { + namespace = await this._languageServerController.getNamespaceForSelection({ + textFromEditor, + selection + }); + + const dataService = this._connectionController.getActiveDataService(); + const connectionDetails = dataService?.getConnectionOptions(); + const toCompile = { + aggregation: selectedText, + options: { + collection: namespace.collectionName, + database: namespace.databaseName, + uri: connectionDetails?.url + } + }; + + transpiledExpression = transpiler.shell[language].compileWithDriver(toCompile, builders); + } else { + transpiledExpression = transpiler.shell[language].compile(selectedText, builders, false); + } + + if (importStatements) { + imports = transpiler.shell[language].getImports(driverSyntax); + } + + this._playgroundResult = { + namespace: namespace.databaseName && namespace.collectionName + ? `${namespace.databaseName}.${namespace.collectionName}` + : null, + type: null, + content: imports ? `${imports}\n\n${transpiledExpression}` : transpiledExpression, + language + }; + + log.info(`Export to ${language} language result`, this._playgroundResult); + await this._openPlaygroundResult(); + } catch (error) { + const printableError = error as { message: string }; + + log.error(`Export to ${language} language error`, error); + void vscode.window.showErrorMessage( + `Unable to export to ${language} language: ${printableError.message}` + ); + } + + return true; + } + deactivate(): void { this._connectionController.removeEventListener( DataServiceEventTypes.ACTIVE_CONNECTION_CHANGED, diff --git a/src/editors/playgroundResultProvider.ts b/src/editors/playgroundResultProvider.ts index 877c859e1..005482825 100644 --- a/src/editors/playgroundResultProvider.ts +++ b/src/editors/playgroundResultProvider.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import ConnectionController from '../connectionController'; import EditDocumentCodeLensProvider from './editDocumentCodeLensProvider'; -import type { PlaygroundResult } from '../types/playgroundType'; +import { PlaygroundResult, ExportToLanguages } from '../types/playgroundType'; export const PLAYGROUND_RESULT_SCHEME = 'PLAYGROUND_RESULT_SCHEME'; @@ -25,7 +25,8 @@ implements vscode.TextDocumentContentProvider { this._playgroundResult = { namespace: null, type: null, - content: undefined + content: undefined, + language: null }; } @@ -47,14 +48,14 @@ implements vscode.TextDocumentContentProvider { return 'undefined'; } - const { type, content } = this._playgroundResult; + const { type, content, language } = this._playgroundResult; if (type === 'undefined') { return 'undefined'; } - if (type === 'string') { - return this._playgroundResult.content as string; + if (type === 'string' || (language && Object.values(ExportToLanguages).includes(language as ExportToLanguages))) { + return this._playgroundResult.content; } this._editDocumentCodeLensProvider?.updateCodeLensesForPlayground( diff --git a/src/language/languageServerController.ts b/src/language/languageServerController.ts index 7ea0c8f96..cf7f96152 100644 --- a/src/language/languageServerController.ts +++ b/src/language/languageServerController.ts @@ -11,9 +11,14 @@ import WebSocket from 'ws'; import { workspace, ExtensionContext, OutputChannel } from 'vscode'; import { createLogger } from '../logging'; -import { PlaygroundExecuteParameters } from '../types/playgroundType'; +import { + PlaygroundExecuteParameters, + ShellExecuteAllResult, + ExportToLanguageMode, + ExportToLanguageNamespace, + PlaygroundTextAndSelection +} from '../types/playgroundType'; import { ServerCommands } from './serverCommands'; -import type { ShellExecuteAllResult } from '../types/playgroundType'; import { ConnectionOptions } from '../types/connectionOptionsType'; const log = createLogger('LanguageServerController'); @@ -165,6 +170,26 @@ export default class LanguageServerController { return result; } + async getExportToLanguageMode( + params: PlaygroundTextAndSelection + ): Promise { + await this._client.onReady(); + return this._client.sendRequest( + ServerCommands.GET_EXPORT_TO_LANGUAGE_MODE, + params + ); + } + + async getNamespaceForSelection( + params: PlaygroundTextAndSelection + ): Promise { + await this._client.onReady(); + return this._client.sendRequest( + ServerCommands.GET_NAMESPACE_FOR_SELECTION, + params + ); + } + async connectToServiceProvider(params: { connectionId: string, connectionString: string; diff --git a/src/language/mongoDBService.ts b/src/language/mongoDBService.ts index 723d96d8f..e12b5bc97 100644 --- a/src/language/mongoDBService.ts +++ b/src/language/mongoDBService.ts @@ -1,6 +1,13 @@ /* eslint-disable no-sync */ import * as util from 'util'; -import { CompletionItemKind, CancellationToken, Connection, CompletionItem, MarkupContent, MarkupKind } from 'vscode-languageserver/node'; +import { + CompletionItemKind, + CancellationToken, + Connection, + CompletionItem, + MarkupContent, + MarkupKind +} from 'vscode-languageserver/node'; import path from 'path'; import { signatures } from '@mongosh/shell-api'; import translator from '@mongosh/i18n'; @@ -9,7 +16,13 @@ import { Worker as WorkerThreads } from 'worker_threads'; import { CollectionItem } from '../types/collectionItemType'; import { ConnectionOptions } from '../types/connectionOptionsType'; import { ServerCommands } from './serverCommands'; -import { ShellExecuteAllResult, PlaygroundExecuteParameters } from '../types/playgroundType'; +import { + ShellExecuteAllResult, + PlaygroundExecuteParameters, + ExportToLanguageMode, + ExportToLanguageNamespace, + PlaygroundTextAndSelection +} from '../types/playgroundType'; import { Visitor } from './visitor'; export const languageServerWorkerFileName = 'languageServerWorker.js'; @@ -33,7 +46,7 @@ export default class MongoDBService { constructor(connection: Connection) { this._connection = connection; this._cachedShellSymbols = this._getShellCompletionItems(); - this._visitor = new Visitor(); + this._visitor = new Visitor(connection.console); } // ------ CONNECTION ------ // @@ -47,7 +60,7 @@ export default class MongoDBService { setExtensionPath(extensionPath: string): void { if (!extensionPath) { - this._connection.console.log('Set extensionPath error: extensionPath is undefined'); + this._connection.console.error('Set extensionPath error: extensionPath is undefined'); } else { this._extensionPath = extensionPath; } @@ -76,7 +89,7 @@ export default class MongoDBService { return Promise.resolve(true); } catch (error) { - this._connection.console.log( + this._connection.console.error( `MONGOSH connect error: ${util.inspect(error)}` ); @@ -100,7 +113,7 @@ export default class MongoDBService { return new Promise((resolve) => { if (!this._extensionPath) { - this._connection.console.log('MONGOSH execute all error: extensionPath is undefined'); + this._connection.console.error('MONGOSH execute all error: extensionPath is undefined'); return resolve(undefined); } @@ -150,7 +163,7 @@ export default class MongoDBService { if (error) { const printableError = error as { message: string }; - this._connection.console.log( + this._connection.console.error( `MONGOSH execute all error: ${util.inspect(error)}` ); this._connection.sendNotification( @@ -192,7 +205,7 @@ export default class MongoDBService { return resolve(undefined); }); } catch (error) { - this._connection.console.log( + this._connection.console.error( `MONGOSH execute all error: ${util.inspect(error)}` ); return resolve(undefined); @@ -203,7 +216,7 @@ export default class MongoDBService { // ------ GET DATA FOR COMPLETION ------ // _getDatabasesCompletionItems(): void { if (!this._extensionPath) { - this._connection.console.log('MONGOSH get list databases error: extensionPath is undefined'); + this._connection.console.error('MONGOSH get list databases error: extensionPath is undefined'); return; } @@ -226,7 +239,7 @@ export default class MongoDBService { const [error, result] = response; if (error) { - this._connection.console.log( + this._connection.console.error( `MONGOSH get list databases error: ${util.inspect(error)}` ); } @@ -237,7 +250,7 @@ export default class MongoDBService { }); }); } catch (error) { - this._connection.console.log( + this._connection.console.error( `MONGOSH get list databases error: ${util.inspect(error)}` ); } @@ -429,10 +442,36 @@ export default class MongoDBService { }); } + getExportToLanguageMode(params: PlaygroundTextAndSelection): ExportToLanguageMode { + const state = this._visitor.parseAST(params); + + if (state.isArray) { + return ExportToLanguageMode.AGGREGATION; + } + + if (state.isObject) { + return ExportToLanguageMode.QUERY; + } + + return ExportToLanguageMode.OTHER; + } + + getNamespaceForSelection(params: PlaygroundTextAndSelection): ExportToLanguageNamespace { + try { + const state = this._visitor.parseAST(params); + return { databaseName: state.databaseName, collectionName: state.collectionName }; + } catch (error) { + this._connection.console.error( + `Get namespace for selection error: ${util.inspect(error)}` + ); + return { databaseName: null, collectionName: null }; + } + } + // eslint-disable-next-line complexity async provideCompletionItems( textFromEditor: string, - position: { line: number; character: number } + position: { line: number, character: number } ): Promise { // eslint-disable-next-line complexity this._connection.console.log( @@ -442,7 +481,7 @@ export default class MongoDBService { `LS current symbol position: ${util.inspect(position)}` ); - const state = this._visitor.parseAST(textFromEditor, position); + const state = this._visitor.parseASTWithPlaceholder(textFromEditor, position); this._connection.console.log( `VISITOR completion state: ${util.inspect(state)}` diff --git a/src/language/server.ts b/src/language/server.ts index 9fc101af5..4f3f3adc1 100644 --- a/src/language/server.ts +++ b/src/language/server.ts @@ -14,7 +14,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument'; import MongoDBService from './mongoDBService'; import { ServerCommands } from './serverCommands'; -import { PlaygroundExecuteParameters } from '../types/playgroundType'; +import { PlaygroundExecuteParameters, PlaygroundTextAndSelection } from '../types/playgroundType'; // Create a connection for the server. The connection uses Node's IPC as a transport. // Also include all preview / proposed LSP features. @@ -160,8 +160,16 @@ connection.onRequest(ServerCommands.DISCONNECT_TO_SERVICE_PROVIDER, () => { return mongoDBService.disconnectFromServiceProvider(); }); +connection.onRequest(ServerCommands.GET_EXPORT_TO_LANGUAGE_MODE, (params: PlaygroundTextAndSelection) => { + return mongoDBService.getExportToLanguageMode(params); +}); + +connection.onRequest(ServerCommands.GET_NAMESPACE_FOR_SELECTION, (params: PlaygroundTextAndSelection) => { + return mongoDBService.getNamespaceForSelection(params); +}); + // This handler provides the list of the completion items. -connection.onCompletion(async (params: TextDocumentPositionParams) => { +connection.onCompletion((params: TextDocumentPositionParams) => { const textFromEditor = documents.get(params.textDocument.uri)?.getText(); return mongoDBService.provideCompletionItems( diff --git a/src/language/serverCommands.ts b/src/language/serverCommands.ts index 3406def14..12a609251 100644 --- a/src/language/serverCommands.ts +++ b/src/language/serverCommands.ts @@ -8,7 +8,9 @@ export enum ServerCommands { GET_LIST_COLLECTIONS = 'GET_LIST_COLLECTIONS', SET_EXTENSION_PATH = 'SET_EXTENSION_PATH', SHOW_ERROR_MESSAGE = 'SHOW_ERROR_MESSAGE', - SHOW_INFO_MESSAGE = 'SHOW_INFO_MESSAGE' + SHOW_INFO_MESSAGE = 'SHOW_INFO_MESSAGE', + GET_NAMESPACE_FOR_SELECTION = 'GET_NAMESPACE_FOR_SELECTION', + GET_EXPORT_TO_LANGUAGE_MODE = 'GET_EXPORT_TO_LANGUAGE_MODE' } export type PlaygroundRunParameters = { diff --git a/src/language/visitor.ts b/src/language/visitor.ts index afa034984..e961d937a 100644 --- a/src/language/visitor.ts +++ b/src/language/visitor.ts @@ -1,12 +1,26 @@ import type * as babel from '@babel/core'; import * as parser from '@babel/parser'; import traverse from '@babel/traverse'; +import { RemoteConsole } from 'vscode-languageserver/node'; +import * as util from 'util'; const PLACEHOLDER = 'TRIGGER_CHARACTER'; -export type CompletionState = { +export interface VisitorSelection { + start: { line: number, character: number }; + end: { line: number, character: number }; +} + +export interface VisitorTextAndSelection { + textFromEditor: string; + selection: VisitorSelection; +} + +export interface CompletionState { databaseName: string | null; collectionName: string | null; + isObject: boolean; + isArray: boolean; isObjectKey: boolean; isShellMethod: boolean; isUseCallExpression: boolean; @@ -14,15 +28,20 @@ export type CompletionState = { isCollectionName: boolean; isAggregationCursor: boolean; isFindCursor: boolean; -}; +} export class Visitor { _state: CompletionState; - _position: { line: number; character: number }; + _selection: VisitorSelection; + _console: RemoteConsole; - constructor() { + constructor(console: RemoteConsole) { this._state = this._getDefaultNodesValues(); - this._position = { line: 0, character: 0 }; + this._selection = { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 } + }; + this._console = console; } _visitCallExpression(node: babel.types.CallExpression): void { @@ -68,11 +87,21 @@ export class Visitor { } _visitObjectExpression(node: babel.types.ObjectExpression): void { + if (this._checkIsObjectSelection(node)) { + this._state.isObject = true; + } + if (this._checkIsObjectKey(node)) { this._state.isObjectKey = true; } } + _visitArrayExpression(node: babel.types.ArrayExpression): void { + if (this._checkIsArraySelection(node)) { + this._state.isArray = true; + } + } + _handleTriggerCharacter( textFromEditor: string, position: { line: number; character: number } @@ -97,25 +126,33 @@ export class Visitor { return textLines.join('\n'); } - parseAST( + parseASTWithPlaceholder( textFromEditor: string, position: { line: number; character: number } ): CompletionState { - this._state = this._getDefaultNodesValues(); - this._position = position; + const selection: VisitorSelection = { start: position, end: { line: 0, character: 0 } }; - const textWithPlaceholder = this._handleTriggerCharacter( + textFromEditor = this._handleTriggerCharacter( textFromEditor, position ); + + return this.parseAST({ textFromEditor, selection }); + } + + parseAST({ textFromEditor, selection }: VisitorTextAndSelection): CompletionState { let ast: any; + this._state = this._getDefaultNodesValues(); + this._selection = selection; + try { - ast = parser.parse(textWithPlaceholder, { + ast = parser.parse(textFromEditor, { // Parse in strict mode and allow module declarations sourceType: 'module' }); } catch (error) { + this._console.error(`parseAST error: ${util.inspect(error)}`); return this._state; } @@ -134,6 +171,9 @@ export class Visitor { case 'ObjectExpression': this._visitObjectExpression(path.node); break; + case 'ArrayExpression': + this._visitArrayExpression(path.node); + break; default: break; } @@ -147,6 +187,8 @@ export class Visitor { return { databaseName: null, collectionName: null, + isObject: false, + isArray: false, isObjectKey: false, isShellMethod: false, isUseCallExpression: false, @@ -204,6 +246,45 @@ export class Visitor { return false; } + _checkIsWithinSelection(node: babel.types.Node): boolean { + if ( + node.loc?.start?.line && + node.loc.start.line - 1 === this._selection.start?.line && + node.loc?.start?.column && + node.loc.start.column === this._selection.start?.character && + node.loc?.end?.line && + node.loc.end.line - 1 === this._selection.end?.line && + node.loc?.end?.column && + node.loc.end.column === this._selection.end?.character + ) { + return true; + } + + return false; + } + + _checkIsObjectSelection(node: babel.types.ObjectExpression): boolean { + if ( + node.type === 'ObjectExpression' && + this._checkIsWithinSelection(node) + ) { + return true; + } + + return false; + } + + _checkIsArraySelection(node: babel.types.ArrayExpression): boolean { + if ( + node.type === 'ArrayExpression' && + this._checkIsWithinSelection(node) + ) { + return true; + } + + return false; + } + // eslint-disable-next-line complexity _checkIsCollectionName(node: babel.types.CallExpression | babel.types.MemberExpression): boolean { if ( @@ -262,9 +343,9 @@ export class Visitor { node.arguments.length === 1 && node.arguments[0].type === 'StringLiteral' && node.loc && - (this._position.line > node.loc.end.line - 1 || - (this._position.line === node.loc.end.line - 1 && - this._position.character >= node.loc.end.column)) + (this._selection.start.line > node.loc.end.line - 1 || + (this._selection.start.line === node.loc.end.line - 1 && + this._selection.start.character >= node.loc.end.column)) ) { return true; } diff --git a/src/language/worker.ts b/src/language/worker.ts index 627706113..62972af6d 100644 --- a/src/language/worker.ts +++ b/src/language/worker.ts @@ -3,21 +3,41 @@ import { MongoClientOptions } from '@mongosh/service-provider-server'; import { CompletionItemKind } from 'vscode-languageserver/node'; -import { EJSON } from 'bson'; +import { EJSON, Document } from 'bson'; import { ElectronRuntime } from '@mongosh/browser-runtime-electron'; import parseSchema = require('mongodb-schema'); import { parentPort, workerData } from 'worker_threads'; - -import type { PlaygroundResult, PlaygroundDebug, ShellExecuteAllResult } from '../types/playgroundType'; +import { PlaygroundResult, PlaygroundDebug, ShellExecuteAllResult } from '../types/playgroundType'; import { ServerCommands } from './serverCommands'; -type EvaluationResult = { +interface EvaluationResult { printable: any; type: string | null; -}; +} + type WorkerResult = ShellExecuteAllResult; type WorkerError = any | null; +const getContent = ({ type, printable }: EvaluationResult) => { + if (type === 'Cursor' || type === 'AggregationCursor') { + return JSON.parse(EJSON.stringify(printable.documents)); + } + + return (typeof printable !== 'object' || printable === null) + ? printable + : JSON.parse(EJSON.stringify(printable)); +}; + +const getLanguage = (evaluationResult: EvaluationResult) => { + const content = getContent(evaluationResult); + + if (typeof content === 'object' && content !== null) { + return 'json'; + } + + return 'plaintext'; +}; + const executeAll = async ( codeToEvaluate: string, connectionString: string, @@ -32,36 +52,32 @@ const executeAll = async ( connectionString, connectionOptions ); - const outputLines: PlaygroundDebug = []; + // Create a new instance of the runtime and evaluate code from a playground. const runtime: ElectronRuntime = new ElectronRuntime(serviceProvider); + runtime.setEvaluationListener({ onPrint(values: EvaluationResult[]) { for (const { type, printable } of values) { outputLines.push({ type, content: printable, - namespace: null + namespace: null, + language: null }); } } }); const { source, type, printable } = await runtime.evaluate(codeToEvaluate); - const namespace = - source && source.namespace - ? `${source.namespace.db}.${source.namespace.collection}` - : null; - const content = - type === 'Cursor' || type === 'AggregationCursor' ? - JSON.parse(EJSON.stringify(printable.documents)) : - typeof printable === 'string' - ? printable - : JSON.parse(EJSON.stringify(printable)); + const namespace = (source && source.namespace) + ? `${source.namespace.db}.${source.namespace.collection}` + : null; const result: PlaygroundResult = { namespace, type: type ? type : typeof printable, - content + content: getContent({ type, printable }), + language: getLanguage({ type, printable }) }; return [null, { outputLines, result }]; @@ -127,6 +143,17 @@ const getFieldsFromSchema = async ( } }; +const prepareCompletionItems = (result: Document) => { + if (!result) { + return []; + } + + return result.databases.map((item) => ({ + label: item.name, + kind: CompletionItemKind.Value + })); +}; + const getListDatabases = async ( connectionString: string, connectionOptions: any @@ -139,14 +166,9 @@ const getListDatabases = async ( // TODO: There is a mistake in the service provider interface // Use `admin` as arguments to get list of dbs - // and remove it later when `mongosh` will merge a fix + // and remove it later when `mongosh` will merge a fix. const result = await serviceProvider.listDatabases('admin'); - const databases = result - ? result.databases.map((item) => ({ - label: item.name, - kind: CompletionItemKind.Value - })) - : []; + const databases = prepareCompletionItems(result); return [null, databases]; } catch (error) { diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index e7858c098..c5dd66e45 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -26,6 +26,7 @@ import CodeActionProvider from './editors/codeActionProvider'; import EXTENSION_COMMANDS from './commands'; import FieldTreeItem from './explorer/fieldTreeItem'; import IndexListTreeItem from './explorer/indexListTreeItem'; +import ExportToLanguageCodeLensProvider from './editors/exportToLanguageCodeLensProvider'; import { LanguageServerController } from './language'; import launchMongoShell from './commands/launchMongoShell'; import SchemaTreeItem from './explorer/schemaTreeItem'; @@ -35,6 +36,7 @@ import TelemetryService from './telemetry/telemetryService'; import PlaygroundsTreeItem from './explorer/playgroundsTreeItem'; import PlaygroundResultProvider from './editors/playgroundResultProvider'; import WebviewController from './views/webviewController'; +import { ExportToLanguages } from './types/playgroundType'; const log = createLogger('commands'); @@ -57,6 +59,7 @@ export default class MDBExtensionController implements vscode.Disposable { _playgroundResultViewProvider: PlaygroundResultProvider; _activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider; _editDocumentCodeLensProvider: EditDocumentCodeLensProvider; + _exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; constructor( context: vscode.ExtensionContext, @@ -91,6 +94,8 @@ export default class MDBExtensionController implements vscode.Disposable { this._activeConnectionCodeLensProvider = new ActiveConnectionCodeLensProvider( this._connectionController ); + this._exportToLanguageCodeLensProvider = new ExportToLanguageCodeLensProvider(); + this._codeActionProvider = new CodeActionProvider(); this._playgroundController = new PlaygroundController( context, this._connectionController, @@ -99,9 +104,10 @@ export default class MDBExtensionController implements vscode.Disposable { this._statusView, this._playgroundResultViewProvider, this._activeConnectionCodeLensProvider, + this._exportToLanguageCodeLensProvider, + this._codeActionProvider, this._explorerController ); - this._codeActionProvider = new CodeActionProvider(this._playgroundController); this._editorsController = new EditorsController( context, this._connectionController, @@ -110,6 +116,7 @@ export default class MDBExtensionController implements vscode.Disposable { this._telemetryService, this._playgroundResultViewProvider, this._activeConnectionCodeLensProvider, + this._exportToLanguageCodeLensProvider, this._codeActionProvider, this._editDocumentCodeLensProvider ); @@ -188,6 +195,21 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommand(EXTENSION_COMMANDS.MDB_REFRESH_PLAYGROUNDS, () => this._playgroundsExplorer.refresh() ); + this.registerCommand(EXTENSION_COMMANDS.MDB_EXPORT_TO_PYTHON, () => + this._playgroundController.exportToLanguage(ExportToLanguages.PYTHON) + ); + this.registerCommand(EXTENSION_COMMANDS.MDB_EXPORT_TO_JAVA, () => + this._playgroundController.exportToLanguage(ExportToLanguages.JAVA) + ); + this.registerCommand(EXTENSION_COMMANDS.MDB_EXPORT_TO_CSHARP, () => + this._playgroundController.exportToLanguage(ExportToLanguages.CSHARP) + ); + this.registerCommand(EXTENSION_COMMANDS.MDB_EXPORT_TO_NODE, () => + this._playgroundController.exportToLanguage(ExportToLanguages.JAVASCRIPT) + ); + this.registerCommand(EXTENSION_COMMANDS.MDB_CHANGE_EXPORT_TO_LANGUAGE_ADDONS, (exportToLanguageAddons) => + this._playgroundController.changeExportToLanguageAddons(exportToLanguageAddons) + ); this.registerCommand( EXTENSION_COMMANDS.MDB_OPEN_MONGODB_DOCUMENT_FROM_CODE_LENS, (data: EditDocumentInfo) => { diff --git a/src/test/suite/editors/codeActionProvider.test.ts b/src/test/suite/editors/codeActionProvider.test.ts index 5c1154c55..e8d4a6854 100644 --- a/src/test/suite/editors/codeActionProvider.test.ts +++ b/src/test/suite/editors/codeActionProvider.test.ts @@ -4,10 +4,12 @@ import chai from 'chai'; import sinon from 'sinon'; import ActiveDBCodeLensProvider from '../../../editors/activeConnectionCodeLensProvider'; +import ExportToLanguageCodeLensProvider from '../../../editors/exportToLanguageCodeLensProvider'; import CodeActionProvider from '../../../editors/codeActionProvider'; import { ExplorerController } from '../../../explorer'; import { LanguageServerController } from '../../../language'; import { PlaygroundController } from '../../../editors'; +import { PlaygroundResult, ExportToLanguageMode } from '../../../types/playgroundType'; import { mdbTestExtension } from '../stubbableMdbExtension'; import { TestExtensionContext } from '../stubs'; @@ -43,6 +45,8 @@ suite('Code Action Provider Test Suite', function () { const testActiveDBCodeLensProvider = new ActiveDBCodeLensProvider( mdbTestExtension.testExtensionController._connectionController ); + const testExportToLanguageCodeLensProvider = new ExportToLanguageCodeLensProvider(); + const testCodeActionProvider = new CodeActionProvider(); const testExplorerController = new ExplorerController( mdbTestExtension.testExtensionController._connectionController ); @@ -55,6 +59,8 @@ suite('Code Action Provider Test Suite', function () { mdbTestExtension.testExtensionController._statusView, mdbTestExtension.testExtensionController._playgroundResultViewProvider, testActiveDBCodeLensProvider, + testExportToLanguageCodeLensProvider, + testCodeActionProvider, testExplorerController ); @@ -82,17 +88,24 @@ suite('Code Action Provider Test Suite', function () { sinon.restore(); }); - test('expected provideCodeActions to return undefined when text is not selected', () => { - const testCodeActionProvider = new CodeActionProvider(mdbTestExtension.testExtensionController._playgroundController); + test('returns undefined when text is not selected', () => { + const testCodeActionProvider = new CodeActionProvider(); const codeActions = testCodeActionProvider.provideCodeActions(); expect(codeActions).to.be.undefined; }); - test('expected provideCodeActions to return a run selected playground blocks action', async () => { + test('returns a run selected playground blocks action', async () => { mdbTestExtension.testExtensionController._playgroundController._selectedText = '123'; - const testCodeActionProvider = new CodeActionProvider(mdbTestExtension.testExtensionController._playgroundController); + const selection = { + start: { line: 0, character: 0 }, + end: { line: 0, character: 4 } + } as vscode.Selection; + const testCodeActionProvider = new CodeActionProvider(); + + testCodeActionProvider.refresh({ selection, mode: ExportToLanguageMode.OTHER }); + const codeActions = testCodeActionProvider.provideCodeActions(); expect(codeActions).to.exist; @@ -100,7 +113,6 @@ suite('Code Action Provider Test Suite', function () { if (codeActions) { expect(codeActions.length).to.be.equal(1); const actionCommand = codeActions[0].command; - expect(codeActions).to.exist; if (actionCommand) { expect(actionCommand.command).to.be.equal('mdb.runSelectedPlaygroundBlocks'); @@ -108,10 +120,186 @@ suite('Code Action Provider Test Suite', function () { await vscode.commands.executeCommand(actionCommand.command); - const expectedResult = { namespace: null, type: 'number', content: 123 }; + const expectedResult = { namespace: null, type: 'number', content: 123, language: 'plaintext' }; expect(mdbTestExtension.testExtensionController._playgroundController._playgroundResult).to.be.deep.equal(expectedResult); expect(mdbTestExtension.testExtensionController._playgroundController._isPartialRun).to.be.equal(true); } } }); + + test('exports to java and includes builders', async () => { + const textFromEditor = "{ name: '22' }"; + const selection = { + start: { line: 0, character: 0 }, + end: { line: 0, character: 14 } + } as vscode.Selection; + const mode = ExportToLanguageMode.QUERY; + + mdbTestExtension.testExtensionController._playgroundController._selectedText = textFromEditor; + mdbTestExtension.testExtensionController._playgroundController._codeActionProvider.selection = selection; + mdbTestExtension.testExtensionController._playgroundController._codeActionProvider.mode = mode; + + const fakeGetAllText = sinon.fake.returns(textFromEditor); + sinon.replace( + mdbTestExtension.testExtensionController._playgroundController, + '_getAllText', + fakeGetAllText + ); + const testCodeActionProvider = new CodeActionProvider(); + + testCodeActionProvider.refresh({ selection, mode }); + + const codeActions = testCodeActionProvider.provideCodeActions(); + + expect(codeActions).to.exist; + + if (codeActions) { + expect(codeActions.length).to.be.equal(5); + const actionCommand = codeActions[2].command; + + if (actionCommand) { + expect(actionCommand.command).to.be.equal('mdb.exportToJava'); + expect(actionCommand.title).to.be.equal('Export To Java'); + + await vscode.commands.executeCommand(actionCommand.command); + + const expectedResult = { namespace: null, type: null, content: 'new Document("name", "22")', language: 'java' }; + + const codeLenses = mdbTestExtension.testExtensionController._playgroundController._exportToLanguageCodeLensProvider.provideCodeLenses(); + expect(codeLenses.length).to.be.equal(3); + + // Only java queries supports builders. + await vscode.commands.executeCommand('mdb.changeExportToLanguageAddons', { + ...mdbTestExtension.testExtensionController._playgroundController._exportToLanguageCodeLensProvider._exportToLanguageAddons, + builders: true + }); + + expectedResult.content = 'eq("name", "22")'; + expect(mdbTestExtension.testExtensionController._playgroundController._playgroundResult).to.be.deep.equal(expectedResult); + } + } + }); + + test('exports to csharp and includes import statements', async () => { + const textFromEditor = "{ name: '22' }"; + const selection = { + start: { line: 0, character: 0 }, + end: { line: 0, character: 14 } + } as vscode.Selection; + const mode = ExportToLanguageMode.QUERY; + + mdbTestExtension.testExtensionController._playgroundController._selectedText = textFromEditor; + mdbTestExtension.testExtensionController._playgroundController._codeActionProvider.selection = selection; + mdbTestExtension.testExtensionController._playgroundController._codeActionProvider.mode = mode; + + const fakeGetAllText = sinon.fake.returns(textFromEditor); + sinon.replace( + mdbTestExtension.testExtensionController._playgroundController, + '_getAllText', + fakeGetAllText + ); + const testCodeActionProvider = new CodeActionProvider(); + + testCodeActionProvider.refresh({ selection, mode }); + + const codeActions = testCodeActionProvider.provideCodeActions(); + + expect(codeActions).to.exist; + + if (codeActions) { + expect(codeActions.length).to.be.equal(5); + const actionCommand = codeActions[3].command; + + if (actionCommand) { + expect(actionCommand.command).to.be.equal('mdb.exportToCsharp'); + expect(actionCommand.title).to.be.equal('Export To C#'); + + await vscode.commands.executeCommand(actionCommand.command); + + const expectedResult = { + namespace: null, + type: null, + content: 'new BsonDocument(\"name\", \"22\")', + language: 'csharp' + }; + expect(mdbTestExtension.testExtensionController._playgroundController._playgroundResult).to.be.deep.equal(expectedResult); + + const codeLenses = mdbTestExtension.testExtensionController._playgroundController._exportToLanguageCodeLensProvider.provideCodeLenses(); + expect(codeLenses.length).to.be.equal(2); + + // Csharp does not support driver syntax. + await vscode.commands.executeCommand('mdb.changeExportToLanguageAddons', { + ...mdbTestExtension.testExtensionController._playgroundController._exportToLanguageCodeLensProvider._exportToLanguageAddons, + importStatements: true + }); + + expectedResult.content = 'using MongoDB.Bson;\nusing MongoDB.Driver;\n\nnew BsonDocument(\"name\", \"22\")'; + expect(mdbTestExtension.testExtensionController._playgroundController._playgroundResult).to.be.deep.equal(expectedResult); + } + } + }); + + test('exports to python and includes driver syntax', async () => { + const textFromEditor = "use('db'); db.coll.find({ name: '22' })"; + const selection = { + start: { line: 0, character: 24 }, + end: { line: 0, character: 38 } + } as vscode.Selection; + const mode = ExportToLanguageMode.QUERY; + + mdbTestExtension.testExtensionController._playgroundController._selectedText = "{ name: '22' }"; + mdbTestExtension.testExtensionController._playgroundController._codeActionProvider.selection = selection; + mdbTestExtension.testExtensionController._playgroundController._codeActionProvider.mode = mode; + + const fakeGetAllText = sinon.fake.returns(textFromEditor); + sinon.replace( + mdbTestExtension.testExtensionController._playgroundController, + '_getAllText', + fakeGetAllText + ); + const testCodeActionProvider = new CodeActionProvider(); + + testCodeActionProvider.refresh({ selection, mode }); + + const codeActions = testCodeActionProvider.provideCodeActions(); + + expect(codeActions).to.exist; + + if (codeActions) { + expect(codeActions.length).to.be.equal(5); + const actionCommand = codeActions[1].command; + + if (actionCommand) { + expect(actionCommand.command).to.be.equal('mdb.exportToPython'); + expect(actionCommand.title).to.be.equal('Export To Python 3'); + + await vscode.commands.executeCommand(actionCommand.command); + + let expectedResult: PlaygroundResult = { + namespace: null, + type: null, + content: "{\n 'name': '22'\n}", + language: 'python' + }; + expect(mdbTestExtension.testExtensionController._playgroundController._playgroundResult).to.be.deep.equal(expectedResult); + + const codeLenses = mdbTestExtension.testExtensionController._playgroundController._exportToLanguageCodeLensProvider.provideCodeLenses(); + expect(codeLenses.length).to.be.equal(2); + + await vscode.commands.executeCommand('mdb.changeExportToLanguageAddons', { + ...mdbTestExtension.testExtensionController._playgroundController._exportToLanguageCodeLensProvider._exportToLanguageAddons, + driverSyntax: true + }); + + expectedResult = { + namespace: 'db.coll', + type: null, + content: "# Requires the PyMongo package.\n# https://api.mongodb.com/python/current\n\nclient = MongoClient('mongodb://localhost:27018/?readPreference=primary&appname=mongodb-vscode+0.0.0-dev.0&directConnection=true&ssl=false')\nresult = client['db']['coll'].aggregate({\n 'name': '22'\n})", + language: 'python' + }; + + expect(mdbTestExtension.testExtensionController._playgroundController._playgroundResult).to.be.deep.equal(expectedResult); + } + } + }); }); diff --git a/src/test/suite/editors/editDocumentCodeLensProvider.test.ts b/src/test/suite/editors/editDocumentCodeLensProvider.test.ts index 218ea1172..9d12b721a 100644 --- a/src/test/suite/editors/editDocumentCodeLensProvider.test.ts +++ b/src/test/suite/editors/editDocumentCodeLensProvider.test.ts @@ -123,7 +123,8 @@ suite('Edit Document Code Lens Provider Test Suite', () => { testCodeLensProvider.updateCodeLensesForPlayground({ namespace: 'db.coll', type: 'Document', - content: { _id: '93333a0d-83f6-4e6f-a575-af7ea6187a4a' } + content: { _id: '93333a0d-83f6-4e6f-a575-af7ea6187a4a' }, + language: 'json' }); const codeLens = testCodeLensProvider.provideCodeLenses(); @@ -164,7 +165,8 @@ suite('Edit Document Code Lens Provider Test Suite', () => { content: [ { _id: '93333a0d-83f6-4e6f-a575-af7ea6187a4a' }, { _id: '21333a0d-83f6-4e6f-a575-af7ea6187444' } - ] + ], + language: 'json' }); const codeLens = testCodeLensProvider.provideCodeLenses(); @@ -226,7 +228,8 @@ suite('Edit Document Code Lens Provider Test Suite', () => { $date: '2014-04-04T21:23:13.331Z' } } - ] + ], + language: 'json' }); const codeLens = testCodeLensProvider.provideCodeLenses(); diff --git a/src/test/suite/editors/exportToLanguageCodeLensProvider.test.ts b/src/test/suite/editors/exportToLanguageCodeLensProvider.test.ts new file mode 100644 index 000000000..330a26285 --- /dev/null +++ b/src/test/suite/editors/exportToLanguageCodeLensProvider.test.ts @@ -0,0 +1,83 @@ +import { beforeEach } from 'mocha'; +import chai from 'chai'; + +import ExportToLanguageCodeLensProvider from '../../../editors/exportToLanguageCodeLensProvider'; +import { ExportToLanguageMode } from '../../../types/playgroundType'; + +const expect = chai.expect; + +suite('Export To Language Code Lens Provider Test Suite', function () { + const defaults = { + importStatements: false, + driverSyntax: false, + builders: false, + language: 'shell' + }; + let testExportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; + + beforeEach(() => { + testExportToLanguageCodeLensProvider = new ExportToLanguageCodeLensProvider(); + }); + + test('has the include import statements code lens when importStatements is false', () => { + testExportToLanguageCodeLensProvider.refresh(defaults); + + const codeLenses = testExportToLanguageCodeLensProvider.provideCodeLenses(); + + expect(codeLenses.length).to.be.equal(2); + expect(codeLenses[0].command?.title).to.be.equal('Include Import Statements'); + }); + + test('has the exclude import statements code lens when importStatements is true', () => { + testExportToLanguageCodeLensProvider.refresh({ ...defaults, importStatements: true }); + + const codeLenses = testExportToLanguageCodeLensProvider.provideCodeLenses(); + + expect(codeLenses.length).to.be.equal(2); + expect(codeLenses[0].command?.title).to.be.equal('Exclude Import Statements'); + }); + + test('has the include import statements code lens when driverSyntax is false', () => { + testExportToLanguageCodeLensProvider.refresh(defaults); + + const codeLenses = testExportToLanguageCodeLensProvider.provideCodeLenses(); + + expect(codeLenses.length).to.be.equal(2); + expect(codeLenses[1].command?.title).to.be.equal('Include Driver Syntax'); + }); + + test('has the exclude import statements code lens when driverSyntax is true', () => { + testExportToLanguageCodeLensProvider.refresh({ ...defaults, driverSyntax: true }); + + const codeLenses = testExportToLanguageCodeLensProvider.provideCodeLenses(); + + expect(codeLenses.length).to.be.equal(2); + expect(codeLenses[1].command?.title).to.be.equal('Exclude Driver Syntax'); + }); + + test('has the use builders code lens when builders is false, language is java, and mode is query', () => { + testExportToLanguageCodeLensProvider.refresh({ ...defaults, mode: ExportToLanguageMode.QUERY, language: 'java' }); + + const codeLenses = testExportToLanguageCodeLensProvider.provideCodeLenses(); + + expect(codeLenses.length).to.be.equal(3); + expect(codeLenses[2].command?.title).to.be.equal('Use Builders'); + }); + + test('has the use raw query code lens when builders is true, language is java, and mode is query', () => { + testExportToLanguageCodeLensProvider.refresh({ ...defaults, builders: true, mode: ExportToLanguageMode.QUERY, language: 'java' }); + + const codeLenses = testExportToLanguageCodeLensProvider.provideCodeLenses(); + + expect(codeLenses.length).to.be.equal(3); + expect(codeLenses[2].command?.title).to.be.equal('Use Raw Query'); + }); + + test('does not have the use raw query code lens when builders is true, language is java, and mode is plain text', () => { + testExportToLanguageCodeLensProvider.refresh({ ...defaults, builders: true, mode: ExportToLanguageMode.OTHER, language: 'java' }); + + const codeLenses = testExportToLanguageCodeLensProvider.provideCodeLenses(); + + expect(codeLenses.length).to.be.equal(2); + }); +}); diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index f42cf4dbc..349058cee 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -4,6 +4,7 @@ import chai from 'chai'; import sinon from 'sinon'; import ActiveDBCodeLensProvider from '../../../editors/activeConnectionCodeLensProvider'; +import ExportToLanguageCodeLensProvider from '../../../editors/exportToLanguageCodeLensProvider'; import ConnectionController from '../../../connectionController'; import { ConnectionModel } from '../../../types/connectionModelType'; import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensProvider'; @@ -16,6 +17,7 @@ import { StorageController } from '../../../storage'; import TelemetryService from '../../../telemetry/telemetryService'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { TestExtensionContext, MockLanguageServerController } from '../stubs'; +import CodeActionProvider from '../../../editors/codeActionProvider'; const expect = chai.expect; @@ -58,6 +60,8 @@ suite('Playground Controller Test Suite', function () { const testActiveDBCodeLensProvider = new ActiveDBCodeLensProvider( testConnectionController ); + const testExportToLanguageCodeLensProvider = new ExportToLanguageCodeLensProvider(); + const testCodeActionProvider = new CodeActionProvider(); const testExplorerController = new ExplorerController( testConnectionController ); @@ -69,6 +73,8 @@ suite('Playground Controller Test Suite', function () { testStatusView, testPlaygroundResultProvider, testActiveDBCodeLensProvider, + testExportToLanguageCodeLensProvider, + testCodeActionProvider, testExplorerController ); const sandbox = sinon.createSandbox(); @@ -459,6 +465,8 @@ suite('Playground Controller Test Suite', function () { testStatusView, testPlaygroundResultProvider, testActiveDBCodeLensProvider, + testExportToLanguageCodeLensProvider, + testCodeActionProvider, testExplorerController ); @@ -466,83 +474,6 @@ suite('Playground Controller Test Suite', function () { activeTestEditorMock ); }); - - test('getDocumentLanguage returns json if content is object', async () => { - await vscode.workspace - .getConfiguration('mdb') - .update('confirmRunAll', false); - const language = testPlaygroundController._getDocumentLanguage({ - test: 'value' - }); - - expect(language).to.be.equal('json'); - }); - - test('getDocumentLanguage returns json if content is array', async () => { - await vscode.workspace - .getConfiguration('mdb') - .update('confirmRunAll', false); - const language = testPlaygroundController._getDocumentLanguage([ - { test: 'value' } - ]); - - expect(language).to.be.equal('json'); - }); - - test('getDocumentLanguage returns json if content is object with BSON value', async () => { - await vscode.workspace - .getConfiguration('mdb') - .update('confirmRunAll', false); - const language = testPlaygroundController._getDocumentLanguage({ - _id: { - $oid: '5d973ae7443762aae72a160' - } - }); - - expect(language).to.be.equal('json'); - }); - - test('getDocumentLanguage returns plaintext if content is string', async () => { - await vscode.workspace - .getConfiguration('mdb') - .update('confirmRunAll', false); - const language = testPlaygroundController._getDocumentLanguage( - 'I am a string' - ); - - expect(language).to.be.equal('plaintext'); - }); - - test('getDocumentLanguage returns plaintext if content is number', async () => { - await vscode.workspace - .getConfiguration('mdb') - .update('confirmRunAll', false); - const language = testPlaygroundController._getDocumentLanguage(12); - - expect(language).to.be.equal('plaintext'); - }); - - test('getDocumentLanguage returns plaintext if content is undefined', async () => { - await vscode.workspace - .getConfiguration('mdb') - .update('confirmRunAll', false); - const language = testPlaygroundController._getDocumentLanguage( - undefined - ); - - expect(language).to.be.equal('plaintext'); - }); - - test('getDocumentLanguage returns plaintext if content is null', async () => { - await vscode.workspace - .getConfiguration('mdb') - .update('confirmRunAll', false); - const language = testPlaygroundController._getDocumentLanguage( - undefined - ); - - expect(language).to.be.equal('plaintext'); - }); }); }); }); diff --git a/src/test/suite/editors/playgroundResultProvider.test.ts b/src/test/suite/editors/playgroundResultProvider.test.ts index 5e78e7e4b..85e7ae57f 100644 --- a/src/test/suite/editors/playgroundResultProvider.test.ts +++ b/src/test/suite/editors/playgroundResultProvider.test.ts @@ -48,7 +48,8 @@ suite('Playground Result Provider Test Suite', () => { { namespace: null, type: null, - content: undefined + content: undefined, + language: null } ); }); @@ -64,7 +65,8 @@ suite('Playground Result Provider Test Suite', () => { content: { _id: '93333a0d-83f6-4e6f-a575-af7ea6187a4a', name: 'Berlin' - } + }, + language: 'json' }; testPlaygroundResultViewProvider.setPlaygroundResult(playgroundResult); @@ -83,7 +85,8 @@ suite('Playground Result Provider Test Suite', () => { testPlaygroundResultViewProvider._playgroundResult = { namespace: 'db.berlin', type: 'undefined', - content: null + content: null, + language: 'plaintext' }; const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); @@ -100,7 +103,8 @@ suite('Playground Result Provider Test Suite', () => { testPlaygroundResultViewProvider._playgroundResult = { namespace: 'db.berlin', type: 'object', - content: null + content: null, + language: 'plaintext' }; const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); @@ -117,7 +121,8 @@ suite('Playground Result Provider Test Suite', () => { testPlaygroundResultViewProvider._playgroundResult = { namespace: 'db.berlin', type: 'number', - content: 4 + content: 4, + language: 'plaintext' }; const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); @@ -134,7 +139,8 @@ suite('Playground Result Provider Test Suite', () => { testPlaygroundResultViewProvider._playgroundResult = { namespace: 'db.berlin', type: 'object', - content: [] + content: [], + language: 'json' }; const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); @@ -151,7 +157,8 @@ suite('Playground Result Provider Test Suite', () => { testPlaygroundResultViewProvider._playgroundResult = { namespace: 'db.berlin', type: 'object', - content: {} + content: {}, + language: 'json' }; const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); @@ -168,7 +175,8 @@ suite('Playground Result Provider Test Suite', () => { testPlaygroundResultViewProvider._playgroundResult = { namespace: 'db.berlin', type: 'boolean', - content: true + content: true, + language: 'plaintext' }; const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); @@ -185,7 +193,8 @@ suite('Playground Result Provider Test Suite', () => { testPlaygroundResultViewProvider._playgroundResult = { namespace: 'db.berlin', type: 'string', - content: 'Berlin' + content: 'Berlin', + language: 'plaintext' }; const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); @@ -211,7 +220,8 @@ suite('Playground Result Provider Test Suite', () => { const playgroundResult = { namespace: 'db.berlin', type: 'Cursor', - content + content, + language: 'json' }; const mockRefresh: any = sinon.fake(); @@ -242,7 +252,8 @@ suite('Playground Result Provider Test Suite', () => { const playgroundResult = { namespace: 'db.berlin', type: 'Document', - content + content, + language: 'json' }; const mockRefresh: any = sinon.fake(); @@ -273,7 +284,8 @@ suite('Playground Result Provider Test Suite', () => { content: [ { _id: 1, item: 'abc', price: 10, quantity: 2, date: new Date('2014-03-01T08:00:00Z') }, { _id: 2, item: 'jkl', price: 20, quantity: 1, date: new Date('2014-03-01T09:00:00Z') } - ] + ], + language: 'json' }; const connectionId = '1c8c2b06-fbfb-40b7-bd8a-bd1f8333a487'; diff --git a/src/test/suite/language/languageServerController.test.ts b/src/test/suite/language/languageServerController.test.ts index bf2333a13..8fe3ff418 100644 --- a/src/test/suite/language/languageServerController.test.ts +++ b/src/test/suite/language/languageServerController.test.ts @@ -5,6 +5,7 @@ import path from 'path'; import sinon from 'sinon'; import ActiveDBCodeLensProvider from '../../../editors/activeConnectionCodeLensProvider'; +import ExportToLanguageCodeLensProvider from '../../../editors/exportToLanguageCodeLensProvider'; import ConnectionController from '../../../connectionController'; import { DataServiceType } from '../../../types/dataServiceType'; import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensProvider'; @@ -19,6 +20,7 @@ import { StorageController } from '../../../storage'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import TelemetryService from '../../../telemetry/telemetryService'; import { TestExtensionContext } from '../stubs'; +import CodeActionProvider from '../../../editors/codeActionProvider'; const expect = chai.expect; @@ -56,6 +58,8 @@ suite('Language Server Controller Test Suite', () => { const testExplorerController = new ExplorerController( testConnectionController ); + const testExportToLanguageCodeLensProvider = new ExportToLanguageCodeLensProvider(); + const testCodeActionProvider = new CodeActionProvider(); const testPlaygroundController = new PlaygroundController( mockExtensionContext, testConnectionController, @@ -64,6 +68,8 @@ suite('Language Server Controller Test Suite', () => { testStatusView, testPlaygroundResultProvider, testActiveDBCodeLensProvider, + testExportToLanguageCodeLensProvider, + testCodeActionProvider, testExplorerController ); diff --git a/src/test/suite/language/mongoDBService.test.ts b/src/test/suite/language/mongoDBService.test.ts index 181b7ed17..3de1695be 100644 --- a/src/test/suite/language/mongoDBService.test.ts +++ b/src/test/suite/language/mongoDBService.test.ts @@ -985,7 +985,7 @@ suite('MongoDBService Test Suite', () => { ); const expectedResult = { outputLines: [], - result: { namespace: null, type: 'number', content: 2 } + result: { namespace: null, type: 'number', content: 2, language: 'plaintext' } }; expect(result).to.deep.equal(expectedResult); @@ -1015,7 +1015,7 @@ suite('MongoDBService Test Suite', () => { ); const expectedResult = { outputLines: [], - result: { namespace: null, type: 'number', content: 3 } + result: { namespace: null, type: 'number', content: 3, language: 'plaintext' } }; expect(result).to.deep.equal(expectedResult); @@ -1032,7 +1032,7 @@ suite('MongoDBService Test Suite', () => { ); const firstRes = { outputLines: [], - result: { namespace: null, type: 'number', content: 2 } + result: { namespace: null, type: 'number', content: 2, language: 'plaintext' } }; expect(firstEvalResult).to.deep.equal(firstRes); @@ -1046,7 +1046,7 @@ suite('MongoDBService Test Suite', () => { ); const secondRes = { outputLines: [], - result: { namespace: null, type: 'number', content: 3 } + result: { namespace: null, type: 'number', content: 3, language: 'plaintext' } }; expect(secondEvalResult).to.deep.equal(secondRes); @@ -1072,7 +1072,102 @@ suite('MongoDBService Test Suite', () => { _id: { $oid: '5fb292760ece2dc9c0362075' } - } + }, + language: 'json' + } + }; + + expect(result).to.deep.equal(expectedResult); + }); + + test('evaluate returns an object', async () => { + const source = new CancellationTokenSource(); + const result = await testMongoDBService.executeAll( + { + connectionId: 'pineapple', + codeToEvaluate: `const obj = { name: "a short string" }; + obj` + }, + source.token + ); + const expectedResult = { + outputLines: [], + result: { + namespace: null, + type: 'object', + content: { + name: 'a short string' + }, + language: 'json' + } + }; + + expect(result).to.deep.equal(expectedResult); + }); + + test('evaluate returns an array', async () => { + const source = new CancellationTokenSource(); + const result = await testMongoDBService.executeAll( + { + connectionId: 'pineapple', + codeToEvaluate: `const arr = [{ name: "a short string" }]; + arr` + }, + source.token + ); + const expectedResult = { + outputLines: [], + result: { + namespace: null, + type: 'object', + content: [{ + name: 'a short string' + }], + language: 'json' + } + }; + + expect(result).to.deep.equal(expectedResult); + }); + + test('evaluate returns undefined', async () => { + const source = new CancellationTokenSource(); + const result = await testMongoDBService.executeAll( + { + connectionId: 'pineapple', + codeToEvaluate: 'undefined' + }, + source.token + ); + const expectedResult = { + outputLines: [], + result: { + namespace: null, + type: 'undefined', + content: undefined, + language: 'plaintext' + } + }; + + expect(result).to.deep.equal(expectedResult); + }); + + test('evaluate returns null', async () => { + const source = new CancellationTokenSource(); + const result = await testMongoDBService.executeAll( + { + connectionId: 'pineapple', + codeToEvaluate: 'null' + }, + source.token + ); + const expectedResult = { + outputLines: [], + result: { + namespace: null, + type: 'object', + content: null, + language: 'plaintext' } }; @@ -1094,7 +1189,8 @@ suite('MongoDBService Test Suite', () => { result: { namespace: null, type: 'string', - content: 'A single line string' + content: 'A single line string', + language: 'plaintext' } }; @@ -1120,7 +1216,8 @@ suite('MongoDBService Test Suite', () => { type: 'string', content: `vscode is - awesome` + awesome`, + language: 'plaintext' } }; @@ -1138,12 +1235,12 @@ suite('MongoDBService Test Suite', () => { ); const expectedResult = { outputLines: [ - { namespace: null, type: null, content: 'Hello' }, - { namespace: null, type: null, content: 1 }, - { namespace: null, type: null, content: 2 }, - { namespace: null, type: null, content: 3 } + { namespace: null, type: null, content: 'Hello', language: null }, + { namespace: null, type: null, content: 1, language: null }, + { namespace: null, type: null, content: 2, language: null }, + { namespace: null, type: null, content: 3, language: null } ], - result: { namespace: null, type: 'number', content: 42 } + result: { namespace: null, type: 'number', content: 42, language: 'plaintext' } }; expect(result).to.deep.equal(expectedResult); diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 0e340fc81..34792d197 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -1532,7 +1532,7 @@ suite('MDBExtensionController Test Suite', function () { ); }); - test('mdb.createIndexFromTreeView should create a MongoDB playground with search template', async () => { + test('mdb.createIndexFromTreeView should create a MongoDB playground with index template', async () => { const mockOpenTextDocument: any = sinon.fake.resolves('untitled'); sinon.replace(vscode.workspace, 'openTextDocument', mockOpenTextDocument); diff --git a/src/test/suite/stubs.ts b/src/test/suite/stubs.ts index 3bab3a3d2..1a34f62c7 100644 --- a/src/test/suite/stubs.ts +++ b/src/test/suite/stubs.ts @@ -5,7 +5,7 @@ import path = require('path'); import { StorageController } from '../../storage'; -import type { ShellExecuteAllResult } from '../../types/playgroundType'; +import { ShellExecuteAllResult, ExportToLanguageMode, ExportToLanguageNamespace } from '../../types/playgroundType'; // Bare mock of the extension context for vscode. class TestExtensionContext implements vscode.ExtensionContext { @@ -241,13 +241,21 @@ class MockLanguageServerController { return; } - executeAll(/* codeToEvaluate: string*/): Promise { + executeAll(/* codeToEvaluate: string */): Promise { return Promise.resolve({ outputLines: [], - result: { namespace: null, type: null, content: 'Result' } + result: { namespace: null, type: null, content: 'Result', language: 'plaintext' } }); } + getExportToLanguageMode(/* params: PlaygroundTextAndSelection */): Promise { + return Promise.resolve(ExportToLanguageMode.OTHER); + } + + getNamespaceForSelection(/* params: PlaygroundTextAndSelection */): Promise { + return Promise.resolve({ databaseName: null, collectionName: null }); + } + connectToServiceProvider(/* params: { connectionString?: string; connectionOptions?: any; diff --git a/src/test/suite/telemetry/telemetryService.test.ts b/src/test/suite/telemetry/telemetryService.test.ts index 895270fb6..55db670bb 100644 --- a/src/test/suite/telemetry/telemetryService.test.ts +++ b/src/test/suite/telemetry/telemetryService.test.ts @@ -343,7 +343,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert AggregationCursor shellApiType to aggregation telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'AggregationCursor', content: '' } + result: { namespace: null, type: 'AggregationCursor', content: '', language: 'plaintext' }, }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -353,7 +353,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert BulkWriteResult shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'BulkWriteResult', content: '' } + result: { namespace: null, type: 'BulkWriteResult', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -363,7 +363,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert Collection shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'Collection', content: '' } + result: { namespace: null, type: 'Collection', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -373,7 +373,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert Cursor shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'Cursor', content: '' } + result: { namespace: null, type: 'Cursor', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -383,7 +383,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert Database shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'Database', content: '' } + result: { namespace: null, type: 'Database', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -393,7 +393,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert DeleteResult shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'DeleteResult', content: '' } + result: { namespace: null, type: 'DeleteResult', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -403,7 +403,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert InsertManyResult shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'InsertManyResult', content: '' } + result: { namespace: null, type: 'InsertManyResult', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -413,7 +413,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert InsertOneResult shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'InsertOneResult', content: '' } + result: { namespace: null, type: 'InsertOneResult', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -423,7 +423,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert ReplicaSet shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'ReplicaSet', content: '' } + result: { namespace: null, type: 'ReplicaSet', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -433,7 +433,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert Shard shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'Shard', content: '' } + result: { namespace: null, type: 'Shard', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -443,7 +443,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert ShellApi shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'ShellApi', content: '' } + result: { namespace: null, type: 'ShellApi', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -453,7 +453,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert UpdateResult shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { namespace: null, type: 'UpdateResult', content: '' } + result: { namespace: null, type: 'UpdateResult', content: '', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); @@ -463,7 +463,7 @@ suite('Telemetry Controller Test Suite', () => { test('return other telemetry type if evaluation returns a string', () => { const res = { outputLines: [], - result: { namespace: null, type: null, content: '2' } + result: { namespace: null, type: null, content: '2', language: 'plaintext' } }; const type = testTelemetryService.getPlaygroundResultType(res); diff --git a/src/types/playgroundType.ts b/src/types/playgroundType.ts index 3affcac03..993391fa8 100644 --- a/src/types/playgroundType.ts +++ b/src/types/playgroundType.ts @@ -1,7 +1,10 @@ +import * as vscode from 'vscode'; + export type OutputItem = { namespace: string | null; type: string | null; content: any; + language: string | null; }; export type PlaygroundDebug = OutputItem[] | undefined; @@ -17,3 +20,37 @@ export type PlaygroundExecuteParameters = { codeToEvaluate: string; connectionId: string; }; + +export interface ExportToLanguageAddons { + textFromEditor?: string; + selectedText?: string; + selection?: vscode.Selection; + importStatements: boolean; + driverSyntax: boolean; + builders: boolean; + language: string; + mode?: ExportToLanguageMode; +} + +export interface PlaygroundTextAndSelection { + textFromEditor: string; + selection: vscode.Selection; +} + +export enum ExportToLanguages { + PYTHON = 'python', + JAVA = 'java', + CSHARP = 'csharp', + JAVASCRIPT = 'javascript' +} + +export enum ExportToLanguageMode { + QUERY = 'QUERY', + AGGREGATION = 'AGGREGATION', + OTHER = 'OTHER' +} + +export interface ExportToLanguageNamespace { + databaseName: string | null; + collectionName: string | null; +} diff --git a/webpack.config.js b/webpack.config.js index 896bc6780..eb8514ed5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,6 +4,8 @@ const path = require('path'); const autoprefixer = require('autoprefixer'); const outputPath = path.join(__dirname, 'dist'); +const ContextMapPlugin = require('context-map-webpack-plugin'); + const baseConfig = { devtool: 'source-map' // performance: { @@ -47,7 +49,13 @@ const extensionConfig = { loader: 'node-loader' } ] - } + }, + plugins: [ + new ContextMapPlugin( + 'node_modules/context-eval', + ['./lib/context-node'] + ) + ] }; const languageServerConfig = {