diff --git a/package-lock.json b/package-lock.json index 80ff5d0629..189c17dd2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6545,20 +6545,6 @@ "tar": "^6.1.15" } }, - "node_modules/@mongodb-js/mongodb-ts-autocomplete": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-ts-autocomplete/-/mongodb-ts-autocomplete-0.2.2.tgz", - "integrity": "sha512-5GwS2zm8OKJeWFK25PalpstMimIrnif1T07Ur+HECOCFhndTQmIV7VJve86GBwrnmM5Cy4P4Pv7FrjwvfE222A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/ts-autocomplete": "^0.3.1", - "@mongosh/shell-api": "^3.11.0", - "mongodb-schema": "^12.6.2", - "node-cache": "^5.1.2", - "typescript": "^5.0.4" - } - }, "node_modules/@mongodb-js/monorepo-tools": { "version": "1.1.16", "resolved": "https://registry.npmjs.org/@mongodb-js/monorepo-tools/-/monorepo-tools-1.1.16.tgz", @@ -33900,6 +33886,7 @@ "license": "Apache-2.0", "dependencies": { "@mongodb-js/mongodb-constants": "^0.10.1", + "@mongodb-js/mongodb-ts-autocomplete": "^0.2.5", "@mongosh/shell-api": "^3.11.0", "semver": "^7.5.4" }, @@ -33916,6 +33903,21 @@ "node": ">=14.15.1" } }, + "packages/autocomplete/node_modules/@mongodb-js/mongodb-ts-autocomplete": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-ts-autocomplete/-/mongodb-ts-autocomplete-0.2.5.tgz", + "integrity": "sha512-9Os75QCF+lSLBP7Wank37bCrFTX27Y6GI4HP889TZ88ArLOyKpboS4BK43ARgB0Rg4/mhog8d7jT6OlVb6VwYA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/ts-autocomplete": "^0.3.1", + "mongodb-schema": "^12.6.2", + "node-cache": "^5.1.2", + "typescript": "^5.0.4" + }, + "peerDependencies": { + "@mongosh/shell-api": "^3.11.0" + } + }, "packages/browser-repl": { "name": "@mongosh/browser-repl", "version": "3.11.0", @@ -34121,6 +34123,7 @@ }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0", + "@mongodb-js/mongodb-ts-autocomplete": "^0.2.5", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-mongosh": "^1.0.0", "@mongosh/types": "3.6.2", @@ -34134,6 +34137,22 @@ "node": ">=14.15.1" } }, + "packages/browser-runtime-core/node_modules/@mongodb-js/mongodb-ts-autocomplete": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-ts-autocomplete/-/mongodb-ts-autocomplete-0.2.5.tgz", + "integrity": "sha512-9Os75QCF+lSLBP7Wank37bCrFTX27Y6GI4HP889TZ88ArLOyKpboS4BK43ARgB0Rg4/mhog8d7jT6OlVb6VwYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/ts-autocomplete": "^0.3.1", + "mongodb-schema": "^12.6.2", + "node-cache": "^5.1.2", + "typescript": "^5.0.4" + }, + "peerDependencies": { + "@mongosh/shell-api": "^3.11.0" + } + }, "packages/browser-runtime-electron": { "name": "@mongosh/browser-runtime-electron", "version": "3.11.0", @@ -34329,7 +34348,6 @@ "license": "Apache-2.0", "dependencies": { "@mongodb-js/devtools-proxy-support": "^0.4.2", - "@mongodb-js/mongodb-ts-autocomplete": "^0.2.4", "@mongosh/arg-parser": "^3.10.3", "@mongosh/autocomplete": "^3.11.0", "@mongosh/editor": "^3.11.0", @@ -34394,21 +34412,6 @@ "win-export-certificate-and-key": "^2.1.0" } }, - "packages/cli-repl/node_modules/@mongodb-js/mongodb-ts-autocomplete": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-ts-autocomplete/-/mongodb-ts-autocomplete-0.2.4.tgz", - "integrity": "sha512-CuE9NnAP8outZY6VS6fZV4hBZGnrVQyhDMCaL+bgoR5dg6nDcrZtNNVbxc976eUVDi0MZhZmYOB/z7sLvi1xFQ==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/ts-autocomplete": "^0.3.1", - "mongodb-schema": "^12.6.2", - "node-cache": "^5.1.2", - "typescript": "^5.0.4" - }, - "peerDependencies": { - "@mongosh/shell-api": "^3.11.0" - } - }, "packages/cli-repl/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -35122,7 +35125,7 @@ "devDependencies": { "@microsoft/api-extractor": "^7.39.3", "@mongodb-js/eslint-config-mongosh": "^1.0.0", - "@mongodb-js/mongodb-ts-autocomplete": "^0.2.2", + "@mongodb-js/mongodb-ts-autocomplete": "^0.2.5", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-mongosh": "^1.0.0", "@mongosh/types": "3.6.2", @@ -35137,6 +35140,22 @@ "node": ">=14.15.1" } }, + "packages/shell-api/node_modules/@mongodb-js/mongodb-ts-autocomplete": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-ts-autocomplete/-/mongodb-ts-autocomplete-0.2.5.tgz", + "integrity": "sha512-9Os75QCF+lSLBP7Wank37bCrFTX27Y6GI4HP889TZ88ArLOyKpboS4BK43ARgB0Rg4/mhog8d7jT6OlVb6VwYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/ts-autocomplete": "^0.3.1", + "mongodb-schema": "^12.6.2", + "node-cache": "^5.1.2", + "typescript": "^5.0.4" + }, + "peerDependencies": { + "@mongosh/shell-api": "^3.11.0" + } + }, "packages/shell-api/node_modules/mongodb-redact": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/mongodb-redact/-/mongodb-redact-1.1.5.tgz", diff --git a/packages/autocomplete/package.json b/packages/autocomplete/package.json index 09cce0d086..69c0f46bbb 100644 --- a/packages/autocomplete/package.json +++ b/packages/autocomplete/package.json @@ -45,6 +45,7 @@ "dependencies": { "@mongodb-js/mongodb-constants": "^0.10.1", "@mongosh/shell-api": "^3.11.0", + "@mongodb-js/mongodb-ts-autocomplete": "^0.2.5", "semver": "^7.5.4" } } diff --git a/packages/autocomplete/src/direct-command-completer.ts b/packages/autocomplete/src/direct-command-completer.ts new file mode 100644 index 0000000000..de1ed790b4 --- /dev/null +++ b/packages/autocomplete/src/direct-command-completer.ts @@ -0,0 +1,52 @@ +import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete'; +import type { TypeSignature } from '@mongosh/shell-api'; +import { signatures as shellSignatures } from '@mongosh/shell-api'; + +type TypeSignatureAttributes = { [key: string]: TypeSignature }; + +export async function directCommandCompleter( + context: AutocompletionContext, + line: string +): Promise { + const SHELL_COMPLETIONS = shellSignatures.ShellApi + .attributes as TypeSignatureAttributes; + + // Split at space-to-non-space transitions when looking at this as a command, + // because multiple spaces (e.g. 'show collections') are valid in commands. + // This split keeps the spaces intact so we can join them back later. + const splitLineWhitespace = line.split(/(? item.trim()) + )) || []; + // Adjust to full input, because `completer` only completed the last item + // in the line, e.g. ['profile'] -> ['show profile'] + const fullLineHits = hits.map((hit) => + [...splitLineWhitespace.slice(0, -1), hit].join('') + ); + + return fullLineHits; +} diff --git a/packages/autocomplete/src/index.spec.ts b/packages/autocomplete/src/index.spec.ts index f12bd2bd17..43e3a620ad 100644 --- a/packages/autocomplete/src/index.spec.ts +++ b/packages/autocomplete/src/index.spec.ts @@ -1,4 +1,4 @@ -import completer, { BASE_COMPLETIONS } from './'; +import { completer, BASE_COMPLETIONS } from './'; import { signatures as shellSignatures, Topologies } from '@mongosh/shell-api'; import { expect } from 'chai'; diff --git a/packages/autocomplete/src/index.ts b/packages/autocomplete/src/index.ts index 1b6f86be4f..eafa68a0a6 100644 --- a/packages/autocomplete/src/index.ts +++ b/packages/autocomplete/src/index.ts @@ -1,5 +1,6 @@ import type { Topologies, TypeSignature } from '@mongosh/shell-api'; import { signatures as shellSignatures } from '@mongosh/shell-api'; +import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete'; import semver from 'semver'; import { CONVERSION_OPERATORS, @@ -13,6 +14,7 @@ import { ON_PREM, DATABASE, } from '@mongodb-js/mongodb-constants'; +import { directCommandCompleter } from './direct-command-completer'; type TypeSignatureAttributes = { [key: string]: TypeSignature }; @@ -78,7 +80,7 @@ const GROUP = '$group'; * * @returns {array} Matching Completions, Current User Input. */ -async function completer( +export async function completer( params: AutocompleteParameters, line: string ): Promise<[string[], string, 'exclusive'] | [string[], string]> { @@ -398,4 +400,51 @@ function filterShellAPI( return hits; } -export default completer; +type AutocompleteShellInstanceState = { + getAutocompleteParameters: () => AutocompleteParameters; + getAutocompletionContext: () => AutocompletionContext; +}; + +function transformAutocompleteResults( + line: string, + results: { result: string }[] +): [string[], string] { + return [results.map((result) => result.result), line]; +} + +export type CompletionResults = + | [string[], string, 'exclusive'] + | [string[], string]; + +export async function initNewAutocompleter( + instanceState: Pick< + AutocompleteShellInstanceState, + 'getAutocompletionContext' + > +): Promise<(text: string) => Promise> { + // only import the autocompleter code the first time we need it to + // hide the time it takes from the initial startup time + const { MongoDBAutocompleter } = await import( + '@mongodb-js/mongodb-ts-autocomplete' + ); + + const autocompletionContext = instanceState.getAutocompletionContext(); + const mongoDBCompleter = new MongoDBAutocompleter({ + context: autocompletionContext, + }); + + return async (text: string): Promise => { + const directResults = await directCommandCompleter( + autocompletionContext, + text + ); + + if (directResults.length) { + return [directResults, text, 'exclusive']; + } + + const results = await mongoDBCompleter.autocomplete(text); + const transformed = transformAutocompleteResults(text, results); + return transformed; + }; +} diff --git a/packages/browser-runtime-core/package.json b/packages/browser-runtime-core/package.json index 1cc7103e74..f8de7a2eda 100644 --- a/packages/browser-runtime-core/package.json +++ b/packages/browser-runtime-core/package.json @@ -41,6 +41,7 @@ "@mongodb-js/eslint-config-mongosh": "^1.0.0", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-mongosh": "^1.0.0", + "@mongodb-js/mongodb-ts-autocomplete": "^0.2.5", "@mongosh/types": "3.6.2", "bson": "^6.10.3", "depcheck": "^1.4.7", diff --git a/packages/browser-runtime-core/src/autocompleter/shell-api-autocompleter.spec.ts b/packages/browser-runtime-core/src/autocompleter/shell-api-autocompleter.spec.ts index 7a5ed05e6a..dee94dd162 100644 --- a/packages/browser-runtime-core/src/autocompleter/shell-api-autocompleter.spec.ts +++ b/packages/browser-runtime-core/src/autocompleter/shell-api-autocompleter.spec.ts @@ -1,8 +1,10 @@ import { ShellApiAutocompleter } from './shell-api-autocompleter'; import { expect } from 'chai'; import { Topologies } from '@mongosh/shell-api'; +import type { AutocompleteParameters } from '@mongosh/autocomplete'; +import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete'; -const standalone440 = { +const standalone440Parameters: AutocompleteParameters = { topology: () => Topologies.Standalone, apiVersionInfo: () => undefined, connectionInfo: () => ({ @@ -11,8 +13,23 @@ const standalone440 = { server_version: '4.4.0', is_local_atlas: false, }), - getCollectionCompletionsForCurrentDb: () => ['bananas'], - getDatabaseCompletions: () => ['databaseOne'], + getCollectionCompletionsForCurrentDb: () => Promise.resolve(['bananas']), + getDatabaseCompletions: () => Promise.resolve(['databaseOne']), +}; + +const standalone440Context: AutocompletionContext = { + currentDatabaseAndConnection: () => ({ + connectionId: 'connection-1', + databaseName: 'databaseOne', + }), + databasesForConnection: () => Promise.resolve(['databaseOne']), + collectionsForDatabase: () => Promise.resolve(['bananas', 'coll1']), + schemaInformationForCollection: () => Promise.resolve({}), +}; + +const shellInstanceState = { + getAutocompleteParameters: () => standalone440Parameters, + getAutocompletionContext: () => standalone440Context, }; describe('Autocompleter', function () { @@ -20,7 +37,7 @@ describe('Autocompleter', function () { let autocompleter: ShellApiAutocompleter; beforeEach(function () { - autocompleter = new ShellApiAutocompleter(standalone440); + autocompleter = new ShellApiAutocompleter(shellInstanceState); }); it('returns completions for text before cursor', async function () { diff --git a/packages/browser-runtime-core/src/autocompleter/shell-api-autocompleter.ts b/packages/browser-runtime-core/src/autocompleter/shell-api-autocompleter.ts index a5350fb397..35b2bb57ca 100644 --- a/packages/browser-runtime-core/src/autocompleter/shell-api-autocompleter.ts +++ b/packages/browser-runtime-core/src/autocompleter/shell-api-autocompleter.ts @@ -1,12 +1,29 @@ -import type { AutocompleteParameters } from '@mongosh/autocomplete'; -import cliReplCompleter from '@mongosh/autocomplete'; +import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete'; +import type { + AutocompleteParameters, + CompletionResults, +} from '@mongosh/autocomplete'; +import { completer, initNewAutocompleter } from '@mongosh/autocomplete'; import type { Autocompleter, Completion } from './autocompleter'; +type AutocompleteShellInstanceState = { + getAutocompleteParameters: () => AutocompleteParameters; + getAutocompletionContext: () => AutocompletionContext; +}; + export class ShellApiAutocompleter implements Autocompleter { - private parameters: AutocompleteParameters; + private shellInstanceState: AutocompleteShellInstanceState; + + // old autocomplete only: + private parameters: AutocompleteParameters | undefined; + + // new autocomplete only: + private newMongoshCompleter: + | ((line: string) => Promise) + | undefined; - constructor(parameters: AutocompleteParameters) { - this.parameters = parameters; + constructor(shellInstanceState: AutocompleteShellInstanceState) { + this.shellInstanceState = shellInstanceState; } async getCompletions(code: string): Promise { @@ -14,7 +31,22 @@ export class ShellApiAutocompleter implements Autocompleter { return []; } - const completions = await cliReplCompleter(this.parameters, code); + let completions: CompletionResults; + + if (process.env.USE_NEW_AUTOCOMPLETE) { + if (!this.newMongoshCompleter) { + this.newMongoshCompleter = await initNewAutocompleter( + this.shellInstanceState + ); + } + + completions = await this.newMongoshCompleter(code); + } else { + if (!this.parameters) { + this.parameters = this.shellInstanceState.getAutocompleteParameters(); + } + completions = await completer(this.parameters, code); + } if (!completions || !completions.length) { return []; diff --git a/packages/browser-runtime-core/src/open-context-runtime.ts b/packages/browser-runtime-core/src/open-context-runtime.ts index 65686b503e..a17a0a8477 100644 --- a/packages/browser-runtime-core/src/open-context-runtime.ts +++ b/packages/browser-runtime-core/src/open-context-runtime.ts @@ -29,7 +29,6 @@ export interface InterpreterEnvironment { */ export class OpenContextRuntime implements Runtime { private interpreterEnvironment: InterpreterEnvironment; - // TODO(MONGOSH-2205): we have to also port this to the new autocomplete private autocompleter: ShellApiAutocompleter | null = null; private shellEvaluator: ShellEvaluator; private instanceState: ShellInstanceState; @@ -53,9 +52,7 @@ export class OpenContextRuntime implements Runtime { async getCompletions(code: string): Promise { if (!this.autocompleter) { - this.autocompleter = new ShellApiAutocompleter( - this.instanceState.getAutocompleteParameters() - ); + this.autocompleter = new ShellApiAutocompleter(this.instanceState); this.updatedConnectionInfoPromise ??= this.instanceState.fetchConnectionInfo(); await this.updatedConnectionInfoPromise; diff --git a/packages/cli-repl/package.json b/packages/cli-repl/package.json index a906605f64..2fcf36b518 100644 --- a/packages/cli-repl/package.json +++ b/packages/cli-repl/package.json @@ -77,7 +77,6 @@ "@mongosh/shell-evaluator": "^3.11.0", "@mongosh/snippet-manager": "^3.11.0", "@mongosh/types": "3.6.2", - "@mongodb-js/mongodb-ts-autocomplete": "^0.2.4", "@segment/analytics-node": "^1.3.0", "ansi-escape-sequences": "^5.1.2", "askcharacter": "^2.0.4", diff --git a/packages/cli-repl/src/cli-repl.spec.ts b/packages/cli-repl/src/cli-repl.spec.ts index d8068df7c4..9b623ed4e2 100644 --- a/packages/cli-repl/src/cli-repl.spec.ts +++ b/packages/cli-repl/src/cli-repl.spec.ts @@ -2620,11 +2620,6 @@ describe('CliRepl', function () { }); it('completes shell commands', async function () { - if (process.env.USE_NEW_AUTOCOMPLETE) { - // TODO(MONGOSH-2035): not supported yet - this.skip(); - } - input.write('const dSomeVariableStartingWithD = 10;\n'); await waitEval(cliRepl.bus); @@ -2636,11 +2631,6 @@ describe('CliRepl', function () { }); it('completes use ', async function () { - if (process.env.USE_NEW_AUTOCOMPLETE) { - // TODO(MONGOSH-2035): not supported yet - this.skip(); - } - if (!hasDatabaseNames) return this.skip(); input.write('db.getMongo()._listDatabases()\n'); // populate database cache await waitEval(cliRepl.bus); diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index 2614527fc0..afe3619ad5 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -1,4 +1,5 @@ -import completer from '@mongosh/autocomplete'; +import type { CompletionResults } from '@mongosh/autocomplete'; +import { completer, initNewAutocompleter } from '@mongosh/autocomplete'; import { MongoshInternalError, MongoshWarning } from '@mongosh/errors'; import { changeHistory } from '@mongosh/history'; import type { @@ -49,8 +50,6 @@ import { Script, createContext, runInContext } from 'vm'; import { installPasteSupport } from './repl-paste-support'; import util from 'util'; -import type { MongoDBAutocompleter } from '@mongodb-js/mongodb-ts-autocomplete'; - declare const __non_webpack_require__: any; /** @@ -133,13 +132,6 @@ type Mutable = { -readonly [P in keyof T]: T[P]; }; -function transformAutocompleteResults( - line: string, - results: { result: string }[] -): [string[], string] { - return [results.map((result) => result.result), line]; -} - /** * An instance of a `mongosh` REPL, without any of the actual I/O. * Specifically, code called by this class should not do any @@ -440,18 +432,14 @@ class MongoshNodeRepl implements EvaluationListener { const origReplCompleter = promisify(repl.completer.bind(repl)); // repl.completer is callback-style - let newMongoshCompleter: MongoDBAutocompleter | undefined; - let oldMongoshCompleter: ( - line: string - ) => Promise<[string[], string, 'exclusive'] | [string[], string]>; + let newMongoshCompleter: (line: string) => Promise; + let oldMongoshCompleter: (line: string) => Promise; if (process.env.USE_NEW_AUTOCOMPLETE) { // we will lazily instantiate the new autocompleter on first use } else { - oldMongoshCompleter = completer.bind( - null, - instanceState.getAutocompleteParameters() - ); + const autocompleteParams = instanceState.getAutocompleteParameters(); + oldMongoshCompleter = completer.bind(null, autocompleteParams); } const innerCompleter = async ( @@ -469,22 +457,10 @@ class MongoshNodeRepl implements EvaluationListener { (async () => { if (process.env.USE_NEW_AUTOCOMPLETE) { if (!newMongoshCompleter) { - // only import the autocompleter code the first time we need it to - // hide the time it takes from the initial startup time - const { MongoDBAutocompleter } = await import( - '@mongodb-js/mongodb-ts-autocomplete' - ); - - const autocompletionContext = - instanceState.getAutocompletionContext(); - newMongoshCompleter = new MongoDBAutocompleter({ - context: autocompletionContext, - }); + newMongoshCompleter = await initNewAutocompleter(instanceState); } - const results = await newMongoshCompleter.autocomplete(text); - const transformed = transformAutocompleteResults(text, results); - return transformed; + return newMongoshCompleter(text); } else { return oldMongoshCompleter(text); } diff --git a/packages/e2e-tests/test/e2e-snapshot.spec.ts b/packages/e2e-tests/test/e2e-snapshot.spec.ts index 70005193ba..fb57cc798c 100644 --- a/packages/e2e-tests/test/e2e-snapshot.spec.ts +++ b/packages/e2e-tests/test/e2e-snapshot.spec.ts @@ -156,7 +156,7 @@ describe('e2e snapshot support', function () { ); verifyAllThatMatchAreInCategory( 'snapshot', - /^node_modules\/(@babel\/types|@babel\/traverse|@mongodb-js\/devtools-connect|mongodb)\/|^packages\/(?!(shell-api\/lib\/api-export\.js|cli-repl\/node_modules\/@mongodb-js\/mongodb-ts-autocomplete\/))/ + /^node_modules\/(@babel\/types|@babel\/traverse|@mongodb-js\/devtools-connect|mongodb)\/|^packages\/(?!(shell-api\/lib\/api-export\.js|autocomplete\/node_modules\/@mongodb-js\/mongodb-ts-autocomplete\/))/ ); }); }); diff --git a/packages/shell-api/package.json b/packages/shell-api/package.json index 170acc1d2f..8e340d94ff 100644 --- a/packages/shell-api/package.json +++ b/packages/shell-api/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@microsoft/api-extractor": "^7.39.3", "@mongodb-js/eslint-config-mongosh": "^1.0.0", - "@mongodb-js/mongodb-ts-autocomplete": "^0.2.2", + "@mongodb-js/mongodb-ts-autocomplete": "^0.2.5", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-mongosh": "^1.0.0", "@mongosh/types": "3.6.2", diff --git a/packages/shell-api/src/aggregation-cursor.spec.ts b/packages/shell-api/src/aggregation-cursor.spec.ts index cf470b8694..1494364e19 100644 --- a/packages/shell-api/src/aggregation-cursor.spec.ts +++ b/packages/shell-api/src/aggregation-cursor.spec.ts @@ -45,6 +45,7 @@ describe('AggregationCursor', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/bulk.spec.ts b/packages/shell-api/src/bulk.spec.ts index 16eb749ccf..1fb3400c85 100644 --- a/packages/shell-api/src/bulk.spec.ts +++ b/packages/shell-api/src/bulk.spec.ts @@ -52,6 +52,7 @@ describe('Bulk API', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); @@ -272,6 +273,7 @@ describe('Bulk API', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/change-stream-cursor.spec.ts b/packages/shell-api/src/change-stream-cursor.spec.ts index 3a92b4227b..24649ae1b9 100644 --- a/packages/shell-api/src/change-stream-cursor.spec.ts +++ b/packages/shell-api/src/change-stream-cursor.spec.ts @@ -52,6 +52,7 @@ describe('ChangeStreamCursor', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/collection.spec.ts b/packages/shell-api/src/collection.spec.ts index 9e7e17ee05..0e3ed16069 100644 --- a/packages/shell-api/src/collection.spec.ts +++ b/packages/shell-api/src/collection.spec.ts @@ -63,6 +63,7 @@ describe('Collection', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/cursor.spec.ts b/packages/shell-api/src/cursor.spec.ts index 72c639d5be..d21f45abb6 100644 --- a/packages/shell-api/src/cursor.spec.ts +++ b/packages/shell-api/src/cursor.spec.ts @@ -64,6 +64,7 @@ describe('Cursor', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/database.spec.ts b/packages/shell-api/src/database.spec.ts index 366a858aeb..d863c1d9d8 100644 --- a/packages/shell-api/src/database.spec.ts +++ b/packages/shell-api/src/database.spec.ts @@ -111,6 +111,7 @@ describe('Database', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/decorators.ts b/packages/shell-api/src/decorators.ts index 9c6ca8b380..6c7d49a5ba 100644 --- a/packages/shell-api/src/decorators.ts +++ b/packages/shell-api/src/decorators.ts @@ -1,5 +1,6 @@ import { MongoshInternalError } from '@mongosh/errors'; import type { ReplPlatform } from '@mongosh/service-provider-core'; +import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete'; import type { Mongo, ShellInstanceState } from '.'; import type { Topologies } from './enums'; import { @@ -350,12 +351,14 @@ function getShellInstanceState(apiObject: any): ShellInstanceState | undefined { * as needed. */ export interface ShellCommandAutocompleteParameters { - getCollectionCompletionsForCurrentDb: ( - collName: string - ) => string[] | Promise; getDatabaseCompletions: (dbName: string) => string[] | Promise; } +export type NewShellCommandAutocompleteParameters = Pick< + AutocompletionContext, + 'currentDatabaseAndConnection' | 'databasesForConnection' +>; + /** * Provide a suggested list of completions for the last item in a shell command, * e.g. `show pro` to `show profile` by returning ['profile']. @@ -365,6 +368,11 @@ export type ShellCommandCompleter = ( args: string[] ) => Promise; +export type NewShellCommandCompleter = ( + context: NewShellCommandAutocompleteParameters, + args: string[] +) => Promise; + /** * Information about a class or a method that is used for * e.g. autocompletion and i18n support. @@ -381,6 +389,7 @@ export interface TypeSignature { isDirectShellCommand?: boolean; acceptsRawInput?: boolean; shellCommandCompleter?: ShellCommandCompleter; + newShellCommandCompleter?: NewShellCommandCompleter; inherited?: boolean; } @@ -427,6 +436,7 @@ type ClassSignature = { isDirectShellCommand: boolean; acceptsRawInput?: boolean; shellCommandCompleter?: ShellCommandCompleter; + newShellCommandCompleter?: NewShellCommandCompleter; inherited?: true; }; }; @@ -511,6 +521,8 @@ function shellApiClassGeneric( method.isDirectShellCommand = method.isDirectShellCommand || false; method.acceptsRawInput = method.acceptsRawInput || false; method.shellCommandCompleter = method.shellCommandCompleter || undefined; + method.newShellCommandCompleter = + method.newShellCommandCompleter || undefined; classSignature.attributes[propertyName] = { type: 'function', @@ -524,6 +536,7 @@ function shellApiClassGeneric( isDirectShellCommand: method.isDirectShellCommand, acceptsRawInput: method.acceptsRawInput, shellCommandCompleter: method.shellCommandCompleter, + newShellCommandCompleter: method.newShellCommandCompleter, }; const attributeHelpKeyPrefix = `${classHelpKeyPrefix}.attributes.${propertyName}`; @@ -584,6 +597,7 @@ function shellApiClassGeneric( isDirectShellCommand: method.isDirectShellCommand, acceptsRawInput: method.acceptsRawInput, shellCommandCompleter: method.shellCommandCompleter, + newShellCommandCompleter: method.newShellCommandCompleter, inherited: true, }; @@ -797,14 +811,21 @@ export function directShellCommand( * @param completer The completer to use for autocomplete */ export function shellCommandCompleter( - shellCommandCompleter: ShellCommandCompleter + shellCommandCompleter: ShellCommandCompleter, + newShellCommandCompleter: NewShellCommandCompleter ) { return function ( value: T, // eslint-disable-next-line @typescript-eslint/no-unused-vars context: ClassMethodDecoratorContext - ): T & { shellCommandCompleter: ShellCommandCompleter } { - return Object.assign(value, { shellCommandCompleter }); + ): T & { + shellCommandCompleter: ShellCommandCompleter; + newShellCommandCompleter: NewShellCommandCompleter; + } { + return Object.assign(value, { + shellCommandCompleter, + newShellCommandCompleter, + }); }; } diff --git a/packages/shell-api/src/explainable-cursor.spec.ts b/packages/shell-api/src/explainable-cursor.spec.ts index 55ee41c3f2..da3b6d6826 100644 --- a/packages/shell-api/src/explainable-cursor.spec.ts +++ b/packages/shell-api/src/explainable-cursor.spec.ts @@ -40,6 +40,7 @@ describe('ExplainableCursor', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/explainable.spec.ts b/packages/shell-api/src/explainable.spec.ts index 4993e0454f..f158c4222a 100644 --- a/packages/shell-api/src/explainable.spec.ts +++ b/packages/shell-api/src/explainable.spec.ts @@ -44,6 +44,7 @@ describe('Explainable', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/field-level-encryption.spec.ts b/packages/shell-api/src/field-level-encryption.spec.ts index 2bf80279e6..9781b56bf3 100644 --- a/packages/shell-api/src/field-level-encryption.spec.ts +++ b/packages/shell-api/src/field-level-encryption.spec.ts @@ -166,6 +166,7 @@ describe('Field Level Encryption', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); expect(signatures.ClientEncryption.attributes?.encrypt).to.deep.equal({ type: 'function', @@ -179,6 +180,7 @@ describe('Field Level Encryption', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/integration.spec.ts b/packages/shell-api/src/integration.spec.ts index 98e3d7d9e8..8dbf1a6067 100644 --- a/packages/shell-api/src/integration.spec.ts +++ b/packages/shell-api/src/integration.spec.ts @@ -2782,8 +2782,11 @@ describe('Shell API (integration)', function () { it('returns information for autocomplete', async function () { const context = instanceState.getAutocompletionContext(); - const { connectionId, databaseName } = - context.currentDatabaseAndConnection(); + const dbAndConnection = context.currentDatabaseAndConnection(); + if (!dbAndConnection) { + throw new Error('No current database and connection found'); + } + const { connectionId, databaseName } = dbAndConnection; const databaseNames = await context.databasesForConnection( connectionId ); diff --git a/packages/shell-api/src/mongo.spec.ts b/packages/shell-api/src/mongo.spec.ts index f8422220a0..d85ae6d490 100644 --- a/packages/shell-api/src/mongo.spec.ts +++ b/packages/shell-api/src/mongo.spec.ts @@ -69,6 +69,7 @@ describe('Mongo', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/plan-cache.spec.ts b/packages/shell-api/src/plan-cache.spec.ts index 8efb5403b5..053e43f7e9 100644 --- a/packages/shell-api/src/plan-cache.spec.ts +++ b/packages/shell-api/src/plan-cache.spec.ts @@ -32,6 +32,7 @@ describe('PlanCache', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/replica-set.spec.ts b/packages/shell-api/src/replica-set.spec.ts index dbfdeb82cb..35d094704b 100644 --- a/packages/shell-api/src/replica-set.spec.ts +++ b/packages/shell-api/src/replica-set.spec.ts @@ -83,6 +83,7 @@ describe('ReplicaSet', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/session.spec.ts b/packages/shell-api/src/session.spec.ts index f44562d195..0d33721b62 100644 --- a/packages/shell-api/src/session.spec.ts +++ b/packages/shell-api/src/session.spec.ts @@ -55,6 +55,7 @@ describe('Session', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/shard.spec.ts b/packages/shell-api/src/shard.spec.ts index 339e7544b9..9c4d832a74 100644 --- a/packages/shell-api/src/shard.spec.ts +++ b/packages/shell-api/src/shard.spec.ts @@ -70,6 +70,7 @@ describe('Shard', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/shell-api.spec.ts b/packages/shell-api/src/shell-api.spec.ts index 94387193cc..a6ac13caeb 100644 --- a/packages/shell-api/src/shell-api.spec.ts +++ b/packages/shell-api/src/shell-api.spec.ts @@ -60,6 +60,8 @@ describe('ShellApi', function () { acceptsRawInput: false, shellCommandCompleter: signatures.ShellApi.attributes?.use.shellCommandCompleter, + newShellCommandCompleter: + signatures.ShellApi.attributes?.use.newShellCommandCompleter, }); expect(signatures.ShellApi.attributes?.show).to.deep.equal({ type: 'function', @@ -74,6 +76,8 @@ describe('ShellApi', function () { acceptsRawInput: false, shellCommandCompleter: signatures.ShellApi.attributes?.show.shellCommandCompleter, + newShellCommandCompleter: + signatures.ShellApi.attributes?.show.newShellCommandCompleter, }); expect(signatures.ShellApi.attributes?.exit).to.deep.equal({ type: 'function', @@ -87,6 +91,7 @@ describe('ShellApi', function () { isDirectShellCommand: true, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); expect(signatures.ShellApi.attributes?.it).to.deep.equal({ type: 'function', @@ -100,6 +105,7 @@ describe('ShellApi', function () { isDirectShellCommand: true, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); expect(signatures.ShellApi.attributes?.print).to.deep.equal({ type: 'function', @@ -113,6 +119,7 @@ describe('ShellApi', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); expect(signatures.ShellApi.attributes?.printjson).to.deep.equal({ type: 'function', @@ -126,6 +133,7 @@ describe('ShellApi', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); expect(signatures.ShellApi.attributes?.sleep).to.deep.equal({ type: 'function', @@ -139,6 +147,7 @@ describe('ShellApi', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); expect(signatures.ShellApi.attributes?.cls).to.deep.equal({ type: 'function', @@ -152,6 +161,7 @@ describe('ShellApi', function () { isDirectShellCommand: true, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); expect(signatures.ShellApi.attributes?.Mongo).to.deep.equal({ type: 'function', @@ -165,6 +175,7 @@ describe('ShellApi', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); expect(signatures.ShellApi.attributes?.connect).to.deep.equal({ type: 'function', @@ -178,6 +189,7 @@ describe('ShellApi', function () { isDirectShellCommand: false, acceptsRawInput: false, shellCommandCompleter: undefined, + newShellCommandCompleter: undefined, }); }); }); diff --git a/packages/shell-api/src/shell-api.ts b/packages/shell-api/src/shell-api.ts index ffbadcf52b..5259fffdd4 100644 --- a/packages/shell-api/src/shell-api.ts +++ b/packages/shell-api/src/shell-api.ts @@ -1,6 +1,7 @@ import type { ShellResult, ShellCommandAutocompleteParameters, + NewShellCommandAutocompleteParameters, } from './decorators'; import { shellApiClassDefault, @@ -135,16 +136,33 @@ async function useCompleter( return await params.getDatabaseCompletions(args[1] ?? ''); } -/** - * Complete a `show` subcommand. - */ -// eslint-disable-next-line @typescript-eslint/require-await -async function showCompleter( - params: ShellCommandAutocompleteParameters, +// eslint-disable-next-line no-control-regex +const CONTROL_CHAR_REGEXP = /[\x00-\x1F\x7F-\x9F]/g; + +async function newUseCompleter( + context: NewShellCommandAutocompleteParameters, args: string[] ): Promise { if (args.length > 2) return undefined; - if (args[1] === 'd') { + + const dbAndConnection = context.currentDatabaseAndConnection(); + if (!dbAndConnection) { + return []; + } + const { connectionId } = dbAndConnection; + + const dbNames = await context.databasesForConnection(connectionId); + + const prefix = (args[1] ?? '').toLowerCase(); + + return dbNames.filter( + (name) => + name.toLowerCase().startsWith(prefix) && !CONTROL_CHAR_REGEXP.test(name) + ); +} + +function completeShowCommand(prefix: string): string[] { + if (prefix === 'd') { // Special-case: The user might want `show dbs` or `show databases`, but they won't care about which they get. return ['databases']; } @@ -162,7 +180,28 @@ async function showCompleter( 'automationNotices', 'nonGenuineMongoDBCheck', ]; - return candidates.filter((str) => str.startsWith(args[1] ?? '')); + return candidates.filter((str) => str.startsWith(prefix)); +} + +/** + * Complete a `show` subcommand. + */ +// eslint-disable-next-line @typescript-eslint/require-await +async function showCompleter( + params: ShellCommandAutocompleteParameters, + args: string[] +): Promise { + if (args.length > 2) return undefined; + return completeShowCommand(args[1] ?? ''); +} + +// eslint-disable-next-line @typescript-eslint/require-await +async function newShowCompleter( + context: NewShellCommandAutocompleteParameters, + args: string[] +): Promise { + if (args.length > 2) return undefined; + return completeShowCommand(args[1] ?? ''); } /** @@ -206,14 +245,14 @@ export default class ShellApi extends ShellApiClass { } @directShellCommand - @shellCommandCompleter(useCompleter) + @shellCommandCompleter(useCompleter, newUseCompleter) use(db: string): any { return this._instanceState.currentDb._mongo.use(db); } @directShellCommand @returnsPromise - @shellCommandCompleter(showCompleter) + @shellCommandCompleter(showCompleter, newShowCompleter) async show(cmd: string, arg?: string): Promise { return await this._instanceState.currentDb._mongo.show(cmd, arg); } diff --git a/packages/shell-api/src/shell-instance-state.ts b/packages/shell-api/src/shell-instance-state.ts index 9f3364539b..d4b736b5b4 100644 --- a/packages/shell-api/src/shell-instance-state.ts +++ b/packages/shell-api/src/shell-instance-state.ts @@ -417,14 +417,23 @@ export class ShellInstanceState { public getAutocompletionContext(): AutocompletionContext { return { - currentDatabaseAndConnection: (): { - connectionId: string; - databaseName: string; - } => { - return { - connectionId: this.currentDb.getMongo()._getConnectionId(), - databaseName: this.currentDb.getName(), - }; + currentDatabaseAndConnection: (): + | { + connectionId: string; + databaseName: string; + } + | undefined => { + try { + return { + connectionId: this.currentDb.getMongo()._getConnectionId(), + databaseName: this.currentDb.getName(), + }; + } catch (err: any) { + if (err.name === 'MongoshInvalidInputError') { + return undefined; + } + throw err; + } }, databasesForConnection: async ( connectionId: string diff --git a/packages/snippet-manager/src/snippet-manager.ts b/packages/snippet-manager/src/snippet-manager.ts index 83a2d8a286..2d606fae81 100644 --- a/packages/snippet-manager/src/snippet-manager.ts +++ b/packages/snippet-manager/src/snippet-manager.ts @@ -83,6 +83,36 @@ async function packBSON(data: any): Promise { return await brotliCompress(bson.serialize(data)); } +function completeSnippetsCommand( + args: string[], + snippets: SnippetDescription[] +) { + const plainCommands = [ + 'update', + 'search', + 'ls', + 'outdated', + 'info', + 'refresh', + 'load-all', + ]; + const pkgCommands = ['install', 'uninstall', 'help']; + if (args.length >= 2 && pkgCommands.includes(args[1])) { + const allSnippetNames = snippets.map(({ snippetName }) => snippetName); + if (args.length === 2) { + return allSnippetNames.map((str) => `${args[1]} ${str}`); + } + return allSnippetNames.filter((str) => + str.startsWith(args[args.length - 1] ?? '') + ); + } else if (args.length === 2) { + return [...plainCommands, ...pkgCommands].filter((str) => + str.startsWith(args[1] ?? '') + ); + } + return undefined; +} + export class SnippetManager implements ShellPlugin { _instanceState: ShellInstanceState; installdir: string; @@ -143,32 +173,14 @@ export class SnippetManager implements ShellPlugin { args: string[] // eslint-disable-next-line @typescript-eslint/require-await ): Promise => { - const plainCommands = [ - 'update', - 'search', - 'ls', - 'outdated', - 'info', - 'refresh', - 'load-all', - ]; - const pkgCommands = ['install', 'uninstall', 'help']; - if (args.length >= 2 && pkgCommands.includes(args[1])) { - const allSnippetNames = this.snippets.map( - ({ snippetName }) => snippetName - ); - if (args.length === 2) { - return allSnippetNames.map((str) => `${args[1]} ${str}`); - } - return allSnippetNames.filter((str) => - str.startsWith(args[args.length - 1] ?? '') - ); - } else if (args.length === 2) { - return [...plainCommands, ...pkgCommands].filter((str) => - str.startsWith(args[1] ?? '') - ); - } - return undefined; + return completeSnippetsCommand(args, this.snippets); + }, + newShellCommandCompleter: async ( + context: unknown, + args: string[] + // eslint-disable-next-line @typescript-eslint/require-await + ): Promise => { + return completeSnippetsCommand(args, this.snippets); }, } as TypeSignature; instanceState.registerPlugin(this);