From 7eede491d6cdf7653757311fc6959ed226f01576 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 11 Jan 2021 21:31:44 +0100 Subject: [PATCH] VSCODE-222: Edit documents from playground results (#232) * feat: edit documents from playground results (VSCODE-222) * test: add tests for document controller and edit code lenses provider * refactor: check content type instead of the whole result object * refactor: use the same playground result file instead of reopen it * feat: show and write mongodb documents using a FileSystemProvider (VSCODE-223) * test: fix some long-running tests * refactor: do not refresh playground results after saving document * refactor: errors clean up * refactor: reset memory file system provider * refactor: remove repeated code * test: update names to be more specific * refactor: set language not extension for playground results * refactor: update code lenses position in code lenses provider * refactor: update comments in code * refactor: use document controller only accesing database * refactor: rename document controller to service * refactor: return lost function parameter --- package-lock.json | 151 +++++- package.json | 12 +- src/commands/index.ts | 5 +- .../activeConnectionCodeLensProvider.ts | 16 +- src/editors/collectionDocumentsProvider.ts | 1 - src/editors/documentProvider.ts | 103 ---- src/editors/editDocumentCodeLensProvider.ts | 92 ++++ src/editors/editorsController.ts | 376 ++++++--------- src/editors/memoryFileSystemProvider.ts | 243 ++++++++++ src/editors/mongoDBDocumentService.ts | 154 ++++++ src/editors/playgroundController.ts | 264 ++++++----- src/editors/playgroundResultProvider.ts | 84 ++-- src/explorer/explorerTreeController.ts | 4 +- src/extension.ts | 1 + src/language/worker.ts | 19 +- src/mdbExtensionController.ts | 27 +- .../editors/activeDBCodeLensProvider.test.ts | 5 +- .../collectionDocumentsProvider.test.ts | 8 +- .../suite/editors/documentController.test.ts | 206 ++++++++ .../suite/editors/documentProvider.test.ts | 406 ---------------- .../editDocumentCodeLensProvider.test.ts | 134 ++++++ .../suite/editors/editorsController.test.ts | 128 ++++- .../editors/playgroundController.test.ts | 68 ++- .../editors/playgroundResultProvider.test.ts | 227 +++++++++ .../language/languageServerController.test.ts | 12 +- .../suite/language/mongoDBService.test.ts | 21 +- src/test/suite/mdbExtensionController.test.ts | 444 +----------------- src/test/suite/stubs.ts | 5 +- .../telemetry/telemetryController.test.ts | 26 +- src/utils/types.ts | 9 +- 30 files changed, 1809 insertions(+), 1442 deletions(-) delete mode 100644 src/editors/documentProvider.ts create mode 100644 src/editors/editDocumentCodeLensProvider.ts create mode 100644 src/editors/memoryFileSystemProvider.ts create mode 100644 src/editors/mongoDBDocumentService.ts create mode 100644 src/test/suite/editors/documentController.test.ts delete mode 100644 src/test/suite/editors/documentProvider.test.ts create mode 100644 src/test/suite/editors/editDocumentCodeLensProvider.test.ts create mode 100644 src/test/suite/editors/playgroundResultProvider.test.ts diff --git a/package-lock.json b/package-lock.json index 4c10ccc5..cf76a435 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1828,12 +1828,6 @@ "@types/react": "*" } }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, "@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", @@ -2152,41 +2146,138 @@ "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" }, "@typescript-eslint/eslint-plugin": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", - "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.12.0.tgz", + "integrity": "sha512-wHKj6q8s70sO5i39H2g1gtpCXCvjVszzj6FFygneNFyIAxRvNSVz9GML7XpqrB9t7hNutXw+MHnLN/Ih6uyB8Q==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/experimental-utils": "4.12.0", + "@typescript-eslint/scope-manager": "4.12.0", + "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", + "semver": "^7.3.2", "tsutils": "^3.17.1" } }, "@typescript-eslint/experimental-utils": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", - "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.12.0.tgz", + "integrity": "sha512-MpXZXUAvHt99c9ScXijx7i061o5HEjXltO+sbYfZAAHxv3XankQkPaNi5myy0Yh0Tyea3Hdq1pi7Vsh0GJb0fA==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.34.0", + "@typescript-eslint/scope-manager": "4.12.0", + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/typescript-estree": "4.12.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" + }, + "dependencies": { + "@typescript-eslint/typescript-estree": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz", + "integrity": "sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/visitor-keys": "4.12.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + } } }, "@typescript-eslint/parser": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", - "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.12.0.tgz", + "integrity": "sha512-9XxVADAo9vlfjfoxnjboBTxYOiNY93/QuvcPgsiKvHxW6tOZx1W4TvkIQ2jB3k5M0pbFP5FlXihLK49TjZXhuQ==", "dev": true, "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.34.0", - "@typescript-eslint/typescript-estree": "2.34.0", - "eslint-visitor-keys": "^1.1.0" + "@typescript-eslint/scope-manager": "4.12.0", + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/typescript-estree": "4.12.0", + "debug": "^4.1.1" + }, + "dependencies": { + "@typescript-eslint/typescript-estree": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz", + "integrity": "sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/visitor-keys": "4.12.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz", + "integrity": "sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/visitor-keys": "4.12.0" } }, + "@typescript-eslint/types": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.12.0.tgz", + "integrity": "sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g==", + "dev": true + }, "@typescript-eslint/typescript-estree": { "version": "2.34.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", @@ -2202,6 +2293,24 @@ "tsutils": "^3.17.1" } }, + "@typescript-eslint/visitor-keys": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz", + "integrity": "sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + } + } + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", diff --git a/package.json b/package.json index 36445b68..aaf82e06 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "onCommand:mdb.disconnect", "onCommand:mdb.removeConnection", "onCommand:mdb.openMongoDBShell", - "onCommand:mdb.saveDocumentToMongoDB", + "onCommand:mdb.saveMongoDBDocument", "onView:mongoDB", "onView:mongoDBConnectionExplorer", "onView:mongoDBPlaygroundsExplorer", @@ -220,7 +220,7 @@ "title": "MongoDB: Run Selected Lines From Playground" }, { - "command": "mdb.saveDocumentToMongoDB", + "command": "mdb.saveMongoDBDocument", "title": "MongoDB: Save Document To MongoDB" }, { @@ -539,7 +539,7 @@ ], "commandPalette": [ { - "command": "mdb.saveDocumentToMongoDB", + "command": "mdb.saveMongoDBDocument", "when": "editorLangId == json" }, { @@ -690,7 +690,7 @@ "when": "editorLangId == mongodb" }, { - "command": "mdb.saveDocumentToMongoDB", + "command": "mdb.saveMongoDBDocument", "key": "ctrl+s", "mac": "cmd+s", "when": "editorLangId == json" @@ -884,8 +884,8 @@ "@types/sinon": "^9.0.1", "@types/vscode": "^1.49.0", "@types/ws": "^7.2.4", - "@typescript-eslint/eslint-plugin": "^2.19.2", - "@typescript-eslint/parser": "^2.19.2", + "@typescript-eslint/eslint-plugin": "^4.12.0", + "@typescript-eslint/parser": "^4.12.0", "autoprefixer": "^9.7.5", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", diff --git a/src/commands/index.ts b/src/commands/index.ts index a70dc805..b0836391 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -14,7 +14,9 @@ enum EXTENSION_COMMANDS { MDB_RUN_ALL_PLAYGROUND_BLOCKS = 'mdb.runAllPlaygroundBlocks', MDB_RUN_ALL_OR_SELECTED_PLAYGROUND_BLOCKS = 'mdb.runPlayground', - MDB_SAVE_DOCUMENT_TO_MONGODB = 'mdb.saveDocumentToMongoDB', + MDB_OPEN_MONGODB_DOCUMENT_FROM_PLAYGROUND = 'mdb.openMongoDBDocumentFromPlayground', + MDB_OPEN_MONGODB_DOCUMENT_FROM_TREE = 'mdb.openMongoDBDocumentFromTree', + MDB_SAVE_MONGODB_DOCUMENT = 'mdb.saveMongoDBDocument', MDB_CHANGE_ACTIVE_CONNECTION = 'mdb.changeActiveConnection', MDB_REFRESH_PLAYGROUNDS = 'mdb.refreshPlaygrounds', @@ -43,7 +45,6 @@ enum EXTENSION_COMMANDS { MDB_ADD_COLLECTION = 'mdb.addCollection', MDB_COPY_COLLECTION_NAME = 'mdb.copyCollectionName', MDB_DROP_COLLECTION = 'mdb.dropCollection', - MDB_VIEW_DOCUMENT = 'mdb.viewDocument', MDB_VIEW_COLLECTION_DOCUMENTS = 'mdb.viewCollectionDocuments', MDB_REFRESH_COLLECTION = 'mdb.refreshCollection', MDB_REFRESH_DOCUMENT_LIST = 'mdb.refreshDocumentList', diff --git a/src/editors/activeConnectionCodeLensProvider.ts b/src/editors/activeConnectionCodeLensProvider.ts index 9736883b..b13c70df 100644 --- a/src/editors/activeConnectionCodeLensProvider.ts +++ b/src/editors/activeConnectionCodeLensProvider.ts @@ -1,16 +1,16 @@ import * as vscode from 'vscode'; import EXTENSION_COMMANDS from '../commands'; +import ConnectionController from '../connectionController'; export default class ActiveConnectionCodeLensProvider implements vscode.CodeLensProvider { - private _connectionController: any; - private _onDidChangeCodeLenses: vscode.EventEmitter< - void - > = new vscode.EventEmitter(); - public readonly onDidChangeCodeLenses: vscode.Event = this + _connectionController: ConnectionController; + _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); + + readonly onDidChangeCodeLenses: vscode.Event = this ._onDidChangeCodeLenses.event; - constructor(connectionController?: any) { + constructor(connectionController: ConnectionController) { this._connectionController = connectionController; vscode.workspace.onDidChangeConfiguration(() => { @@ -18,11 +18,11 @@ export default class ActiveConnectionCodeLensProvider }); } - public refresh(): void { + refresh(): void { this._onDidChangeCodeLenses.fire(); } - public provideCodeLenses(): vscode.CodeLens[] { + provideCodeLenses(): vscode.CodeLens[] { const codeLens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 0)); let message = ''; diff --git a/src/editors/collectionDocumentsProvider.ts b/src/editors/collectionDocumentsProvider.ts index 1e2da447..aceb08a5 100644 --- a/src/editors/collectionDocumentsProvider.ts +++ b/src/editors/collectionDocumentsProvider.ts @@ -5,7 +5,6 @@ import CollectionDocumentsOperationsStore from './collectionDocumentsOperationsS import ConnectionController from '../connectionController'; import { StatusView } from '../views'; -export const DOCUMENT_LOCATION_URI_IDENTIFIER = 'documentLocation'; export const NAMESPACE_URI_IDENTIFIER = 'namespace'; export const OPERATION_ID_URI_IDENTIFIER = 'operationId'; export const CONNECTION_ID_URI_IDENTIFIER = 'connectionId'; diff --git a/src/editors/documentProvider.ts b/src/editors/documentProvider.ts deleted file mode 100644 index ba307e17..00000000 --- a/src/editors/documentProvider.ts +++ /dev/null @@ -1,103 +0,0 @@ -import * as vscode from 'vscode'; - -import ConnectionController from '../connectionController'; -import { StatusView } from '../views'; -import { - CONNECTION_ID_URI_IDENTIFIER, - NAMESPACE_URI_IDENTIFIER -} from './collectionDocumentsProvider'; -import DocumentIdStore from './documentIdStore'; - -export const DOCUMENT_ID_URI_IDENTIFIER = 'documentId'; - -export const VIEW_DOCUMENT_SCHEME = 'VIEW_DOCUMENT_SCHEME'; - -export default class DocumentViewProvider - implements vscode.TextDocumentContentProvider { - _connectionController: ConnectionController; - _documentIdStore: DocumentIdStore; - _statusView: StatusView; - - constructor( - connectionController: ConnectionController, - documentIdStore: DocumentIdStore, - statusView: StatusView - ) { - this._connectionController = connectionController; - this._documentIdStore = documentIdStore; - this._statusView = statusView; - } - - onDidChangeEmitter = new vscode.EventEmitter(); - onDidChange = this.onDidChangeEmitter.event; - - provideTextDocumentContent(uri: vscode.Uri): Promise { - return new Promise((resolve, reject) => { - const uriParams = new URLSearchParams(uri.query); - const namespace = uriParams.get(NAMESPACE_URI_IDENTIFIER) || ''; - const connectionId = uriParams.get(CONNECTION_ID_URI_IDENTIFIER); - - const documentIdReference = - uriParams.get(DOCUMENT_ID_URI_IDENTIFIER) || ''; - const documentId = this._documentIdStore.get(documentIdReference); - if (!documentId) { - vscode.window.showErrorMessage( - 'Unable to fetch document: reference has expired.' - ); - return reject( - new Error('Unable to fetch document: reference has expired.') - ); - } - - // Ensure we're still connected to the correct connection. - if (connectionId !== this._connectionController.getActiveConnectionId()) { - vscode.window.showErrorMessage( - `Unable to fetch document: no longer connected to ${connectionId}` - ); - return reject( - new Error( - `Unable to fetch document: no longer connected to ${connectionId}` - ) - ); - } - - this._statusView.showMessage('Fetching document...'); - - const dataservice = this._connectionController.getActiveDataService(); - if (dataservice === null) { - const errorMessage = `Unable to find document: no longer connected to ${connectionId}`; - vscode.window.showErrorMessage(errorMessage); - return reject(new Error(errorMessage)); - } - - dataservice.find( - namespace, - { - _id: documentId - }, - { - limit: 1 - }, - (err: Error | undefined, documents: object[]) => { - this._statusView.hideMessage(); - - if (err) { - const errorMessage = `Unable to find document: ${err.message}`; - vscode.window.showErrorMessage(errorMessage); - return reject(new Error(errorMessage)); - } - - if (!documents || documents.length === 0) { - const errorMessage = `Unable to find document: ${JSON.stringify( - documentId - )}`; - vscode.window.showErrorMessage(errorMessage); - return reject(new Error(errorMessage)); - } - - return resolve(JSON.stringify(documents[0], null, 2)); - } - ); - }); - } -} diff --git a/src/editors/editDocumentCodeLensProvider.ts b/src/editors/editDocumentCodeLensProvider.ts new file mode 100644 index 00000000..53952158 --- /dev/null +++ b/src/editors/editDocumentCodeLensProvider.ts @@ -0,0 +1,92 @@ +import * as vscode from 'vscode'; +import { EJSON } from 'bson'; +import EXTENSION_COMMANDS from '../commands'; +import type { DocCodeLensesInfo } from '../utils/types'; +import type { OutputItem } from '../utils/types'; + +export default class EditDocumentCodeLensProvider + implements vscode.CodeLensProvider { + _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); + _codeLenses: vscode.CodeLens[] = []; + _codeLensesInfo: DocCodeLensesInfo; + + readonly onDidChangeCodeLenses: vscode.Event = this + ._onDidChangeCodeLenses.event; + + constructor() { + this._codeLensesInfo = []; + + vscode.workspace.onDidChangeConfiguration(() => { + this._onDidChangeCodeLenses.fire(); + }); + } + + updateCodeLensesPosition(playgroundResult: OutputItem): void { + if (!playgroundResult) { + this._codeLensesInfo = []; + + return; + } + + const content = playgroundResult.content; + const namespace = playgroundResult.namespace; + const type = playgroundResult.type; + const codeLensesInfo: DocCodeLensesInfo = []; + + // Show code lenses only for the list of documents or a single document + // that are returned by the find() method. + if (type === 'Cursor' && Array.isArray(content)) { + // When the playground result is the collection, + // show the first code lense after [{. + let line = 2; + + content.forEach((item) => { + // We need _id and namespace for code lenses + // to be able to save the editable document. + if (item !== null && item._id && namespace) { + codeLensesInfo.push({ line, documentId: item._id, namespace }); + // To calculate the position of the next open curly bracket, + // we stringify the object and use a regular expression + // so we can count the number of lines. + line += JSON.stringify(item, null, 2).split(/\r\n|\r|\n/).length; + } + }); + } else if (type === 'Document' && content._id && namespace) { + // When the playground result is the single document, + // show the single code lense after {. + codeLensesInfo.push({ line: 1, documentId: content._id, namespace }); + } + + this._codeLensesInfo = codeLensesInfo; + this._onDidChangeCodeLenses.fire(); + } + + provideCodeLenses(): vscode.CodeLens[] { + this._codeLenses = []; + + if (this._codeLensesInfo) { + this._codeLensesInfo.forEach((item) => { + const position = new vscode.Position(item.line, 0); + const range = new vscode.Range(position, position); + const command: { + title: string; + command: EXTENSION_COMMANDS; + arguments: { + documentId: EJSON.SerializableTypes; + namespace: string; + }[]; + } = { + title: 'Edit Document', + command: EXTENSION_COMMANDS.MDB_OPEN_MONGODB_DOCUMENT_FROM_PLAYGROUND, + arguments: [ + { documentId: item.documentId, namespace: item.namespace } + ] + }; + + this._codeLenses.push(new vscode.CodeLens(range, command)); + }); + } + + return this._codeLenses; + } +} diff --git a/src/editors/editorsController.ts b/src/editors/editorsController.ts index fe492f47..75470ba9 100644 --- a/src/editors/editorsController.ts +++ b/src/editors/editorsController.ts @@ -1,29 +1,23 @@ import * as vscode from 'vscode'; -import * as fse from 'fs-extra'; -import * as os from 'os'; import { EJSON } from 'bson'; -import * as path from 'path'; import CollectionDocumentsCodeLensProvider from './collectionDocumentsCodeLensProvider'; import CollectionDocumentsOperationsStore from './collectionDocumentsOperationsStore'; import CollectionDocumentsProvider, { CONNECTION_ID_URI_IDENTIFIER, OPERATION_ID_URI_IDENTIFIER, NAMESPACE_URI_IDENTIFIER, - VIEW_COLLECTION_SCHEME, - DOCUMENT_LOCATION_URI_IDENTIFIER + VIEW_COLLECTION_SCHEME } from './collectionDocumentsProvider'; -import DocumentProvider, { - DOCUMENT_ID_URI_IDENTIFIER, - VIEW_DOCUMENT_SCHEME -} from './documentProvider'; -import PlaygroundResultProvider, { - PLAYGROUND_RESULT_SCHEME -} from './playgroundResultProvider'; import ConnectionController from '../connectionController'; import { createLogger } from '../logging'; import { StatusView } from '../views'; import PlaygroundController from './playgroundController'; import DocumentIdStore from './documentIdStore'; +import MongoDBDocumentService, { + DOCUMENT_ID_URI_IDENTIFIER, + VIEW_DOCUMENT_SCHEME +} from './mongoDBDocumentService'; +import { MemoryFileSystemProvider } from './memoryFileSystemProvider'; import TelemetryController from '../telemetry/telemetryController'; const log = createLogger('editors controller'); @@ -34,14 +28,14 @@ const log = createLogger('editors controller'); */ export default class EditorsController { _connectionController: ConnectionController; - _documentIdStore: DocumentIdStore; _playgroundController: PlaygroundController; _collectionDocumentsOperationsStore = new CollectionDocumentsOperationsStore(); _collectionViewProvider: CollectionDocumentsProvider; - _documentViewProvider: DocumentProvider; - _playgroundResultViewProvider: PlaygroundResultProvider; _context: vscode.ExtensionContext; _statusView: StatusView; + _memoryFileSystemProvider: MemoryFileSystemProvider; + _documentIdStore: DocumentIdStore; + _mongoDBDocumentService: MongoDBDocumentService; _telemetryController: TelemetryController; constructor( @@ -58,6 +52,25 @@ export default class EditorsController { this._context = context; this._statusView = statusView; this._telemetryController = telemetryController; + this._memoryFileSystemProvider = new MemoryFileSystemProvider(); + this._documentIdStore = new DocumentIdStore(); + this._mongoDBDocumentService = new MongoDBDocumentService( + this._context, + this._documentIdStore, + this._connectionController, + this._statusView, + this._telemetryController + ); + + context.subscriptions.push( + vscode.workspace.registerFileSystemProvider( + VIEW_DOCUMENT_SCHEME, + this._memoryFileSystemProvider, + { + isCaseSensitive: true + } + ) + ); const collectionViewProvider = new CollectionDocumentsProvider( connectionController, @@ -71,6 +84,7 @@ export default class EditorsController { collectionViewProvider ) ); + this._collectionViewProvider = collectionViewProvider; context.subscriptions.push( @@ -85,37 +99,6 @@ export default class EditorsController { ) ); - const documentIdStore = new DocumentIdStore(); - this._documentIdStore = documentIdStore; - - const documentViewProvider = new DocumentProvider( - connectionController, - documentIdStore, - new StatusView(context) - ); - - context.subscriptions.push( - vscode.workspace.registerTextDocumentContentProvider( - VIEW_DOCUMENT_SCHEME, - documentViewProvider - ) - ); - this._documentViewProvider = documentViewProvider; - - const playgroundResultViewProvider = new PlaygroundResultProvider( - playgroundController, - new StatusView(context) - ); - - context.subscriptions.push( - vscode.workspace.registerTextDocumentContentProvider( - PLAYGROUND_RESULT_SCHEME, - playgroundResultViewProvider - ) - ); - - this._playgroundResultViewProvider = playgroundResultViewProvider; - vscode.workspace.onDidCloseTextDocument((e) => { const uriParams = new URLSearchParams(e.uri.query); const documentIdReference = @@ -127,219 +110,108 @@ export default class EditorsController { log.info('activated.'); } - provideDocumentContent( - namespace: string, - documentId: any, - connectionId: string | null - ): Promise { - log.info( - 'fetch document from MongoDB', - namespace, - documentId, - connectionId - ); - - return new Promise((resolve, reject) => { - const dataservice = this._connectionController.getActiveDataService(); - - if (dataservice === null) { - const errorMessage = `Unable to find document: no longer connected to ${connectionId}`; - - vscode.window.showErrorMessage(errorMessage); + async openMongoDBDocument(data: { + documentId: EJSON.SerializableTypes; + namespace: string; + }): Promise { + try { + let fileDocumentId = EJSON.stringify(data.documentId); - return reject(new Error(errorMessage)); - } - - this._statusView.showMessage('Fetching document...'); - - dataservice.find( - namespace, - { - _id: documentId - }, - { - limit: 1 - }, - (error: Error | undefined, documents: object[]) => { - this._statusView.hideMessage(); + fileDocumentId = + fileDocumentId.length > 50 + ? fileDocumentId.substring(0, 50) + : fileDocumentId; - if (error) { - const errorMessage = `Unable to find document: ${error.message}`; - - vscode.window.showErrorMessage(errorMessage); - - return reject(new Error(errorMessage)); - } - - if (!documents || documents.length === 0) { - const errorMessage = `Unable to find document: ${JSON.stringify( - documentId - )}`; - - vscode.window.showErrorMessage(errorMessage); - - return reject(new Error(errorMessage)); - } - - return resolve(JSON.parse(EJSON.stringify(documents[0]))); - } - ); - }); - } - - async onViewDocument( - namespace: string, - documentId: EJSON.SerializableTypes - ): Promise { - log.info('view document from the sidebar in editor', namespace); - - const documentLocation = `${DOCUMENT_LOCATION_URI_IDENTIFIER}=mongodb`; - const connectionId = this._connectionController.getActiveConnectionId(); - const connectionIdUriQuery = `${CONNECTION_ID_URI_IDENTIFIER}=${connectionId}`; - const documentIdReference = this._documentIdStore.add(documentId); - const documentIdUriQuery = `${DOCUMENT_ID_URI_IDENTIFIER}=${documentIdReference}`; - const namespaceUriQuery = `${NAMESPACE_URI_IDENTIFIER}=${namespace}`; - const localDocPath: string = path.join( - os.tmpdir(), - 'vscode-opened-documents', - `${documentIdReference}.json` - ); - const document = await this.provideDocumentContent( - namespace, - documentId, - connectionId - ); - - await fse.ensureFile(localDocPath); - await fse.writeJson(localDocPath, document, { - spaces: 2, - EOL: os.EOL - }); - - const uri: vscode.Uri = vscode.Uri.file(localDocPath).with({ - query: `?${documentLocation}&${namespaceUriQuery}&${connectionIdUriQuery}&${documentIdUriQuery}` - }); - - return new Promise(async (resolve, reject) => { - vscode.workspace.openTextDocument(uri).then((doc) => { - vscode.window - .showTextDocument(doc, { preview: false, preserveFocus: true }) - .then(() => resolve(true), reject); - }, reject); - }); - } - - saveDocumentToMongoDB(): Promise { - return new Promise(async (resolve) => { - const activeEditor = vscode.window.activeTextEditor; - - log.info('save document to MongoDB', activeEditor); - - if (!activeEditor) { - return resolve(true); - } - - const uriParams = new URLSearchParams(activeEditor.document.uri.query); - const documentLocation = - uriParams.get(DOCUMENT_LOCATION_URI_IDENTIFIER) || ''; - const namespace = uriParams.get(NAMESPACE_URI_IDENTIFIER) || ''; - const connectionId = uriParams.get(CONNECTION_ID_URI_IDENTIFIER); - const documentIdReference = - uriParams.get(DOCUMENT_ID_URI_IDENTIFIER) || ''; - const documentId = this._documentIdStore.get(documentIdReference); - - if ( - documentLocation !== 'mongodb' || - !namespace || - !connectionId || - !documentId - ) { - vscode.commands.executeCommand('workbench.action.files.save'); - - return resolve(true); - } - - const activeConnectionId = this._connectionController.getActiveConnectionId(); - const connectionName = this._connectionController.getSavedConnectionName( - connectionId - ); - - if (activeConnectionId !== connectionId) { - // Send metrics to Segment. - this._telemetryController.trackDocumentUpdated('treeview', false); + const fileName = `${VIEW_DOCUMENT_SCHEME}:/${data.namespace}:${fileDocumentId}.json`; + const document = (await this._mongoDBDocumentService.fetchDocument( + data + )) as EJSON.SerializableTypes; + if (document === null) { vscode.window.showErrorMessage( - `Unable to save document: no longer connected to '${connectionName}'` + `Unable to open document: document ${data.documentId} not found` ); - return resolve(false); + return false; } - const dataservice = this._connectionController.getActiveDataService(); + this._saveDocumnentToMemoryFileSystem(fileName, document); - if (dataservice === null) { - // Send metrics to Segment. - this._telemetryController.trackDocumentUpdated('treeview', false); + const activeConnectionId = this._connectionController.getActiveConnectionId(); + const namespaceUriQuery = `${NAMESPACE_URI_IDENTIFIER}=${data.namespace}`; + const connectionIdUriQuery = `${CONNECTION_ID_URI_IDENTIFIER}=${activeConnectionId}`; + const documentIdReference = this._documentIdStore.add(data.documentId); + const documentIdUriQuery = `${DOCUMENT_ID_URI_IDENTIFIER}=${documentIdReference}`; + const uri: vscode.Uri = vscode.Uri.parse(fileName).with({ + query: `?${namespaceUriQuery}&${connectionIdUriQuery}&${documentIdUriQuery}` + }); + + return new Promise(async (resolve, reject) => { + vscode.workspace.openTextDocument(uri).then((doc) => { + vscode.window + .showTextDocument(doc, { preview: false }) + .then(() => resolve(true), reject); + }, reject); + }); + } catch (error) { + vscode.window.showErrorMessage(error.message); + + return false; + } + } - vscode.window.showErrorMessage( - `Unable to save document: no longer connected to '${connectionName}'` - ); + async saveMongoDBDocument(): Promise { + const activeEditor = vscode.window.activeTextEditor; - return resolve(false); - } + if (!activeEditor) { + vscode.window.showErrorMessage('The active editor cannot be found'); - this._statusView.showMessage('Saving document...'); + return false; + } - let newDocument: EJSON.SerializableTypes = {}; + const uriParams = new URLSearchParams(activeEditor.document.uri.query); + const namespace = uriParams.get(NAMESPACE_URI_IDENTIFIER); + const connectionId = uriParams.get(CONNECTION_ID_URI_IDENTIFIER); + const documentIdReference = uriParams.get(DOCUMENT_ID_URI_IDENTIFIER) || ''; + const documentId = this._documentIdStore.get(documentIdReference); - try { - newDocument = EJSON.parse(activeEditor?.document.getText()); - } catch (error) { - // Send metrics to Segment. - this._telemetryController.trackDocumentUpdated('treeview', false); + // If not MongoDB document save to disk instead of MongoDB. + if ( + activeEditor.document.uri.scheme !== 'VIEW_DOCUMENT_SCHEME' || + !namespace || + !connectionId || + // A valid documentId can be false. + documentId === null || + documentId === undefined + ) { + vscode.commands.executeCommand('workbench.action.files.save'); - vscode.window.showErrorMessage(error.message); + return false; + } - return resolve(false); - } + try { + const newDocument = EJSON.parse(activeEditor.document.getText() || ''); - dataservice.findOneAndReplace( + await this._mongoDBDocumentService.replaceDocument({ namespace, - { - _id: documentId - }, - newDocument, - { - returnOriginal: false - }, - (error) => { - this._statusView.hideMessage(); - - if (error) { - const errorMessage = `Unable to save document: ${error.message}`; - - // Send metrics to Segment. - this._telemetryController.trackDocumentUpdated('treeview', false); + connectionId, + documentId, + newDocument + }); - vscode.window.showErrorMessage(errorMessage); + // Save document changes to active editor. + activeEditor?.document.save(); - return resolve(false); - } - - // Send metrics to Segment. - this._telemetryController.trackDocumentUpdated('treeview', true); - - activeEditor?.document.save(); - vscode.window.showInformationMessage( - `The document was saved successfully to '${namespace}'` - ); - - return resolve(true); - } + vscode.window.showInformationMessage( + `The document was saved successfully to '${namespace}'` ); - return resolve(true); - }); + return true; + } catch (error) { + vscode.window.showErrorMessage(error.message); + + return false; + } } static getViewCollectionDocumentsUri( @@ -364,12 +236,12 @@ export default class EditorsController { log.info('view collection documents', namespace); const operationId = this._collectionDocumentsOperationsStore.createNewOperation(); - const uri = EditorsController.getViewCollectionDocumentsUri( operationId, namespace, this._connectionController.getActiveConnectionId() ); + return new Promise((resolve, reject) => { vscode.workspace.openTextDocument(uri).then((doc) => { vscode.window @@ -422,6 +294,34 @@ export default class EditorsController { // Notify the document provider to update with the new document limit. this._collectionViewProvider.onDidChangeEmitter.fire(uri); + return Promise.resolve(true); } + + _saveDocumnentToMemoryFileSystem( + fileName: string, + document: EJSON.SerializableTypes + ): void { + this._memoryFileSystemProvider.writeFile( + vscode.Uri.parse(fileName), + Buffer.from(JSON.stringify(document, null, 2)), + { create: true, overwrite: true } + ); + } + + _resetMemoryFileSystemProvider(): void { + const prefix = `${VIEW_DOCUMENT_SCHEME}:/`; + + for (const [name] of this._memoryFileSystemProvider.readDirectory( + vscode.Uri.parse(prefix) + )) { + this._memoryFileSystemProvider.delete( + vscode.Uri.parse(`${prefix}${name}`) + ); + } + } + + public deactivate(): void { + this._resetMemoryFileSystemProvider(); + } } diff --git a/src/editors/memoryFileSystemProvider.ts b/src/editors/memoryFileSystemProvider.ts new file mode 100644 index 00000000..fb115dd5 --- /dev/null +++ b/src/editors/memoryFileSystemProvider.ts @@ -0,0 +1,243 @@ +import * as path from 'path'; +import * as vscode from 'vscode'; + +export class File implements vscode.FileStat { + type: vscode.FileType; + ctime: number; + mtime: number; + size: number; + name: string; + data?: Uint8Array; + + constructor(name: string) { + this.type = vscode.FileType.File; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.size = 0; + this.name = name; + } +} + +export class Directory implements vscode.FileStat { + type: vscode.FileType; + ctime: number; + mtime: number; + size: number; + name: string; + entries: Map; + + constructor(name: string) { + this.type = vscode.FileType.Directory; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.size = 0; + this.name = name; + this.entries = new Map(); + } +} + +export type Entry = File | Directory; + +export class MemoryFileSystemProvider implements vscode.FileSystemProvider { + root = new Directory(''); + + stat(uri: vscode.Uri): vscode.FileStat { + return this._lookup(uri, false); + } + + readDirectory(uri: vscode.Uri): [string, vscode.FileType][] { + const entry = this._lookupAsDirectory(uri, false); + const result: [string, vscode.FileType][] = []; + + for (const [name, child] of entry.entries) { + result.push([name, child.type]); + } + + return result; + } + + readFile(uri: vscode.Uri): Uint8Array { + const data = this._lookupAsFile(uri, false).data; + + if (data) { + return data; + } + + throw vscode.FileSystemError.FileNotFound(); + } + + writeFile( + uri: vscode.Uri, + content: Uint8Array, + options: { create: boolean; overwrite: boolean } + ): void { + const basename = path.posix.basename(uri.path); + const parent = this._lookupParentDirectory(uri); + let entry = parent.entries.get(basename); + + if (entry instanceof Directory) { + throw vscode.FileSystemError.FileIsADirectory(uri); + } + + if (!entry && !options.create) { + throw vscode.FileSystemError.FileNotFound(uri); + } + + if (entry && options.create && !options.overwrite) { + throw vscode.FileSystemError.FileExists(uri); + } + + if (!entry) { + entry = new File(basename); + parent.entries.set(basename, entry); + this._fireSoon({ type: vscode.FileChangeType.Created, uri }); + } + + entry.mtime = Date.now(); + entry.size = content.byteLength; + entry.data = content; + + this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); + } + + rename( + oldUri: vscode.Uri, + newUri: vscode.Uri, + options: { overwrite: boolean } + ): void { + if (!options.overwrite && this._lookup(newUri, true)) { + throw vscode.FileSystemError.FileExists(newUri); + } + + const entry = this._lookup(oldUri, false); + const oldParent = this._lookupParentDirectory(oldUri); + const newParent = this._lookupParentDirectory(newUri); + const newName = path.posix.basename(newUri.path); + + oldParent.entries.delete(entry.name); + entry.name = newName; + newParent.entries.set(newName, entry); + + this._fireSoon( + { type: vscode.FileChangeType.Deleted, uri: oldUri }, + { type: vscode.FileChangeType.Created, uri: newUri } + ); + } + + delete(uri: vscode.Uri): void { + const dirname = uri.with({ path: path.posix.dirname(uri.path) }); + const basename = path.posix.basename(uri.path); + const parent = this._lookupAsDirectory(dirname, false); + + if (!parent.entries.has(basename)) { + throw vscode.FileSystemError.FileNotFound(uri); + } + + parent.entries.delete(basename); + parent.mtime = Date.now(); + parent.size -= 1; + + this._fireSoon( + { type: vscode.FileChangeType.Changed, uri: dirname }, + { uri, type: vscode.FileChangeType.Deleted } + ); + } + + createDirectory(uri: vscode.Uri): void { + const basename = path.posix.basename(uri.path); + const dirname = uri.with({ path: path.posix.dirname(uri.path) }); + const parent = this._lookupAsDirectory(dirname, false); + const entry = new Directory(basename); + + parent.entries.set(entry.name, entry); + parent.mtime = Date.now(); + parent.size += 1; + + this._fireSoon( + { type: vscode.FileChangeType.Changed, uri: dirname }, + { type: vscode.FileChangeType.Created, uri } + ); + } + + private _lookup(uri: vscode.Uri, silent: false): Entry; + private _lookup(uri: vscode.Uri, silent: boolean): Entry | undefined; + private _lookup(uri: vscode.Uri, silent: boolean): Entry | undefined { + const parts = uri.path.split('/'); + let entry: Entry = this.root; + + for (const part of parts) { + if (!part) { + continue; + } + + let child: Entry | undefined; + + if (entry instanceof Directory) { + child = entry.entries.get(part); + } + + if (!child) { + if (!silent) { + throw vscode.FileSystemError.FileNotFound(uri); + } else { + return undefined; + } + } + + entry = child; + } + + return entry; + } + + private _lookupAsDirectory(uri: vscode.Uri, silent: boolean): Directory { + const entry = this._lookup(uri, silent); + + if (entry instanceof Directory) { + return entry; + } + + throw vscode.FileSystemError.FileNotADirectory(uri); + } + + private _lookupAsFile(uri: vscode.Uri, silent: boolean): File { + const entry = this._lookup(uri, silent); + + if (entry instanceof File) { + return entry; + } + + throw vscode.FileSystemError.FileIsADirectory(uri); + } + + private _lookupParentDirectory(uri: vscode.Uri): Directory { + const dirname = uri.with({ path: path.posix.dirname(uri.path) }); + + return this._lookupAsDirectory(dirname, false); + } + + private _emitter = new vscode.EventEmitter(); + private _bufferedEvents: vscode.FileChangeEvent[] = []; + private _fireSoonHandle?: NodeJS.Timer; + + readonly onDidChangeFile: vscode.Event = this + ._emitter.event; + + watch(_resource: vscode.Uri): vscode.Disposable { + // Ignore, fires for all changes... + return new vscode.Disposable(() => {}); + } + + private _fireSoon(...events: vscode.FileChangeEvent[]): void { + this._bufferedEvents.push(...events); + + if (this._fireSoonHandle) { + clearTimeout(this._fireSoonHandle); + } + + this._fireSoonHandle = setTimeout(() => { + this._emitter.fire(this._bufferedEvents); + this._bufferedEvents.length = 0; + }, 5); + } +} diff --git a/src/editors/mongoDBDocumentService.ts b/src/editors/mongoDBDocumentService.ts new file mode 100644 index 00000000..770e421d --- /dev/null +++ b/src/editors/mongoDBDocumentService.ts @@ -0,0 +1,154 @@ +import * as vscode from 'vscode'; +import { EJSON } from 'bson'; +import DocumentIdStore from './documentIdStore'; +import ConnectionController from '../connectionController'; +import { StatusView } from '../views'; +import TelemetryController from '../telemetry/telemetryController'; +import { createLogger } from '../logging'; +import util from 'util'; + +export const DOCUMENT_ID_URI_IDENTIFIER = 'documentId'; + +export const VIEW_DOCUMENT_SCHEME = 'VIEW_DOCUMENT_SCHEME'; + +const log = createLogger('document controller'); + +export default class MongoDBDocumentService { + _context: vscode.ExtensionContext; + _documentIdStore: DocumentIdStore; + _connectionController: ConnectionController; + _statusView: StatusView; + _telemetryController: TelemetryController; + + constructor( + context: vscode.ExtensionContext, + documentIdStore: DocumentIdStore, + connectionController: ConnectionController, + statusView: StatusView, + telemetryController: TelemetryController + ) { + this._context = context; + this._documentIdStore = documentIdStore; + this._connectionController = connectionController; + this._statusView = statusView; + this._telemetryController = telemetryController; + } + + _fetchDocumentFailed(message: string): void { + const errorMessage = `Unable to fetch document: ${message}`; + + throw new Error(errorMessage); + } + + _saveDocumentFailed(message: string): void { + const errorMessage = `Unable to save document: ${message}`; + + // Send a telemetry event that saving the document failed. + this._telemetryController.trackDocumentUpdated('treeview', false); + + throw new Error(errorMessage); + } + + async replaceDocument(data: { + documentId: EJSON.SerializableTypes; + namespace: string; + connectionId: string; + newDocument: EJSON.SerializableTypes; + }): Promise { + log.info('replace document in MongoDB', data); + + const { documentId, namespace, connectionId, newDocument } = data; + const activeConnectionId = this._connectionController.getActiveConnectionId(); + const connectionName = this._connectionController.getSavedConnectionName( + connectionId + ); + + if (activeConnectionId !== connectionId) { + return this._saveDocumentFailed( + `no longer connected to '${connectionName}'` + ); + } + + const dataservice = this._connectionController.getActiveDataService(); + + if (dataservice === null) { + return this._saveDocumentFailed( + `no longer connected to '${connectionName}'` + ); + } + + const findOneAndReplace = util.promisify( + dataservice.findOneAndReplace.bind(dataservice) + ); + + this._statusView.showMessage('Saving document...'); + + try { + await findOneAndReplace( + namespace, + { + _id: documentId + }, + newDocument, + { + returnOriginal: false + } + ); + + this._statusView.hideMessage(); + this._telemetryController.trackDocumentUpdated('treeview', true); + } catch (error) { + this._statusView.hideMessage(); + + return this._saveDocumentFailed(error.message); + } + } + + async fetchDocument(data: { + documentId: EJSON.SerializableTypes; + namespace: string; + }): Promise { + log.info('fetch document from MongoDB', data); + + const { documentId, namespace } = data; + const activeConnectionId = this._connectionController.getActiveConnectionId(); + const connectionName = activeConnectionId + ? this._connectionController.getSavedConnectionName(activeConnectionId) + : ''; + const dataservice = this._connectionController.getActiveDataService(); + + if (dataservice === null) { + return this._fetchDocumentFailed( + `no longer connected to ${connectionName}` + ); + } + + const find = util.promisify(dataservice.find.bind(dataservice)); + + this._statusView.showMessage('Fetching document...'); + + try { + const documents = await find( + namespace, + { + _id: documentId + }, + { + limit: 1 + } + ); + + this._statusView.hideMessage(); + + if (!documents || documents.length === 0) { + return null; + } + + return JSON.parse(EJSON.stringify(documents[0])); + } catch (error) { + this._statusView.hideMessage(); + + return this._fetchDocumentFailed(error.message); + } + } +} diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index e945a53b..2f85b6a5 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -12,8 +12,13 @@ import playgroundSearchTemplate from '../templates/playgroundSearchTemplate'; import playgroundCreateIndexTemplate from '../templates/playgroundCreateIndexTemplate'; import { createLogger } from '../logging'; import type { ExecuteAllResult } from '../utils/types'; -import { PLAYGROUND_RESULT_SCHEME } from './playgroundResultProvider'; +import PlaygroundResultProvider, { + PLAYGROUND_RESULT_SCHEME, + PLAYGROUND_RESULT_URI +} from './playgroundResultProvider'; import type { OutputItem } from '../utils/types'; +import { StatusView } from '../views'; +import { EJSON } from 'bson'; const log = createLogger('playground controller'); @@ -21,28 +26,31 @@ const log = createLogger('playground controller'); * This controller manages playground. */ export default class PlaygroundController { - public connectionController: ConnectionController; - public activeTextEditor?: TextEditor; - public partialExecutionCodeLensProvider: PartialExecutionCodeLensProvider; - public playgroundResult?: OutputItem; - private _context: vscode.ExtensionContext; - private _languageServerController: LanguageServerController; - private _telemetryController: TelemetryController; - private _activeConnectionCodeLensProvider?: ActiveConnectionCodeLensProvider; - private _outputChannel: OutputChannel; - private _connectionString?: string; - private _connectionOptions?: any; - private _selectedText?: string; - private _codeToEvaluate: string; - private _isPartialRun: boolean; - private _playgroundResultViewColumn?: vscode.ViewColumn; - private _playgroundResultTextDocument?: vscode.TextDocument; + connectionController: ConnectionController; + activeTextEditor?: TextEditor; + partialExecutionCodeLensProvider: PartialExecutionCodeLensProvider; + playgroundResult?: OutputItem; + _context: vscode.ExtensionContext; + _languageServerController: LanguageServerController; + _telemetryController: TelemetryController; + _activeConnectionCodeLensProvider?: ActiveConnectionCodeLensProvider; + _outputChannel: OutputChannel; + _connectionString?: string; + _connectionOptions?: any; + _selectedText?: string; + _codeToEvaluate: string; + _isPartialRun: boolean; + _playgroundResultViewColumn?: vscode.ViewColumn; + _playgroundResultTextDocument?: vscode.TextDocument; + _statusView: StatusView; + _playgroundResultViewProvider: PlaygroundResultProvider; constructor( context: vscode.ExtensionContext, connectionController: ConnectionController, languageServerController: LanguageServerController, - telemetryController: TelemetryController + telemetryController: TelemetryController, + statusView: StatusView ) { this._context = context; this._codeToEvaluate = ''; @@ -50,6 +58,7 @@ export default class PlaygroundController { this.connectionController = connectionController; this._languageServerController = languageServerController; this._telemetryController = telemetryController; + this._statusView = statusView; this._outputChannel = vscode.window.createOutputChannel( 'Playground output' ); @@ -85,7 +94,7 @@ export default class PlaygroundController { } ); - const onEditorChange = (editor) => { + const onEditorChange = (editor: vscode.TextEditor | undefined) => { if (editor?.document.uri.scheme === PLAYGROUND_RESULT_SCHEME) { this._playgroundResultViewColumn = editor.viewColumn; this._playgroundResultTextDocument = editor?.document; @@ -100,28 +109,41 @@ export default class PlaygroundController { vscode.window.onDidChangeActiveTextEditor(onEditorChange); onEditorChange(vscode.window.activeTextEditor); - vscode.window.onDidChangeTextEditorSelection((editor) => { - if ( - editor && - editor.textEditor && - editor.textEditor.document && - editor.textEditor.document.languageId === 'mongodb' - ) { - this._selectedText = (editor.selections as Array) - .sort((a, b) => (a.start.line > b.start.line ? 1 : -1)) // Sort lines selected as alt+click - .map((item, index) => { - if (index === editor.selections.length - 1) { - this.showCodeLensForSelection(item); - } - - return this.getSelectedText(item); - }) - .join('\n'); + vscode.window.onDidChangeTextEditorSelection( + (editor: vscode.TextEditorSelectionChangeEvent) => { + if ( + editor && + editor.textEditor && + editor.textEditor.document && + editor.textEditor.document.languageId === 'mongodb' + ) { + this._selectedText = (editor.selections as Array) + .sort((a, b) => (a.start.line > b.start.line ? 1 : -1)) // Sort lines selected as alt+click. + .map((item, index) => { + if (index === editor.selections.length - 1) { + this.showCodeLensForSelection(item); + } + + return this.getSelectedText(item); + }) + .join('\n'); + } } - }); + ); + + const playgroundResultViewProvider = new PlaygroundResultProvider(context); + + context.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider( + PLAYGROUND_RESULT_SCHEME, + playgroundResultViewProvider + ) + ); + + this._playgroundResultViewProvider = playgroundResultViewProvider; } - public showCodeLensForSelection(item: vscode.Range): void { + showCodeLensForSelection(item: vscode.Range): void { const selectedText = this.getSelectedText(item).trim(); const lastSelectedLine = this.activeTextEditor?.document.lineAt(item.end.line).text.trim() || ''; @@ -142,11 +164,11 @@ export default class PlaygroundController { } } - public disconnectFromServiceProvider(): Promise { + disconnectFromServiceProvider(): Promise { return this._languageServerController.disconnectFromServiceProvider(); } - public connectToServiceProvider(): Promise { + connectToServiceProvider(): Promise { const model = this.connectionController .getActiveConnectionModel() ?.getAttributes({ derived: true }); @@ -168,7 +190,7 @@ export default class PlaygroundController { return this._languageServerController.disconnectFromServiceProvider(); } - private createPlaygroundFileWithContent( + createPlaygroundFileWithContent( content: string | undefined ): Promise { return new Promise((resolve, reject) => { @@ -185,7 +207,7 @@ export default class PlaygroundController { }); } - public createPlaygroundForSearch( + createPlaygroundForSearch( databaseName: string, collectionName: string ): Promise { @@ -196,7 +218,7 @@ export default class PlaygroundController { return this.createPlaygroundFileWithContent(content); } - public createPlaygroundForNewIndex( + createPlaygroundForNewIndex( databaseName: string, collectionName: string ): Promise { @@ -207,7 +229,7 @@ export default class PlaygroundController { return this.createPlaygroundFileWithContent(content); } - public createPlayground(): Promise { + createPlayground(): Promise { const useDefaultTemplate = !!vscode.workspace .getConfiguration('mdb') .get('useDefaultTemplateForPlayground'); @@ -226,13 +248,15 @@ export default class PlaygroundController { }); } - public async evaluate(codeToEvaluate: string): Promise { + async evaluate(codeToEvaluate: string): Promise { + this._statusView.showMessage('Getting results...'); + // Send a request to the language server to execute scripts from a playground. const result: ExecuteAllResult = await this._languageServerController.executeAll( codeToEvaluate ); - // Send metrics to Segment. + this._statusView.hideMessage(); this._telemetryController.trackPlaygroundCodeExecuted( result, this._isPartialRun, @@ -242,15 +266,15 @@ export default class PlaygroundController { return result; } - private getAllText(): string { + getAllText(): string { return this.activeTextEditor?.document.getText() || ''; } - private getSelectedText(selection: vscode.Range): string { + getSelectedText(selection: vscode.Range): string { return this.activeTextEditor?.document.getText(selection) || ''; } - public evaluateWithCancelModal(): Promise { + evaluateWithCancelModal(): Promise { if (!this._connectionString) { return Promise.reject( new Error('Please connect to a database before running a playground.') @@ -297,52 +321,71 @@ export default class PlaygroundController { }); } - public getVirtualDocumentUri(content?: any) { - let extension = ''; + getDocumentLanguage(content?: EJSON.SerializableTypes): string { + if (typeof content === 'object' && content !== null) { + return 'json'; + } + + return 'plaintext'; + } + + async openPlaygroundResult(): Promise { + this._playgroundResultViewProvider.setPlaygroundResult( + this.playgroundResult + ); - if (typeof content === 'object') { - extension = 'json'; + if (this._playgroundResultTextDocument) { + await this.reopenResultAsVirtualDocument(); } else { - extension = 'txt'; + await this.openResultAsVirtualDocument(); } - return vscode.Uri.parse( - [ - `${PLAYGROUND_RESULT_SCHEME}:Playground Result`, - `.${extension}`, - `?id=${Date.now()}` - ].join('') - ); - } + if (this._playgroundResultTextDocument) { + const language = this.getDocumentLanguage(this.playgroundResult?.content); - private openResultAsVirtualDocument( - viewColumn: vscode.ViewColumn - ): Thenable { - const content = - this.playgroundResult && this.playgroundResult.content - ? this.playgroundResult.content - : ''; - - return vscode.workspace - .openTextDocument(this.getVirtualDocumentUri(content)) - .then( - (doc) => { - this._playgroundResultTextDocument = doc; - return vscode.window.showTextDocument(doc, { - preview: false, - viewColumn - }); - }, - (error) => { - log.error('Open result as VirtualDocument ERROR', error); - return vscode.window.showErrorMessage( - `Unable to open a result document: ${error.message}` - ); - } + await vscode.languages.setTextDocumentLanguage( + this._playgroundResultTextDocument, + language ); + } } - public async evaluatePlayground(): Promise { + async reopenResultAsVirtualDocument() { + let viewColumn: vscode.ViewColumn = + this._playgroundResultViewColumn || vscode.ViewColumn.Beside; + + this._playgroundResultViewProvider.refresh(); + + await vscode.window.showTextDocument(PLAYGROUND_RESULT_URI, { + preview: false, + viewColumn + }); + } + + async openResultAsVirtualDocument(): Promise { + let viewColumn: vscode.ViewColumn = + this._playgroundResultViewColumn || vscode.ViewColumn.Beside; + + await vscode.workspace.openTextDocument(PLAYGROUND_RESULT_URI).then( + async (doc) => { + this._playgroundResultTextDocument = doc; + + return vscode.window.showTextDocument(doc, { + preview: false, + viewColumn + }); + }, + (error) => { + log.error('Open result as VirtualDocument ERROR', error); + + return vscode.window.showErrorMessage( + `Unable to open a result document: ${error.message}` + ); + } + ); + } + + async evaluatePlayground(): Promise { return new Promise(async (resolve) => { const shouldConfirmRunAll = vscode.workspace .getConfiguration('mdb') @@ -376,6 +419,7 @@ export default class PlaygroundController { for (const line of evaluateResponse.outputLines) { this._outputChannel.appendLine(line.content); } + this._outputChannel.show(true); } @@ -385,31 +429,13 @@ export default class PlaygroundController { this.playgroundResult = evaluateResponse.result; - let viewColumn: vscode.ViewColumn = - this._playgroundResultViewColumn || vscode.ViewColumn.Beside; - - if (this._playgroundResultTextDocument) { - await vscode.window - .showTextDocument(this._playgroundResultTextDocument, { - preview: false, - viewColumn - }) - .then((editor) => { - viewColumn = editor.viewColumn || vscode.ViewColumn.Beside; - vscode.commands.executeCommand( - 'workbench.action.closeActiveEditor' - ); - return this.openResultAsVirtualDocument(viewColumn); - }); - } else { - await this.openResultAsVirtualDocument(viewColumn); - } + await this.openPlaygroundResult(); return resolve(true); }); } - public runSelectedPlaygroundBlocks(): Promise { + runSelectedPlaygroundBlocks(): Promise { if ( !this.activeTextEditor || this.activeTextEditor.document.languageId !== 'mongodb' @@ -441,7 +467,7 @@ export default class PlaygroundController { return this.evaluatePlayground(); } - public runAllPlaygroundBlocks(): Promise { + runAllPlaygroundBlocks(): Promise { if ( !this.activeTextEditor || this.activeTextEditor.document.languageId !== 'mongodb' @@ -459,7 +485,7 @@ export default class PlaygroundController { return this.evaluatePlayground(); } - public runAllOrSelectedPlaygroundBlocks(): Promise { + runAllOrSelectedPlaygroundBlocks(): Promise { if ( !this.activeTextEditor || this.activeTextEditor.document.languageId !== 'mongodb' @@ -488,21 +514,17 @@ export default class PlaygroundController { return this.evaluatePlayground(); } - public openPlayground(filePath: string): Promise { - return new Promise(async (resolve) => { - await vscode.workspace.openTextDocument(filePath).then( - (doc) => vscode.window.showTextDocument(doc, 1, false), - (error) => { - vscode.window.showErrorMessage(`Unable to read file: ${filePath}`); - log.error('Open playground ERROR', error); - } - ); - - return resolve(true); + openPlayground(filePath: string): Promise { + return new Promise((resolve, reject) => { + vscode.workspace.openTextDocument(filePath).then((doc) => { + vscode.window + .showTextDocument(doc, { preview: false }) + .then(() => resolve(true), reject); + }, reject); }); } - public deactivate(): void { + deactivate(): void { this.connectionController.removeEventListener( DataServiceEventTypes.ACTIVE_CONNECTION_CHANGED, () => { diff --git a/src/editors/playgroundResultProvider.ts b/src/editors/playgroundResultProvider.ts index 7a2e7258..c501dcc3 100644 --- a/src/editors/playgroundResultProvider.ts +++ b/src/editors/playgroundResultProvider.ts @@ -1,44 +1,66 @@ import * as vscode from 'vscode'; -import { StatusView } from '../views'; -import PlaygroundController from './playgroundController'; +import EditDocumentCodeLensProvider from './editDocumentCodeLensProvider'; +import type { OutputItem } from '../utils/types'; export const PLAYGROUND_RESULT_SCHEME = 'PLAYGROUND_RESULT_SCHEME'; +export const PLAYGROUND_RESULT_URI = vscode.Uri.parse( + `${PLAYGROUND_RESULT_SCHEME}:Playground Result` +); + export default class PlaygroundResultProvider implements vscode.TextDocumentContentProvider { - _playgroundController: PlaygroundController; - _statusView: StatusView; - - constructor( - playgroundController: PlaygroundController, - statusView: StatusView - ) { - this._playgroundController = playgroundController; - this._statusView = statusView; + _editDocumentCodeLensProvider: EditDocumentCodeLensProvider; + _playgroundResult: OutputItem; + + constructor(context: vscode.ExtensionContext) { + this._editDocumentCodeLensProvider = new EditDocumentCodeLensProvider(); + this._playgroundResult = { + namespace: null, + type: null, + content: undefined + }; + + context.subscriptions.push( + vscode.languages.registerCodeLensProvider( + { + scheme: PLAYGROUND_RESULT_SCHEME, + language: 'json' + }, + this._editDocumentCodeLensProvider + ) + ); } onDidChangeEmitter = new vscode.EventEmitter(); onDidChange = this.onDidChangeEmitter.event; - provideTextDocumentContent(): Promise { - return new Promise((resolve) => { - this._statusView.showMessage('Getting results...'); - - if ( - typeof this._playgroundController.playgroundResult?.content === - 'object' || - this._playgroundController.playgroundResult?.type !== 'string' - ) { - return resolve( - JSON.stringify( - this._playgroundController.playgroundResult?.content, - null, - 2 - ) - ); - } - - return resolve(this._playgroundController.playgroundResult?.content); - }); + setPlaygroundResult(playgroundResult?: OutputItem): void { + if (playgroundResult) { + this._playgroundResult = playgroundResult; + } + } + + async refresh(): Promise { + this.onDidChangeEmitter.fire(PLAYGROUND_RESULT_URI); + } + + provideTextDocumentContent(): string { + const type = this._playgroundResult.type; + const content = this._playgroundResult.content; + + if (type === 'undefined') { + return 'undefined'; + } + + if (type === 'string') { + return this._playgroundResult.content; + } + + this._editDocumentCodeLensProvider?.updateCodeLensesPosition( + this._playgroundResult + ); + + return JSON.stringify(content, null, 2); } } diff --git a/src/explorer/explorerTreeController.ts b/src/explorer/explorerTreeController.ts index 0499fb89..b7b875c5 100644 --- a/src/explorer/explorerTreeController.ts +++ b/src/explorer/explorerTreeController.ts @@ -13,7 +13,7 @@ import EXTENSION_COMMANDS from '../commands'; const log = createLogger('explorer controller'); export default class ExplorerTreeController -implements vscode.TreeDataProvider { + implements vscode.TreeDataProvider { private _connectionController: ConnectionController; private _connectionTreeItems: { [key: string]: ConnectionTreeItem }; contextValue = 'explorerTreeController'; @@ -99,7 +99,7 @@ implements vscode.TreeDataProvider { if (selectedItem.contextValue === DOCUMENT_ITEM) { vscode.commands.executeCommand( - EXTENSION_COMMANDS.MDB_VIEW_DOCUMENT, + EXTENSION_COMMANDS.MDB_OPEN_MONGODB_DOCUMENT_FROM_TREE, event.selection[0] ); } diff --git a/src/extension.ts b/src/extension.ts index 316406ea..b929475b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import { ext } from './extensionConstants'; import { createKeytar } from './utils/keytar'; import { createLogger } from './logging'; + const log = createLogger('extension.ts'); /** diff --git a/src/language/worker.ts b/src/language/worker.ts index da1f4c40..d88f6a54 100644 --- a/src/language/worker.ts +++ b/src/language/worker.ts @@ -41,18 +41,25 @@ const executeAll = async ( for (const { type, printable } of values) { outputLines.push({ type, - content: printable + content: printable, + namespace: null }); } } }); - const { type, printable } = await runtime.evaluate(codeToEvaluate); + const { source, type, printable } = await runtime.evaluate(codeToEvaluate); + const namespace = + source && source.namespace + ? `${source.namespace.db}.${source.namespace.collection}` + : null; + const content = + typeof printable === 'string' + ? printable + : JSON.parse(EJSON.stringify(printable)); const result = { + namespace, type: type ? type : typeof printable, - content: - typeof printable === 'string' - ? printable - : JSON.parse(EJSON.stringify(printable)) + content }; return [null, { outputLines, result }]; diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 49a8fe6b..1c555194 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -4,7 +4,7 @@ * Activated from `./src/extension.ts` */ import * as vscode from 'vscode'; - +import { EJSON } from 'bson'; import ConnectionController from './connectionController'; import launchMongoShell from './commands/launchMongoShell'; import { EditorsController, PlaygroundController } from './editors'; @@ -50,7 +50,7 @@ export default class MDBExtensionController implements vscode.Disposable { constructor( context: vscode.ExtensionContext, - options: { shouldTrackTelemetry?: boolean } + options: { shouldTrackTelemetry: boolean } ) { this._context = context; this._statusView = new StatusView(context); @@ -75,7 +75,8 @@ export default class MDBExtensionController implements vscode.Disposable { context, this._connectionController, this._languageServerController, - this._telemetryController + this._telemetryController, + this._statusView ); this._editorsController = new EditorsController( context, @@ -164,8 +165,13 @@ export default class MDBExtensionController implements vscode.Disposable { () => this._languageServerController.startStreamLanguageServerLogs() ); - this.registerCommand(EXTENSION_COMMANDS.MDB_SAVE_DOCUMENT_TO_MONGODB, () => - this._editorsController.saveDocumentToMongoDB() + this.registerCommand( + EXTENSION_COMMANDS.MDB_OPEN_MONGODB_DOCUMENT_FROM_PLAYGROUND, + (data: { documentId: EJSON.SerializableTypes; namespace: string }) => + this._editorsController.openMongoDBDocument(data) + ); + this.registerCommand(EXTENSION_COMMANDS.MDB_SAVE_MONGODB_DOCUMENT, () => + this._editorsController.saveMongoDBDocument() ); this.registerEditorCommands(); @@ -411,12 +417,12 @@ export default class MDBExtensionController implements vscode.Disposable { } ); this.registerCommand( - EXTENSION_COMMANDS.MDB_VIEW_DOCUMENT, + EXTENSION_COMMANDS.MDB_OPEN_MONGODB_DOCUMENT_FROM_TREE, (element: DocumentTreeItem): Promise => { - return this._editorsController.onViewDocument( - element.namespace, - element.documentId - ); + return this._editorsController.openMongoDBDocument({ + documentId: element.documentId, + namespace: element.namespace + }); } ); this.registerCommand( @@ -517,5 +523,6 @@ export default class MDBExtensionController implements vscode.Disposable { this._playgroundController.deactivate(); this._telemetryController.deactivate(); this._languageServerController.deactivate(); + this._editorsController.deactivate(); } } diff --git a/src/test/suite/editors/activeDBCodeLensProvider.test.ts b/src/test/suite/editors/activeDBCodeLensProvider.test.ts index 3b72c4a0..4e9de174 100644 --- a/src/test/suite/editors/activeDBCodeLensProvider.test.ts +++ b/src/test/suite/editors/activeDBCodeLensProvider.test.ts @@ -18,10 +18,11 @@ suite('Active DB CodeLens Provider Test Suite', () => { mockStorageController, mockExtensionContext ); + const testStatusView = new StatusView(mockExtensionContext); suite('user is not connected', () => { const testConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), + testStatusView, mockStorageController, testTelemetryController ); @@ -53,7 +54,7 @@ suite('Active DB CodeLens Provider Test Suite', () => { suite('user is connected', () => { const testConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), + testStatusView, mockStorageController, testTelemetryController ); diff --git a/src/test/suite/editors/collectionDocumentsProvider.test.ts b/src/test/suite/editors/collectionDocumentsProvider.test.ts index b999bc68..e5abd604 100644 --- a/src/test/suite/editors/collectionDocumentsProvider.test.ts +++ b/src/test/suite/editors/collectionDocumentsProvider.test.ts @@ -211,13 +211,13 @@ suite('Collection Documents Provider Test Suite', () => { ); mockConnectionController.setActiveConnection(mockActiveConnection); - const textStatusView = new StatusView(mockExtensionContext); + const testStatusView = new StatusView(mockExtensionContext); const testQueryStore = new CollectionDocumentsOperationsStore(); const testCollectionViewProvider = new CollectionDocumentsProvider( mockConnectionController, testQueryStore, - textStatusView + testStatusView ); const operationId = testQueryStore.createNewOperation(); @@ -227,10 +227,10 @@ suite('Collection Documents Provider Test Suite', () => { ); const mockShowMessage = sinon.fake(); - sinon.replace(textStatusView, 'showMessage', mockShowMessage); + sinon.replace(testStatusView, 'showMessage', mockShowMessage); const mockHideMessage = sinon.fake(); - sinon.replace(textStatusView, 'hideMessage', mockHideMessage); + sinon.replace(testStatusView, 'hideMessage', mockHideMessage); mockActiveConnection.find = ( namespace, diff --git a/src/test/suite/editors/documentController.test.ts b/src/test/suite/editors/documentController.test.ts new file mode 100644 index 00000000..ec10228b --- /dev/null +++ b/src/test/suite/editors/documentController.test.ts @@ -0,0 +1,206 @@ +import * as vscode from 'vscode'; +import MongoDBDocumentService from '../../../editors/mongoDBDocumentService'; +import DocumentIdStore from '../../../editors/documentIdStore'; +import ConnectionController from '../../../connectionController'; +import { TestExtensionContext } from '../stubs'; +import { StorageController } from '../../../storage'; +import { StatusView } from '../../../views'; +import TelemetryController from '../../../telemetry/telemetryController'; +import { afterEach } from 'mocha'; +import { MemoryFileSystemProvider } from '../../../editors/memoryFileSystemProvider'; +import { EJSON } from 'bson'; + +const sinon = require('sinon'); +const chai = require('chai'); +const expect = chai.expect; + +suite('MongoDB Document Service Test Suite', () => { + const testDocumentIdStore = new DocumentIdStore(); + const mockExtensionContext = new TestExtensionContext(); + const testStorageController = new StorageController(mockExtensionContext); + const testStatusView = new StatusView(mockExtensionContext); + const testTelemetryController = new TelemetryController( + testStorageController, + mockExtensionContext + ); + const testConnectionController = new ConnectionController( + testStatusView, + testStorageController, + testTelemetryController + ); + const testMongoDBDocumentService = new MongoDBDocumentService( + mockExtensionContext, + testDocumentIdStore, + testConnectionController, + testStatusView, + testTelemetryController + ); + + const sandbox = sinon.createSandbox(); + + afterEach(() => { + sandbox.restore(); + sinon.restore(); + }); + + test('replaceDocument calls findOneAndReplace and saves a document when connected', async () => { + const namespace = 'waffle.house'; + const connectionId = 'tasty_sandwhich'; + const documentId = '93333a0d-83f6-4e6f-a575-af7ea6187a4a'; + const document: EJSON.SerializableTypes = { _id: '123' }; + const newDocument = { _id: '123', price: 5000 }; + + const mockActiveConnectionId = sinon.fake.returns('tasty_sandwhich'); + sinon.replace( + testConnectionController, + 'getActiveConnectionId', + mockActiveConnectionId + ); + + const mockGetActiveDataService = sinon.fake.returns({ + findOneAndReplace: async ( + namespace: string, + filter: object, + replacement: object, + options: object, + callback: (error: Error | null, result: object) => void + ) => { + document.price = 5000; + + return callback(null, document); + } + }); + sinon.replace( + testConnectionController, + 'getActiveDataService', + mockGetActiveDataService + ); + + const mockShowMessage = sinon.fake(); + sinon.replace(testStatusView, 'showMessage', mockShowMessage); + + const mockHideMessage = sinon.fake(); + sinon.replace(testStatusView, 'hideMessage', mockHideMessage); + + await testMongoDBDocumentService.replaceDocument({ + namespace, + documentId, + connectionId, + newDocument + }); + + expect(document).to.be.deep.equal(newDocument); + }); + + test('fetchDocument calls find and returns a single document when connected', async () => { + const namespace = 'waffle.house'; + const connectionName = 'tasty_sandwhich'; + const documentId = '93333a0d-83f6-4e6f-a575-af7ea6187a4a'; + const documents = [{ _id: '123' }]; + + const mockGetActiveDataService = sinon.fake.returns({ + find: ( + namespace: string, + filter: object, + options: object, + callback: (error: Error | null, result: object) => void + ) => { + return callback(null, [{ _id: '123' }]); + } + }); + sinon.replace( + testConnectionController, + 'getActiveDataService', + mockGetActiveDataService + ); + + const mockShowMessage = sinon.fake(); + sinon.replace(testStatusView, 'showMessage', mockShowMessage); + + const mockHideMessage = sinon.fake(); + sinon.replace(testStatusView, 'hideMessage', mockHideMessage); + + const result = await testMongoDBDocumentService.fetchDocument({ + namespace, + documentId + }); + + expect(result).to.be.deep.equal(JSON.parse(EJSON.stringify(documents[0]))); + }); + + test("if a user is not connected, documents won't be saved to MongoDB", async () => { + const namespace = 'waffle.house'; + const connectionId = 'tasty_sandwhich'; + const documentId = '93333a0d-83f6-4e6f-a575-af7ea6187a4a'; + const newDocument = { _id: '123', price: 5000 }; + + const fakeVscodeErrorMessage = sinon.fake(); + sinon.replace(vscode.window, 'showErrorMessage', fakeVscodeErrorMessage); + + const mockActiveConnectionId = sinon.fake.returns(null); + sinon.replace( + testConnectionController, + 'getActiveConnectionId', + mockActiveConnectionId + ); + + const mockGetSavedConnectionName = sinon.fake.returns('tasty_sandwhich'); + sinon.replace( + testConnectionController, + 'getSavedConnectionName', + mockGetSavedConnectionName + ); + + try { + await testMongoDBDocumentService.replaceDocument({ + documentId, + namespace, + connectionId, + newDocument + }); + } catch (error) { + const expectedMessage = + "Unable to save document: no longer connected to 'tasty_sandwhich'"; + + expect(error.message).to.be.equal(expectedMessage); + } + }); + + test("if a user switched the active connection, document opened from the previous connection can't be saved", async () => { + const namespace = 'waffle.house'; + const connectionId = 'tasty_sandwhich'; + const documentId = '93333a0d-83f6-4e6f-a575-af7ea6187a4a'; + const newDocument = { _id: '123', price: 5000 }; + + const fakeVscodeErrorMessage = sinon.fake(); + sinon.replace(vscode.window, 'showErrorMessage', fakeVscodeErrorMessage); + + const mockActiveConnectionId = sinon.fake.returns('berlin.coctails'); + sinon.replace( + testConnectionController, + 'getActiveConnectionId', + mockActiveConnectionId + ); + + const mockGetSavedConnectionName = sinon.fake.returns('tasty_sandwhich'); + sinon.replace( + testConnectionController, + 'getSavedConnectionName', + mockGetSavedConnectionName + ); + + try { + await testMongoDBDocumentService.replaceDocument({ + documentId, + namespace, + connectionId, + newDocument + }); + } catch (error) { + const expectedMessage = + "Unable to save document: no longer connected to 'tasty_sandwhich'"; + + expect(error.message).to.be.equal(expectedMessage); + } + }); +}); diff --git a/src/test/suite/editors/documentProvider.test.ts b/src/test/suite/editors/documentProvider.test.ts deleted file mode 100644 index 8830e774..00000000 --- a/src/test/suite/editors/documentProvider.test.ts +++ /dev/null @@ -1,406 +0,0 @@ -import assert from 'assert'; -import * as vscode from 'vscode'; -import { afterEach } from 'mocha'; -import * as sinon from 'sinon'; -import { ObjectId } from 'bson'; -import TelemetryController from '../../../telemetry/telemetryController'; -import DocumentProvider from '../../../editors/documentProvider'; -import ConnectionController from '../../../connectionController'; -import { StatusView } from '../../../views'; -import { TestExtensionContext } from '../stubs'; -import { StorageController } from '../../../storage'; -import { - seedDataAndCreateDataService, - cleanupTestDB, - TEST_DB_NAME -} from '../dbTestHelper'; -import { - documentWithAllBSONTypes, - documentWithAllBsonTypesJsonified, - documentWithBinaryId, - documentWithBinaryIdString -} from './documentStringFixtures'; -import DocumentIdStore from '../../../editors/documentIdStore'; - -const mockDocumentAsJsonString = `{ - "_id": "first_id", - "field1": "first_field" -}`; - -const docAsString2 = `{ - "_id": "5e32b4d67bf47f4525f2f8ab", - "bowl": "noodles" -}`; - -const docAsString3 = `{ - "_id": 15, - "bowl": "noodles" -}`; - -suite('Document Provider Test Suite', () => { - afterEach(() => { - sinon.restore(); - }); - - test('expected provideTextDocumentContent to parse uri and return the document in the form of a string from a find call', (done) => { - const mockActiveConnection = { - find: (namespace, filter, options, callback): void => { - assert( - namespace === 'fruit.pineapple', - `Expected find namespace to be 'fruit.pineapple' found ${namespace}` - ); - - assert( - options.limit === 1, - `Expected find limit to be 1, found ${options.limit}` - ); - - return callback(null, [{ a: 'Declaration of Independence' }]); - } - }; - - const mockExtensionContext = new TestExtensionContext(); - const mockStorageController = new StorageController(mockExtensionContext); - const testTelemetryController = new TelemetryController( - mockStorageController, - mockExtensionContext - ); - const mockConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), - mockStorageController, - testTelemetryController - ); - mockConnectionController.setActiveConnection(mockActiveConnection); - - const docStore = new DocumentIdStore(); - const testCollectionViewProvider = new DocumentProvider( - mockConnectionController, - docStore, - new StatusView(mockExtensionContext) - ); - - const documentIdRef = docStore.add('123'); - const uri = vscode.Uri.parse( - `scheme:Results: filename.json?namespace=fruit.pineapple&documentId=${documentIdRef}` - ); - - testCollectionViewProvider - .provideTextDocumentContent(uri) - .then((document) => { - assert( - document.includes('Declaration of Independence'), - `Expected provideTextDocumentContent to return document string, found ${document}` - ); - done(); - }) - .catch(done); - }); - - test('expected provideTextDocumentContent to return a json.stringify string', (done) => { - const mockDocument = [ - { - _id: 'first_id', - field1: 'first_field' - } - ]; - - const mockActiveConnection = { - find: (namespace, filter, options, callback): void => { - return callback(null, mockDocument); - } - }; - - const mockExtensionContext = new TestExtensionContext(); - const mockStorageController = new StorageController(mockExtensionContext); - const testTelemetryController = new TelemetryController( - mockStorageController, - mockExtensionContext - ); - const mockConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), - mockStorageController, - testTelemetryController - ); - mockConnectionController.setActiveConnection(mockActiveConnection); - - const docStore = new DocumentIdStore(); - - const testCollectionViewProvider = new DocumentProvider( - mockConnectionController, - docStore, - new StatusView(mockExtensionContext) - ); - - const documentIdRef = docStore.add('123'); - - const uri = vscode.Uri.parse( - `scheme:Results: filename.json?namespace=test.test&documentId=${documentIdRef}` - ); - - testCollectionViewProvider - .provideTextDocumentContent(uri) - .then((document) => { - assert( - document === mockDocumentAsJsonString, - `Expected provideTextDocumentContent to return json stringified string, found ${document}` - ); - done(); - }) - .catch(done); - }); - - test('provideTextDocumentContent shows a status bar item while it is running then hide it', (done) => { - const mockActiveConnection = { find: {} }; - - const mockExtensionContext = new TestExtensionContext(); - const mockStorageController = new StorageController(mockExtensionContext); - const testTelemetryController = new TelemetryController( - mockStorageController, - mockExtensionContext - ); - const mockConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), - mockStorageController, - testTelemetryController - ); - mockConnectionController.setActiveConnection(mockActiveConnection); - - const textStatusView = new StatusView(mockExtensionContext); - - const docStore = new DocumentIdStore(); - - const testCollectionViewProvider = new DocumentProvider( - mockConnectionController, - docStore, - textStatusView - ); - - const documentIdRef = docStore.add('123'); - - const uri = vscode.Uri.parse( - `scheme:Results: filename.json?namespace=aaaaaaaa&documentId=${documentIdRef}` - ); - - const mockShowMessage = sinon.fake(); - sinon.replace(textStatusView, 'showMessage', mockShowMessage); - - const mockHideMessage = sinon.fake(); - sinon.replace(textStatusView, 'hideMessage', mockHideMessage); - - mockActiveConnection.find = ( - namespace, - filter, - options, - callback - ): void => { - assert(mockShowMessage.called); - assert(!mockHideMessage.called); - assert(mockShowMessage.firstCall.args[0] === 'Fetching document...'); - - return callback(null, [{ b: 'aaaaaaaaaaaaaaaaa' }]); - }; - - testCollectionViewProvider - .provideTextDocumentContent(uri) - .then(() => { - assert(mockHideMessage.called); - }) - .then(done, done); - }); - - suite('Document Provider with live database', function () { - this.timeout(5000); - - afterEach(async () => { - await cleanupTestDB(); - }); - - test('handles displaying a document with all bson types', (done) => { - seedDataAndCreateDataService('ramen', [ - documentWithAllBSONTypes - ]).then( - (dataService) => { - const mockExtensionContext = new TestExtensionContext(); - const mockStorageController = new StorageController( - mockExtensionContext - ); - const testTelemetryController = new TelemetryController( - mockStorageController, - mockExtensionContext - ); - const mockConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), - mockStorageController, - testTelemetryController - ); - - mockConnectionController.setActiveConnection(dataService); - - const docStore = new DocumentIdStore(); - - const testCollectionViewProvider = new DocumentProvider( - mockConnectionController, - docStore, - new StatusView(mockExtensionContext) - ); - - const documentIdRef = docStore.add((documentWithAllBSONTypes as any)._id); - const uri = vscode.Uri.parse( - `scheme:Results: filename.json?namespace=${TEST_DB_NAME}.ramen&documentId=${documentIdRef}` - ); - - testCollectionViewProvider - .provideTextDocumentContent(uri) - .then((document) => { - assert( - document === documentWithAllBsonTypesJsonified, - `Expected provideTextDocumentContent to return json stringified string, found ${document}` - ); - done(); - }) - .catch(done); - } - ); - }); - - test('can find a doc with a binary _id', async () => { - const dataService = await seedDataAndCreateDataService('ramen', [ - documentWithBinaryId - ]); - const mockExtensionContext = new TestExtensionContext(); - const mockStorageController = new StorageController( - mockExtensionContext - ); - const testTelemetryController = new TelemetryController( - mockStorageController, - mockExtensionContext - ); - const mockConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), - mockStorageController, - testTelemetryController - ); - - mockConnectionController.setActiveConnection(dataService); - - const docStore = new DocumentIdStore(); - const testCollectionViewProvider = new DocumentProvider( - mockConnectionController, - docStore, - new StatusView(mockExtensionContext) - ); - - const documentIdRef = docStore.add(documentWithBinaryId._id); - const uri = vscode.Uri.parse( - `scheme:Results: filename.json?namespace=${TEST_DB_NAME}.ramen&documentId=${documentIdRef}` - ); - - const document = await testCollectionViewProvider - .provideTextDocumentContent(uri); - assert( - document === documentWithBinaryIdString, - `Expected provideTextDocumentContent to return json stringified string ${documentWithBinaryIdString}, found ${document}` - ); - }); - - test('expected provideTextDocumentContent to handle an id that is an object id', (done) => { - const mockDocument = { - _id: new ObjectId('5e32b4d67bf47f4525f2f8ab'), - bowl: 'noodles' - }; - - seedDataAndCreateDataService('ramen', [mockDocument]).then( - (dataService) => { - const mockExtensionContext = new TestExtensionContext(); - const mockStorageController = new StorageController( - mockExtensionContext - ); - const testTelemetryController = new TelemetryController( - mockStorageController, - mockExtensionContext - ); - const mockConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), - mockStorageController, - testTelemetryController - ); - - mockConnectionController.setActiveConnection(dataService); - - const docStore = new DocumentIdStore(); - const testCollectionViewProvider = new DocumentProvider( - mockConnectionController, - docStore, - new StatusView(mockExtensionContext) - ); - const documentIdRef = docStore.add(mockDocument._id); - - const uri = vscode.Uri.parse( - `scheme:Results: filename.json?namespace=${TEST_DB_NAME}.ramen&documentId=${documentIdRef}` - ); - - testCollectionViewProvider - .provideTextDocumentContent(uri) - .then((document) => { - assert( - document === docAsString2, - `Expected provideTextDocumentContent to return json stringified string, found ${document}` - ); - done(); - }) - .catch(done); - } - ); - }); - - test('expected provideTextDocumentContent to handle an id that is not an object id', (done) => { - const mockDocument = { - _id: 15, - bowl: 'noodles' - }; - - seedDataAndCreateDataService('ramen', [mockDocument]).then( - (dataService) => { - const mockExtensionContext = new TestExtensionContext(); - const mockStorageController = new StorageController( - mockExtensionContext - ); - const testTelemetryController = new TelemetryController( - mockStorageController, - mockExtensionContext - ); - const mockConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), - mockStorageController, - testTelemetryController - ); - - mockConnectionController.setActiveConnection(dataService); - - const docStore = new DocumentIdStore(); - const testCollectionViewProvider = new DocumentProvider( - mockConnectionController, - docStore, - new StatusView(mockExtensionContext) - ); - - const documentIdRef = docStore.add(mockDocument._id); - const uri = vscode.Uri.parse( - `scheme:Results: filename.json?namespace=${TEST_DB_NAME}.ramen&documentId=${documentIdRef}` - ); - - testCollectionViewProvider - .provideTextDocumentContent(uri) - .then((document) => { - assert( - document === docAsString3, - `Expected provideTextDocumentContent to return json stringified string, found ${document}` - ); - done(); - }) - .catch(done); - } - ); - }); - }); -}); diff --git a/src/test/suite/editors/editDocumentCodeLensProvider.test.ts b/src/test/suite/editors/editDocumentCodeLensProvider.test.ts new file mode 100644 index 00000000..0970c358 --- /dev/null +++ b/src/test/suite/editors/editDocumentCodeLensProvider.test.ts @@ -0,0 +1,134 @@ +import assert from 'assert'; +import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensProvider'; + +suite('Edit Document Code Lens Provider Test Suite', () => { + test('provideCodeLenses returns an empty array if codeLensesInfo is empty', () => { + const testCodeLensProvider = new EditDocumentCodeLensProvider(); + const codeLens = testCodeLensProvider.provideCodeLenses(); + + assert(!!codeLens); + assert(codeLens.length === 0); + }); + + test('provideCodeLenses returns one code lens when result is a single document', () => { + const testCodeLensProvider = new EditDocumentCodeLensProvider(); + + testCodeLensProvider.updateCodeLensesPosition({ + namespace: 'db.coll', + type: 'Document', + content: { _id: '93333a0d-83f6-4e6f-a575-af7ea6187a4a' } + }); + + const codeLens = testCodeLensProvider.provideCodeLenses(); + + assert(!!codeLens); + assert(codeLens.length === 1); + const range = codeLens[0].range; + const expectedStartLine = 1; + assert( + range.start.line === expectedStartLine, + `Expected a codeLens position to be at line ${expectedStartLine}, found ${range.start.line}` + ); + const expectedEnd = 1; + assert( + range.end.line === expectedEnd, + `Expected a codeLens position to be at line ${expectedEnd}, found ${range.end.line}` + ); + }); + + test('provideCodeLenses returns two code lenses when result is array of two documents', () => { + const testCodeLensProvider = new EditDocumentCodeLensProvider(); + + testCodeLensProvider.updateCodeLensesPosition({ + namespace: 'db.coll', + type: 'Cursor', + content: [ + { _id: '93333a0d-83f6-4e6f-a575-af7ea6187a4a' }, + { _id: '21333a0d-83f6-4e6f-a575-af7ea6187444' } + ] + }); + + const codeLens = testCodeLensProvider.provideCodeLenses(); + + assert(!!codeLens); + assert(codeLens.length === 2); + const firstRange = codeLens[0].range; + const firstExpectedStartLine = 2; + assert( + firstRange.start.line === firstExpectedStartLine, + `Expected a codeLens position to be at line ${firstExpectedStartLine}, found ${firstRange.start.line}` + ); + const firstExpectedEnd = 2; + assert( + firstRange.end.line === firstExpectedEnd, + `Expected a codeLens position to be at line ${firstExpectedEnd}, found ${firstRange.end.line}` + ); + const secondRange = codeLens[1].range; + const secondExpectedStartLine = 5; + assert( + secondRange.start.line === secondExpectedStartLine, + `Expected a codeLens position to be at line ${secondExpectedStartLine}, found ${secondRange.start.line}` + ); + const secondExpectedEnd = 5; + assert( + secondRange.end.line === secondExpectedEnd, + `Expected a codeLens position to be at line ${secondExpectedEnd}, found ${secondRange.end.line}` + ); + }); + + test('provideCodeLenses returns code lenses when result is ejson array', () => { + const testCodeLensProvider = new EditDocumentCodeLensProvider(); + + testCodeLensProvider.updateCodeLensesPosition({ + namespace: 'db.coll', + type: 'Cursor', + content: [ + { + _id: 4, + item: 'xyz', + price: 5, + quantity: 20, + date: { + $date: '2014-04-04T11:21:39.736Z' + } + }, + { + _id: 5, + item: 'abc', + price: 10, + quantity: 10, + date: { + $date: '2014-04-04T21:23:13.331Z' + } + } + ] + }); + + const codeLens = testCodeLensProvider.provideCodeLenses(); + + assert(!!codeLens); + assert(codeLens.length === 2); + const firstRange = codeLens[0].range; + const firstExpectedStartLine = 2; + assert( + firstRange.start.line === firstExpectedStartLine, + `Expected a codeLens position to be at line ${firstExpectedStartLine}, found ${firstRange.start.line}` + ); + const firstExpectedEnd = 2; + assert( + firstRange.end.line === firstExpectedEnd, + `Expected a codeLens position to be at line ${firstExpectedEnd}, found ${firstRange.end.line}` + ); + const secondRange = codeLens[1].range; + const secondExpectedStartLine = 11; + assert( + secondRange.start.line === secondExpectedStartLine, + `Expected a codeLens position to be at line ${secondExpectedStartLine}, found ${secondRange.start.line}` + ); + const secondExpectedEnd = 11; + assert( + secondRange.end.line === secondExpectedEnd, + `Expected a codeLens position to be at line ${secondExpectedEnd}, found ${secondRange.end.line}` + ); + }); +}); diff --git a/src/test/suite/editors/editorsController.test.ts b/src/test/suite/editors/editorsController.test.ts index f34ab1a5..7f5ae963 100644 --- a/src/test/suite/editors/editorsController.test.ts +++ b/src/test/suite/editors/editorsController.test.ts @@ -1,8 +1,21 @@ +import * as vscode from 'vscode'; import assert from 'assert'; +import { afterEach } from 'mocha'; import { EditorsController } from '../../../editors'; +const sinon = require('sinon'); +const chai = require('chai'); +const expect = chai.expect; + suite('Editors Controller Test Suite', () => { + const sandbox = sinon.createSandbox(); + + afterEach(() => { + sandbox.restore(); + sinon.restore(); + }); + test('getViewCollectionDocumentsUri builds a uri from the namespace and connection info', () => { const testOpId = '100011011101110011'; const testNamespace = 'myFavoriteNamespace'; @@ -23,8 +36,121 @@ suite('Editors Controller Test Suite', () => { ); assert( testUri.query === - 'namespace=myFavoriteNamespace&connectionId=alienSateliteConnection&operationId=100011011101110011', + 'namespace=myFavoriteNamespace&connectionId=alienSateliteConnection&operationId=100011011101110011', `Expected uri query ${testUri.query} to equal 'namespace=myFavoriteNamespace&connectionId=alienSateliteConnection&operationId=100011011101110011'.` ); }); + + test('saveMongoDBDocument returns false if there is no active editor', async () => { + sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => undefined); + + const result = await vscode.commands.executeCommand( + 'mdb.saveMongoDBDocument' + ); + + expect(result).to.be.equal(false); + }); + + test('saveMongoDBDocument returns false if this is not a mongodb document', async () => { + sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ + document: { + scheme: 'file', + uri: { + query: [ + 'namespace=waffle.house', + 'connectionId=tasty_sandwhich', + 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' + ].join('&') + } + } + })); + + const result = await vscode.commands.executeCommand( + 'mdb.saveMongoDBDocument' + ); + + expect(result).to.be.equal(false); + }); + + test('saveMongoDBDocument returns false if this is not a mongodb document and namespace is missing', async () => { + sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ + document: { + uri: { + scheme: 'VIEW_DOCUMENT_SCHEME', + query: [ + 'connectionId=tasty_sandwhich', + 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' + ].join('&') + } + } + })); + + const result = await vscode.commands.executeCommand( + 'mdb.saveMongoDBDocument' + ); + + expect(result).to.be.equal(false); + }); + + test('saveMongoDBDocument returns false if this is not a mongodb document and connectionId is missing', async () => { + sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ + document: { + uri: { + scheme: 'VIEW_DOCUMENT_SCHEME', + query: [ + 'namespace=waffle.house', + 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' + ].join('&') + } + } + })); + + const result = await vscode.commands.executeCommand( + 'mdb.saveMongoDBDocument' + ); + + expect(result).to.be.equal(false); + }); + + test('saveMongoDBDocument returns false if this is not a mongodb document and documentId is missing', async () => { + sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ + document: { + uri: { + scheme: 'VIEW_DOCUMENT_SCHEME', + query: [ + 'namespace=waffle.house', + 'connectionId=tasty_sandwhich' + ].join('&') + } + } + })); + + const result = await vscode.commands.executeCommand( + 'mdb.saveMongoDBDocument' + ); + + expect(result).to.be.equal(false); + }); + + test('saveMongoDBDocument returns false if a user saves an invalid javascript value', async () => { + sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ + document: { + uri: { + scheme: 'VIEW_DOCUMENT_SCHEME', + query: [ + 'namespace=waffle.house', + 'connectionId=tasty_sandwhich', + 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' + ].join('&') + }, + getText: () => '{' + } + })); + + const result = await vscode.commands.executeCommand( + 'mdb.saveMongoDBDocument' + ); + + expect(result).to.be.equal(false); + }); }); diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index 79761170..74985570 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -31,8 +31,9 @@ suite('Playground Controller Test Suite', function () { mockStorageController, mockExtensionContext ); + const testStatusView = new StatusView(mockExtensionContext); const testConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), + testStatusView, mockStorageController, testTelemetryController ); @@ -44,7 +45,8 @@ suite('Playground Controller Test Suite', function () { mockExtensionContext, testConnectionController, mockLanguageServerController as LanguageServerController, - testTelemetryController + testTelemetryController, + testStatusView ); const sandbox = sinon.createSandbox(); let fakeShowInformationMessage: any; @@ -328,7 +330,8 @@ suite('Playground Controller Test Suite', function () { mockExtensionContext, testConnectionController, mockLanguageServerController as LanguageServerController, - testTelemetryController + testTelemetryController, + testStatusView ); expect(playgroundControllerTest.activeTextEditor).to.deep.equal( @@ -336,7 +339,7 @@ suite('Playground Controller Test Suite', function () { ); }); - test('evaluatePlayground should open editor to print results', async () => { + test('evaluatePlayground opens an editor to print results', async () => { await vscode.workspace .getConfiguration('mdb') .update('confirmRunAll', false); @@ -345,64 +348,81 @@ suite('Playground Controller Test Suite', function () { expect(isEditprOpened).to.be.equal(true); }); - test('getVirtualDocumentUri should return json uri if content is object', async () => { + test('getDocumentLanguage returns json if content is object', async () => { await vscode.workspace .getConfiguration('mdb') .update('confirmRunAll', false); - const uri = await testPlaygroundController.getVirtualDocumentUri({ + const language = testPlaygroundController.getDocumentLanguage({ test: 'value' }); - expect(uri.scheme).to.be.equal('PLAYGROUND_RESULT_SCHEME'); - expect(uri.path).to.be.equal('Playground Result.json'); + expect(language).to.be.equal('json'); }); - test('getVirtualDocumentUri should return json uri if content is array', async () => { + test('getDocumentLanguage returns json if content is array', async () => { await vscode.workspace .getConfiguration('mdb') .update('confirmRunAll', false); - const uri = await testPlaygroundController.getVirtualDocumentUri([ + const language = await testPlaygroundController.getDocumentLanguage([ { test: 'value' } ]); - expect(uri.scheme).to.be.equal('PLAYGROUND_RESULT_SCHEME'); - expect(uri.path).to.be.equal('Playground Result.json'); + expect(language).to.be.equal('json'); }); - test('getVirtualDocumentUri should return json uri if content is object with BSON value', async () => { + test('getDocumentLanguage returns json if content is object with BSON value', async () => { await vscode.workspace .getConfiguration('mdb') .update('confirmRunAll', false); - const uri = await testPlaygroundController.getVirtualDocumentUri({ + const language = await testPlaygroundController.getDocumentLanguage({ _id: { $oid: '5d973ae7443762aae72a160' } }); - expect(uri.scheme).to.be.equal('PLAYGROUND_RESULT_SCHEME'); - expect(uri.path).to.be.equal('Playground Result.json'); + expect(language).to.be.equal('json'); }); - test('getVirtualDocumentUri should return txt uri if content is string', async () => { + test('getDocumentLanguage returns plaintext if content is string', async () => { await vscode.workspace .getConfiguration('mdb') .update('confirmRunAll', false); - const uri = await testPlaygroundController.getVirtualDocumentUri( + const language = await testPlaygroundController.getDocumentLanguage( 'I am a string' ); - expect(uri.scheme).to.be.equal('PLAYGROUND_RESULT_SCHEME'); - expect(uri.path).to.be.equal('Playground Result.txt'); + 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 = await testPlaygroundController.getDocumentLanguage(12); + + expect(language).to.be.equal('plaintext'); }); - test('getVirtualDocumentUri should return txt uri if content is number', async () => { + test('getDocumentLanguage returns plaintext if content is undefined', async () => { await vscode.workspace .getConfiguration('mdb') .update('confirmRunAll', false); - const uri = await testPlaygroundController.getVirtualDocumentUri(12); + const language = await 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 = await testPlaygroundController.getDocumentLanguage( + undefined + ); - expect(uri.scheme).to.be.equal('PLAYGROUND_RESULT_SCHEME'); - expect(uri.path).to.be.equal('Playground Result.txt'); + expect(language).to.be.equal('plaintext'); }); }); }); diff --git a/src/test/suite/editors/playgroundResultProvider.test.ts b/src/test/suite/editors/playgroundResultProvider.test.ts new file mode 100644 index 00000000..46516c74 --- /dev/null +++ b/src/test/suite/editors/playgroundResultProvider.test.ts @@ -0,0 +1,227 @@ +import PlaygroundResultProvider from '../../../editors/playgroundResultProvider'; +import { TestExtensionContext } from '../stubs'; +import { afterEach } from 'mocha'; + +const sinon = require('sinon'); +const chai = require('chai'); +const expect = chai.expect; + +suite('Playground Result Provider Test Suite', () => { + const mockExtensionContext = new TestExtensionContext(); + + afterEach(() => { + sinon.restore(); + }); + + test('sets default playground result', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + + expect(testPlaygroundResultViewProvider._playgroundResult).to.be.deep.equal( + { + namespace: null, + type: null, + content: undefined + } + ); + }); + + test('refreshes playground result', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + const playgroundResult = { + namespace: 'db.berlin', + type: 'Cursor', + content: { + _id: '93333a0d-83f6-4e6f-a575-af7ea6187a4a', + name: 'Berlin' + } + }; + + testPlaygroundResultViewProvider.setPlaygroundResult(playgroundResult); + + expect(testPlaygroundResultViewProvider._playgroundResult).to.be.deep.equal( + playgroundResult + ); + }); + + test('returns undefined formatted to string if content is undefined', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + + testPlaygroundResultViewProvider._playgroundResult = { + namespace: 'db.berlin', + type: 'undefined', + content: null + }; + + const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); + + expect(result).to.be.equal('undefined'); + }); + + test('returns null formatted to string if content is null', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + + testPlaygroundResultViewProvider._playgroundResult = { + namespace: 'db.berlin', + type: 'object', + content: null + }; + + const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); + + expect(result).to.be.equal('null'); + }); + + test('returns number formatted to string if content is number', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + + testPlaygroundResultViewProvider._playgroundResult = { + namespace: 'db.berlin', + type: 'number', + content: 4 + }; + + const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); + + expect(result).to.be.equal('4'); + }); + + test('returns array formatted to string if content is array', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + + testPlaygroundResultViewProvider._playgroundResult = { + namespace: 'db.berlin', + type: 'object', + content: [] + }; + + const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); + + expect(result).to.be.equal('[]'); + }); + + test('returns object formatted to string if content is object', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + + testPlaygroundResultViewProvider._playgroundResult = { + namespace: 'db.berlin', + type: 'object', + content: {} + }; + + const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); + + expect(result).to.be.equal('{}'); + }); + + test('returns boolean formatted to string if content is boolean', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + + testPlaygroundResultViewProvider._playgroundResult = { + namespace: 'db.berlin', + type: 'boolean', + content: true + }; + + const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); + + expect(result).to.be.equal('true'); + }); + + test('returns string if content is string', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + + testPlaygroundResultViewProvider._playgroundResult = { + namespace: 'db.berlin', + type: 'string', + content: 'Berlin' + }; + + const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); + + expect(result).to.be.equal('Berlin'); + }); + + test('returns Cursor formatted to string if content is string', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + const content = [ + { + _id: '93333a0d-83f6-4e6f-a575-af7ea6187a4a', + name: 'Berlin' + }, + { + _id: '55333a0d-83f6-4e6f-a575-af7ea6187a55', + name: 'Rome' + } + ]; + const playgroundResult = { + namespace: 'db.berlin', + type: 'Cursor', + content + }; + + const mockRefresh = sinon.fake.resolves(); + sinon.replace( + testPlaygroundResultViewProvider._editDocumentCodeLensProvider, + 'updateCodeLensesPosition', + mockRefresh + ); + + testPlaygroundResultViewProvider._playgroundResult = playgroundResult; + + const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); + mockRefresh.firstArg; + + expect(result).to.be.equal(JSON.stringify(content, null, 2)); + expect(mockRefresh.firstArg).to.be.deep.equal(playgroundResult); + }); + + test('returns Document formatted to string if content is string', () => { + const testPlaygroundResultViewProvider = new PlaygroundResultProvider( + mockExtensionContext + ); + const content = { + _id: '20213a0d-83f6-4e6f-a575-af7ea6187lala', + name: 'Minsk' + }; + const playgroundResult = { + namespace: 'db.berlin', + type: 'Document', + content + }; + + const mockRefresh = sinon.fake.resolves(); + sinon.replace( + testPlaygroundResultViewProvider._editDocumentCodeLensProvider, + 'updateCodeLensesPosition', + mockRefresh + ); + + testPlaygroundResultViewProvider._playgroundResult = playgroundResult; + + const result = testPlaygroundResultViewProvider.provideTextDocumentContent(); + mockRefresh.firstArg; + + expect(result).to.be.equal(JSON.stringify(content, null, 2)); + expect(mockRefresh.firstArg).to.be.deep.equal(playgroundResult); + }); +}); diff --git a/src/test/suite/language/languageServerController.test.ts b/src/test/suite/language/languageServerController.test.ts index 95ac0fd6..6df4e36b 100644 --- a/src/test/suite/language/languageServerController.test.ts +++ b/src/test/suite/language/languageServerController.test.ts @@ -14,7 +14,6 @@ import { LanguageServerController } from '../../../language'; import ConnectionController from '../../../connectionController'; import { StatusView } from '../../../views'; import { StorageController } from '../../../storage'; - import { TestExtensionContext } from '../stubs'; import { mdbTestExtension } from '../stubbableMdbExtension'; @@ -36,20 +35,21 @@ suite('Language Server Controller Test Suite', () => { mockStorageController, mockExtensionContext ); - - testLanguageServerController.startLanguageServer(); - + const testStatusView = new StatusView(mockExtensionContext); const testConnectionController = new ConnectionController( - new StatusView(mockExtensionContext), + testStatusView, mockStorageController, testTelemetryController ); + testLanguageServerController.startLanguageServer(); + const testPlaygroundController = new PlaygroundController( mockExtensionContext, testConnectionController, testLanguageServerController, - testTelemetryController + testTelemetryController, + testStatusView ); before(async () => { diff --git a/src/test/suite/language/mongoDBService.test.ts b/src/test/suite/language/mongoDBService.test.ts index 9889fe7f..6147e7a3 100644 --- a/src/test/suite/language/mongoDBService.test.ts +++ b/src/test/suite/language/mongoDBService.test.ts @@ -993,7 +993,7 @@ suite('MongoDBService Test Suite', () => { ); const expectedResult = { outputLines: [], - result: { type: 'number', content: 2 } + result: { namespace: null, type: 'number', content: 2 } }; expect(result).to.deep.equal(expectedResult); @@ -1011,7 +1011,7 @@ suite('MongoDBService Test Suite', () => { ); const expectedResult = { outputLines: [], - result: { type: 'number', content: 3 } + result: { namespace: null, type: 'number', content: 3 } }; expect(result).to.deep.equal(expectedResult); @@ -1029,7 +1029,7 @@ suite('MongoDBService Test Suite', () => { ); const firstRes = { outputLines: [], - result: { type: 'number', content: 2 } + result: { namespace: null, type: 'number', content: 2 } }; expect(firstEvalResult).to.deep.equal(firstRes); @@ -1042,7 +1042,7 @@ suite('MongoDBService Test Suite', () => { ); const secondRes = { outputLines: [], - result: { type: 'number', content: 3 } + result: { namespace: null, type: 'number', content: 3 } }; expect(secondEvalResult).to.deep.equal(secondRes); @@ -1063,6 +1063,7 @@ suite('MongoDBService Test Suite', () => { const expectedResult = { outputLines: [], result: { + namespace: null, type: 'object', content: { _id: { @@ -1089,6 +1090,7 @@ suite('MongoDBService Test Suite', () => { const expectedResult = { outputLines: [], result: { + namespace: null, type: 'string', content: 'A single line string' } @@ -1113,6 +1115,7 @@ suite('MongoDBService Test Suite', () => { const expectedResult = { outputLines: [], result: { + namespace: null, type: 'string', content: `vscode is @@ -1135,12 +1138,12 @@ suite('MongoDBService Test Suite', () => { ); const expectedResult = { outputLines: [ - { type: null, content: 'Hello' }, - { type: null, content: 1 }, - { type: null, content: 2 }, - { type: null, content: 3 } + { namespace: null, type: null, content: 'Hello' }, + { namespace: null, type: null, content: 1 }, + { namespace: null, type: null, content: 2 }, + { namespace: null, type: null, content: 3 } ], - result: { type: 'number', content: 42 } + result: { namespace: null, type: 'number', content: 42 } }; expect(result).to.deep.equal(expectedResult); diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 3178c813..f716b1b1 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -1,5 +1,4 @@ import assert from 'assert'; -import * as fse from 'fs-extra'; import * as vscode from 'vscode'; import { afterEach, beforeEach } from 'mocha'; import Connection = require('mongodb-connection-model/lib/model'); @@ -1255,8 +1254,8 @@ suite('MDBExtensionController Test Suite', function () { sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ document: { uri: { + scheme: 'VIEW_DOCUMENT_SCHEME', query: [ - '?documentLocation=mongodb', 'namespace=waffle.house', 'connectionId=tasty_sandwhich', 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' @@ -1288,10 +1287,11 @@ suite('MDBExtensionController Test Suite', function () { filter: object, replacement: object, options: object, - callback: (error: Error | undefined, result: object) => void + callback: (error: Error | null, result: object) => void ) => { mockDocument.name = 'something sweet'; - callback(undefined, mockDocument); + + return callback(null, mockDocument); } }); sinon.replace( @@ -1302,27 +1302,24 @@ suite('MDBExtensionController Test Suite', function () { const documentItem = new DocumentTreeItem(mockDocument, 'waffle.house', 0); - await vscode.commands.executeCommand('mdb.viewDocument', documentItem); - - assert( - mockOpenTextDocument.firstArg.path.includes('vscode-opened-documents') + await vscode.commands.executeCommand( + 'mdb.openMongoDBDocumentFromTree', + documentItem ); + assert(mockOpenTextDocument.firstArg.path.includes('.json')); - assert(mockOpenTextDocument.firstArg.scheme === 'file'); + assert(mockOpenTextDocument.firstArg.scheme === 'VIEW_DOCUMENT_SCHEME'); assert(mockOpenTextDocument.firstArg.query.includes('documentId=')); assert(mockOpenTextDocument.firstArg.query.includes('connectionId=')); assert( mockOpenTextDocument.firstArg.query.includes('namespace=waffle.house') ); - assert( - mockOpenTextDocument.firstArg.query.includes('documentLocation=mongodb') - ); assert( mockShowTextDocument.firstArg === 'magna carta', 'Expected it to call vscode to show the returned document from the provider' ); - await vscode.commands.executeCommand('mdb.saveDocumentToMongoDB'); + await vscode.commands.executeCommand('mdb.saveMongoDBDocument'); assert(mockDocument.name === 'something sweet'); assert(mockDocument.time.$time === '12345'); @@ -1334,411 +1331,6 @@ suite('MDBExtensionController Test Suite', function () { fakeShowInformationMessage.firstArg === expectedMessage, `Expected an error message "${expectedMessage}" to be shown when attempting to add a database to a not connected connection found "${fakeShowInformationMessage.firstArg}"` ); - - await fse.remove(mockOpenTextDocument.firstArg.path); - }); - - test('if the active editor is missing, the save function does not call findOneAndReplace', async () => { - const mockDocument = { - _id: 'pancakes', - name: '' - }; - - sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => null); - - const mockGetActiveDataService = sinon.fake.returns({ - findOneAndReplace: ( - namespace: string, - filter: object, - replacement: object, - options: object, - callback: (error: Error | undefined, result: object) => void - ) => { - mockDocument.name = 'something sweet'; - callback(undefined, mockDocument); - } - }); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getActiveDataService', - mockGetActiveDataService - ); - - await vscode.commands.executeCommand('mdb.saveDocumentToMongoDB'); - - assert(mockDocument.name === ''); - }); - - test("json files that are not MongoDB documents won't be saved to database", async () => { - const mockDocument = { - _id: 'pancakes', - name: '' - }; - - sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ - document: { - uri: { - query: [ - 'namespace=waffle.house', - 'connectionId=tasty_sandwhich', - 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' - ].join('&') - } - } - })); - - const mockGetActiveDataService = sinon.fake.returns({ - findOneAndReplace: ( - namespace: string, - filter: object, - replacement: object, - options: object, - callback: (error: Error | undefined, result: object) => void - ) => { - mockDocument.name = 'something sweet'; - callback(undefined, mockDocument); - } - }); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getActiveDataService', - mockGetActiveDataService - ); - - await vscode.commands.executeCommand('mdb.saveDocumentToMongoDB'); - - assert(mockDocument.name === ''); - }); - - test("MongoDB documents without namespace parameter won't be saved to database", async () => { - const mockDocument = { - _id: 'pancakes', - name: '' - }; - - sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ - document: { - uri: { - query: [ - '?documentLocation=mongodb', - 'connectionId=tasty_sandwhich', - 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' - ].join('&') - } - } - })); - - const mockGetActiveDataService = sinon.fake.returns({ - findOneAndReplace: ( - namespace: string, - filter: object, - replacement: object, - options: object, - callback: (error: Error | undefined, result: object) => void - ) => { - mockDocument.name = 'something sweet'; - callback(undefined, mockDocument); - } - }); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getActiveDataService', - mockGetActiveDataService - ); - - await vscode.commands.executeCommand('mdb.saveDocumentToMongoDB'); - - assert(mockDocument.name === ''); - }); - - test("MongoDB documents without connectionId parameter won't be saved to database", async () => { - const mockDocument = { - _id: 'pancakes', - name: '' - }; - - sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ - document: { - uri: { - query: [ - '?documentLocation=mongodb', - 'namespace=waffle.house', - 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' - ].join('&') - } - } - })); - - const mockGetActiveDataService = sinon.fake.returns({ - findOneAndReplace: ( - namespace: string, - filter: object, - replacement: object, - options: object, - callback: (error: Error | undefined, result: object) => void - ) => { - mockDocument.name = 'something sweet'; - callback(undefined, mockDocument); - } - }); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getActiveDataService', - mockGetActiveDataService - ); - - await vscode.commands.executeCommand('mdb.saveDocumentToMongoDB'); - - assert(mockDocument.name === ''); - }); - - test("MongoDB documents without documentId parameter won't be saved to database", async () => { - const mockDocument = { - _id: 'pancakes', - name: '' - }; - - sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ - document: { - uri: { - query: [ - '?documentLocation=mongodb', - 'namespace=waffle.house', - 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' - ].join('&') - } - } - })); - - const mockGetActiveDataService = sinon.fake.returns({ - findOneAndReplace: ( - namespace: string, - filter: object, - replacement: object, - options: object, - callback: (error: Error | undefined, result: object) => void - ) => { - mockDocument.name = 'something sweet'; - callback(undefined, mockDocument); - } - }); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getActiveDataService', - mockGetActiveDataService - ); - - await vscode.commands.executeCommand('mdb.saveDocumentToMongoDB'); - - assert(mockDocument.name === ''); - }); - - test("if a user is not connected, documents won't be saved to MongoDB", async () => { - const fakeVscodeErrorMessage = sinon.fake(); - sinon.replace(vscode.window, 'showErrorMessage', fakeVscodeErrorMessage); - - sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ - document: { - uri: { - query: [ - '?documentLocation=mongodb', - 'namespace=waffle.house', - 'connectionId=tasty_sandwhich', - 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' - ].join('&') - } - } - })); - - const mockGet = sinon.fake.returns('pancakes'); - sinon.replace( - mdbTestExtension.testExtensionController._editorsController - ._documentIdStore, - 'get', - mockGet - ); - - const mockActiveConnectionId = sinon.fake.returns(null); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getActiveConnectionId', - mockActiveConnectionId - ); - - const mockGetSavedConnectionName = sinon.fake.returns('connect:27017'); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getSavedConnectionName', - mockGetSavedConnectionName - ); - - await vscode.commands.executeCommand('mdb.saveDocumentToMongoDB'); - - const expectedMessage = - "Unable to save document: no longer connected to 'connect:27017'"; - - assert( - fakeVscodeErrorMessage.firstArg === expectedMessage, - `Expected an error message "${expectedMessage}" to be shown when attempting to add a database to a not connected connection found "${fakeVscodeErrorMessage.firstArg}"` - ); - }); - - test("if a user switched the active connection, document opened from the previous connection can't be saved", async () => { - const fakeVscodeErrorMessage = sinon.fake(); - sinon.replace(vscode.window, 'showErrorMessage', fakeVscodeErrorMessage); - - const mockGet = sinon.fake.returns('pancakes'); - sinon.replace( - mdbTestExtension.testExtensionController._editorsController - ._documentIdStore, - 'get', - mockGet - ); - - sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ - document: { - uri: { - query: [ - '?documentLocation=mongodb', - 'namespace=waffle.house', - 'connectionId=tasty_sandwhich', - 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' - ].join('&') - } - } - })); - - const mockActiveConnectionId = sinon.fake.returns('berlin.coctails'); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getActiveConnectionId', - mockActiveConnectionId - ); - - const mockGetSavedConnectionName = sinon.fake.returns('connect:27017'); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getSavedConnectionName', - mockGetSavedConnectionName - ); - - await vscode.commands.executeCommand('mdb.saveDocumentToMongoDB'); - - const expectedMessage = - "Unable to save document: no longer connected to 'connect:27017'"; - - assert( - fakeVscodeErrorMessage.firstArg === expectedMessage, - `Expected an error message "${expectedMessage}" to be shown when attempting to add a database to a not connected connection found "${fakeVscodeErrorMessage.firstArg}"` - ); - }); - - test('if dataservice is missing, an error occurs', async () => { - const fakeVscodeErrorMessage = sinon.fake(); - sinon.replace(vscode.window, 'showErrorMessage', fakeVscodeErrorMessage); - - const mockGet = sinon.fake.returns('pancakes'); - sinon.replace( - mdbTestExtension.testExtensionController._editorsController - ._documentIdStore, - 'get', - mockGet - ); - - sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ - document: { - uri: { - query: [ - '?documentLocation=mongodb', - 'namespace=waffle.house', - 'connectionId=tasty_sandwhich', - 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' - ].join('&') - } - } - })); - - const mockGetActiveDataService = sinon.fake.returns(null); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getActiveDataService', - mockGetActiveDataService - ); - - const mockActiveConnectionId = sinon.fake.returns('tasty_sandwhich'); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getActiveConnectionId', - mockActiveConnectionId - ); - - const mockGetSavedConnectionName = sinon.fake.returns('connect:27017'); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getSavedConnectionName', - mockGetSavedConnectionName - ); - - await vscode.commands.executeCommand('mdb.saveDocumentToMongoDB'); - - const expectedMessage = - "Unable to save document: no longer connected to 'connect:27017'"; - - assert( - fakeVscodeErrorMessage.firstArg === expectedMessage, - `Expected an error message "${expectedMessage}" to be shown when attempting to add a database to a not connected connection found "${fakeVscodeErrorMessage.firstArg}"` - ); - }); - - test('if a user saves an invalid javascript value, an error occurs', async () => { - const fakeVscodeErrorMessage = sinon.fake(); - sinon.replace(vscode.window, 'showErrorMessage', fakeVscodeErrorMessage); - - const mockGet = sinon.fake.returns('pancakes'); - sinon.replace( - mdbTestExtension.testExtensionController._editorsController - ._documentIdStore, - 'get', - mockGet - ); - - sandbox.replaceGetter(vscode.window, 'activeTextEditor', () => ({ - document: { - uri: { - query: [ - '?documentLocation=mongodb', - 'namespace=waffle.house', - 'connectionId=tasty_sandwhich', - 'documentId=93333a0d-83f6-4e6f-a575-af7ea6187a4a' - ].join('&') - }, - getText: () => '{' - } - })); - - const mockActiveConnectionId = sinon.fake.returns('tasty_sandwhich'); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getActiveConnectionId', - mockActiveConnectionId - ); - - const mockGetSavedConnectionName = sinon.fake.returns('connect:27017'); - sinon.replace( - mdbTestExtension.testExtensionController._connectionController, - 'getSavedConnectionName', - mockGetSavedConnectionName - ); - - await vscode.commands.executeCommand('mdb.saveDocumentToMongoDB'); - - const expectedMessage = - "Unable to save document: no longer connected to 'connect:27017'"; - - assert( - fakeVscodeErrorMessage.firstArg === expectedMessage, - `Expected an error message "${expectedMessage}" to be shown when attempting to add a database to a not connected connection found "${fakeVscodeErrorMessage.firstArg}"` - ); }); test('mdb.searchForDocuments should create a MongoDB playground with search template', async () => { @@ -1800,9 +1392,11 @@ suite('MDBExtensionController Test Suite', function () { const mockShowTextDocument = sinon.fake.resolves(); sinon.replace(vscode.window, 'showTextDocument', mockShowTextDocument); - await vscode.workspace - .getConfiguration('mdb') - .update('useDefaultTemplateForPlayground', true); + const mockGetConfiguration = sinon.fake.returns({ + get: () => true + }); + sinon.replace(vscode.workspace, 'getConfiguration', mockGetConfiguration); + await vscode.commands.executeCommand('mdb.createPlayground'); assert(mockOpenTextDocument.firstArg.language === 'mongodb'); @@ -1841,9 +1435,11 @@ suite('MDBExtensionController Test Suite', function () { const mockShowTextDocument = sinon.fake.resolves(); sinon.replace(vscode.window, 'showTextDocument', mockShowTextDocument); - await vscode.workspace - .getConfiguration('mdb') - .update('useDefaultTemplateForPlayground', false); + const mockGetConfiguration = sinon.fake.returns({ + get: () => false + }); + sinon.replace(vscode.workspace, 'getConfiguration', mockGetConfiguration); + await vscode.commands.executeCommand('mdb.createPlayground'); assert(mockOpenTextDocument.firstArg.language === 'mongodb'); diff --git a/src/test/suite/stubs.ts b/src/test/suite/stubs.ts index 3c77f3db..3c0c409d 100644 --- a/src/test/suite/stubs.ts +++ b/src/test/suite/stubs.ts @@ -166,8 +166,7 @@ const mockVSCodeTextDocument = { undefined, validateRange: (range: vscode.Range): vscode.Range => mockRange, - validatePosition: (position: vscode.Position): vscode.Position => - mockPosition + validatePosition: (position: vscode.Position): vscode.Position => mockPosition }; class MockLanguageServerController { @@ -196,7 +195,7 @@ class MockLanguageServerController { executeAll(codeToEvaluate: string): Promise { return Promise.resolve({ outputLines: [], - result: { type: null, content: 'Result' } + result: { namespace: null, type: null, content: 'Result' } }); } diff --git a/src/test/suite/telemetry/telemetryController.test.ts b/src/test/suite/telemetry/telemetryController.test.ts index 8d66b634..9d4a8fe5 100644 --- a/src/test/suite/telemetry/telemetryController.test.ts +++ b/src/test/suite/telemetry/telemetryController.test.ts @@ -186,7 +186,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert AggregationCursor shellApiType to aggregation telemetry type', () => { const res = { outputLines: [], - result: { type: 'AggregationCursor', content: '' } + result: { namespace: null, type: 'AggregationCursor', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -196,7 +196,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert BulkWriteResult shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'BulkWriteResult', content: '' } + result: { namespace: null, type: 'BulkWriteResult', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -206,7 +206,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert Collection shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'Collection', content: '' } + result: { namespace: null, type: 'Collection', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -216,7 +216,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert Cursor shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'Cursor', content: '' } + result: { namespace: null, type: 'Cursor', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -226,7 +226,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert Database shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'Database', content: '' } + result: { namespace: null, type: 'Database', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -236,7 +236,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert DeleteResult shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'DeleteResult', content: '' } + result: { namespace: null, type: 'DeleteResult', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -246,7 +246,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert InsertManyResult shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'InsertManyResult', content: '' } + result: { namespace: null, type: 'InsertManyResult', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -256,7 +256,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert InsertOneResult shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'InsertOneResult', content: '' } + result: { namespace: null, type: 'InsertOneResult', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -266,7 +266,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert ReplicaSet shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'ReplicaSet', content: '' } + result: { namespace: null, type: 'ReplicaSet', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -276,7 +276,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert Shard shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'Shard', content: '' } + result: { namespace: null, type: 'Shard', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -286,7 +286,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert ShellApi shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'ShellApi', content: '' } + result: { namespace: null, type: 'ShellApi', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -296,7 +296,7 @@ suite('Telemetry Controller Test Suite', () => { test('convert UpdateResult shellApiType to other telemetry type', () => { const res = { outputLines: [], - result: { type: 'UpdateResult', content: '' } + result: { namespace: null, type: 'UpdateResult', content: '' } }; const type = testTelemetryController.getPlaygroundResultType(res); @@ -306,7 +306,7 @@ suite('Telemetry Controller Test Suite', () => { test('return other telemetry type if evaluation returns a string', () => { const res = { outputLines: [], - result: { type: null, content: '2' } + result: { namespace: null, type: null, content: '2' } }; const type = testTelemetryController.getPlaygroundResultType(res); diff --git a/src/utils/types.ts b/src/utils/types.ts index 266717da..f2fe2c35 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,9 +1,16 @@ export type OutputItem = { + namespace: string | null; type: string | null; - content: string; + content: any; }; export type ExecuteAllResult = { outputLines: OutputItem[] | undefined; result: OutputItem | undefined; }; + +export type DocCodeLensesInfo = { + line: number; + documentId: string; + namespace: string; +}[];