Skip to content

Commit d1fe3f5

Browse files
committed
port autocomplete direct commands and browser-runtime-core
1 parent c8941c1 commit d1fe3f5

File tree

10 files changed

+299
-100
lines changed

10 files changed

+299
-100
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete';
2+
import type { TypeSignature } from '@mongosh/shell-api';
3+
import { signatures as shellSignatures } from '@mongosh/shell-api';
4+
5+
type TypeSignatureAttributes = { [key: string]: TypeSignature };
6+
7+
export async function directCommandCompleter(
8+
context: AutocompletionContext,
9+
line: string
10+
): Promise<string[]> {
11+
const SHELL_COMPLETIONS = shellSignatures.ShellApi
12+
.attributes as TypeSignatureAttributes;
13+
14+
// Split at space-to-non-space transitions when looking at this as a command,
15+
// because multiple spaces (e.g. 'show collections') are valid in commands.
16+
// This split keeps the spaces intact so we can join them back later.
17+
const splitLineWhitespace = line.split(/(?<!\S)(?=\S)/);
18+
const command = splitLineWhitespace[0].trim();
19+
if (!SHELL_COMPLETIONS[command]?.isDirectShellCommand) {
20+
return [];
21+
}
22+
23+
// If the shell API provides us with a completer, use it.
24+
// examples: use, show, snippet
25+
const completer = SHELL_COMPLETIONS[command].newShellCommandCompleter;
26+
if (!completer) {
27+
// examples without a custom completer: exit, quit, it, cls
28+
return [line];
29+
}
30+
31+
if (splitLineWhitespace.length === 1) {
32+
if (splitLineWhitespace[0].trimEnd() === splitLineWhitespace[0]) {
33+
// Treat e.g. 'show' like 'show '.
34+
splitLineWhitespace[0] += ' ';
35+
}
36+
37+
// Complete the first argument after the command.
38+
splitLineWhitespace.push('');
39+
}
40+
const hits =
41+
(await completer(
42+
context,
43+
splitLineWhitespace.map((item) => item.trim())
44+
)) || [];
45+
// Adjust to full input, because `completer` only completed the last item
46+
// in the line, e.g. ['profile'] -> ['show profile']
47+
const fullLineHits = hits.map((hit) =>
48+
[...splitLineWhitespace.slice(0, -1), hit].join('')
49+
);
50+
51+
return fullLineHits;
52+
}

packages/autocomplete/src/index.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Topologies, TypeSignature } from '@mongosh/shell-api';
22
import { signatures as shellSignatures } from '@mongosh/shell-api';
3+
import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete';
34
import semver from 'semver';
45
import {
56
CONVERSION_OPERATORS,
@@ -13,6 +14,7 @@ import {
1314
ON_PREM,
1415
DATABASE,
1516
} from '@mongodb-js/mongodb-constants';
17+
import { directCommandCompleter } from './direct-command-completer';
1618

1719
type TypeSignatureAttributes = { [key: string]: TypeSignature };
1820

@@ -78,7 +80,7 @@ const GROUP = '$group';
7880
*
7981
* @returns {array} Matching Completions, Current User Input.
8082
*/
81-
async function completer(
83+
export async function completer(
8284
params: AutocompleteParameters,
8385
line: string
8486
): Promise<[string[], string, 'exclusive'] | [string[], string]> {
@@ -398,4 +400,51 @@ function filterShellAPI(
398400
return hits;
399401
}
400402

401-
export default completer;
403+
type AutocompleteShellInstanceState = {
404+
getAutocompleteParameters: () => AutocompleteParameters;
405+
getAutocompletionContext: () => AutocompletionContext;
406+
};
407+
408+
function transformAutocompleteResults(
409+
line: string,
410+
results: { result: string }[]
411+
): [string[], string] {
412+
return [results.map((result) => result.result), line];
413+
}
414+
415+
export type CompletionResults =
416+
| [string[], string, 'exclusive']
417+
| [string[], string];
418+
419+
export async function initNewAutocompleter(
420+
instanceState: Pick<
421+
AutocompleteShellInstanceState,
422+
'getAutocompletionContext'
423+
>
424+
): Promise<(text: string) => Promise<CompletionResults>> {
425+
// only import the autocompleter code the first time we need it to
426+
// hide the time it takes from the initial startup time
427+
const { MongoDBAutocompleter } = await import(
428+
'@mongodb-js/mongodb-ts-autocomplete'
429+
);
430+
431+
const autocompletionContext = instanceState.getAutocompletionContext();
432+
const mongoDBCompleter = new MongoDBAutocompleter({
433+
context: autocompletionContext,
434+
});
435+
436+
return async (text: string): Promise<CompletionResults> => {
437+
const directResults = await directCommandCompleter(
438+
autocompletionContext,
439+
text
440+
);
441+
442+
if (directResults.length) {
443+
return [directResults, text, 'exclusive'];
444+
}
445+
446+
const results = await mongoDBCompleter.autocomplete(text);
447+
const transformed = transformAutocompleteResults(text, results);
448+
return transformed;
449+
};
450+
}

packages/browser-runtime-core/src/autocompleter/shell-api-autocompleter.spec.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { ShellApiAutocompleter } from './shell-api-autocompleter';
22
import { expect } from 'chai';
33
import { Topologies } from '@mongosh/shell-api';
4+
import type { AutocompleteParameters } from '@mongosh/autocomplete';
5+
import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete';
46

5-
const standalone440 = {
7+
const standalone440Parameters: AutocompleteParameters = {
68
topology: () => Topologies.Standalone,
79
apiVersionInfo: () => undefined,
810
connectionInfo: () => ({
@@ -11,16 +13,40 @@ const standalone440 = {
1113
server_version: '4.4.0',
1214
is_local_atlas: false,
1315
}),
14-
getCollectionCompletionsForCurrentDb: () => ['bananas'],
15-
getDatabaseCompletions: () => ['databaseOne'],
16+
getCollectionCompletionsForCurrentDb: () => Promise.resolve(['bananas']),
17+
getDatabaseCompletions: () => Promise.resolve(['databaseOne']),
18+
};
19+
20+
const standalone440Context: AutocompletionContext = {
21+
currentDatabaseAndConnection: () => ({
22+
connectionId: 'connection-1',
23+
databaseName: 'databaseOne',
24+
}),
25+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
26+
databasesForConnection: (connectionId: string) =>
27+
Promise.resolve(['databaseOne']),
28+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
29+
collectionsForDatabase: (connectionId: string, dtabaseName: string) =>
30+
Promise.resolve(['bananas', 'coll1']),
31+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
32+
schemaInformationForCollection: (
33+
connectionId: string,
34+
databaseName: string,
35+
collectionName: string
36+
) => Promise.resolve({}),
37+
};
38+
39+
const shellInstanceState = {
40+
getAutocompleteParameters: () => standalone440Parameters,
41+
getAutocompletionContext: () => standalone440Context,
1642
};
1743

1844
describe('Autocompleter', function () {
1945
describe('getCompletions', function () {
2046
let autocompleter: ShellApiAutocompleter;
2147

2248
beforeEach(function () {
23-
autocompleter = new ShellApiAutocompleter(standalone440);
49+
autocompleter = new ShellApiAutocompleter(shellInstanceState);
2450
});
2551

2652
it('returns completions for text before cursor', async function () {

packages/browser-runtime-core/src/autocompleter/shell-api-autocompleter.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,52 @@
1-
import type { AutocompleteParameters } from '@mongosh/autocomplete';
2-
import cliReplCompleter from '@mongosh/autocomplete';
1+
import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete';
2+
import type {
3+
AutocompleteParameters,
4+
CompletionResults,
5+
} from '@mongosh/autocomplete';
6+
import { completer, initNewAutocompleter } from '@mongosh/autocomplete';
37
import type { Autocompleter, Completion } from './autocompleter';
48

9+
type AutocompleteShellInstanceState = {
10+
getAutocompleteParameters: () => AutocompleteParameters;
11+
getAutocompletionContext: () => AutocompletionContext;
12+
};
13+
514
export class ShellApiAutocompleter implements Autocompleter {
6-
private parameters: AutocompleteParameters;
15+
private shellInstanceState: AutocompleteShellInstanceState;
16+
17+
// old autocomplete only:
18+
private parameters: AutocompleteParameters | undefined;
19+
20+
// new autocomplete only:
21+
private newMongoshCompleter:
22+
| ((line: string) => Promise<CompletionResults>)
23+
| undefined;
724

8-
constructor(parameters: AutocompleteParameters) {
9-
this.parameters = parameters;
25+
constructor(shellInstanceState: AutocompleteShellInstanceState) {
26+
this.shellInstanceState = shellInstanceState;
1027
}
1128

1229
async getCompletions(code: string): Promise<Completion[]> {
1330
if (!code) {
1431
return [];
1532
}
1633

17-
const completions = await cliReplCompleter(this.parameters, code);
34+
let completions: CompletionResults;
35+
36+
if (process.env.USE_NEW_AUTOCOMPLETE) {
37+
if (!this.newMongoshCompleter) {
38+
this.newMongoshCompleter = await initNewAutocompleter(
39+
this.shellInstanceState
40+
);
41+
}
42+
43+
completions = await this.newMongoshCompleter(code);
44+
} else {
45+
if (!this.parameters) {
46+
this.parameters = this.shellInstanceState.getAutocompleteParameters();
47+
}
48+
completions = await completer(this.parameters, code);
49+
}
1850

1951
if (!completions || !completions.length) {
2052
return [];

packages/browser-runtime-core/src/open-context-runtime.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export interface InterpreterEnvironment {
2929
*/
3030
export class OpenContextRuntime implements Runtime {
3131
private interpreterEnvironment: InterpreterEnvironment;
32-
// TODO(MONGOSH-2205): we have to also port this to the new autocomplete
3332
private autocompleter: ShellApiAutocompleter | null = null;
3433
private shellEvaluator: ShellEvaluator;
3534
private instanceState: ShellInstanceState;
@@ -53,9 +52,7 @@ export class OpenContextRuntime implements Runtime {
5352

5453
async getCompletions(code: string): Promise<Completion[]> {
5554
if (!this.autocompleter) {
56-
this.autocompleter = new ShellApiAutocompleter(
57-
this.instanceState.getAutocompleteParameters()
58-
);
55+
this.autocompleter = new ShellApiAutocompleter(this.instanceState);
5956
this.updatedConnectionInfoPromise ??=
6057
this.instanceState.fetchConnectionInfo();
6158
await this.updatedConnectionInfoPromise;

packages/cli-repl/src/cli-repl.spec.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2620,11 +2620,6 @@ describe('CliRepl', function () {
26202620
});
26212621

26222622
it('completes shell commands', async function () {
2623-
if (process.env.USE_NEW_AUTOCOMPLETE) {
2624-
// TODO(MONGOSH-2035): not supported yet
2625-
this.skip();
2626-
}
2627-
26282623
input.write('const dSomeVariableStartingWithD = 10;\n');
26292624
await waitEval(cliRepl.bus);
26302625

@@ -2636,11 +2631,6 @@ describe('CliRepl', function () {
26362631
});
26372632

26382633
it('completes use <db>', async function () {
2639-
if (process.env.USE_NEW_AUTOCOMPLETE) {
2640-
// TODO(MONGOSH-2035): not supported yet
2641-
this.skip();
2642-
}
2643-
26442634
if (!hasDatabaseNames) return this.skip();
26452635
input.write('db.getMongo()._listDatabases()\n'); // populate database cache
26462636
await waitEval(cliRepl.bus);

packages/cli-repl/src/mongosh-repl.ts

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import completer from '@mongosh/autocomplete';
1+
import type { CompletionResults } from '@mongosh/autocomplete';
2+
import { completer, initNewAutocompleter } from '@mongosh/autocomplete';
23
import { MongoshInternalError, MongoshWarning } from '@mongosh/errors';
34
import { changeHistory } from '@mongosh/history';
45
import type {
@@ -49,8 +50,6 @@ import { Script, createContext, runInContext } from 'vm';
4950
import { installPasteSupport } from './repl-paste-support';
5051
import util from 'util';
5152

52-
import type { MongoDBAutocompleter } from '@mongodb-js/mongodb-ts-autocomplete';
53-
5453
declare const __non_webpack_require__: any;
5554

5655
/**
@@ -133,13 +132,6 @@ type Mutable<T> = {
133132
-readonly [P in keyof T]: T[P];
134133
};
135134

136-
function transformAutocompleteResults(
137-
line: string,
138-
results: { result: string }[]
139-
): [string[], string] {
140-
return [results.map((result) => result.result), line];
141-
}
142-
143135
/**
144136
* An instance of a `mongosh` REPL, without any of the actual I/O.
145137
* Specifically, code called by this class should not do any
@@ -440,18 +432,14 @@ class MongoshNodeRepl implements EvaluationListener {
440432

441433
const origReplCompleter = promisify(repl.completer.bind(repl)); // repl.completer is callback-style
442434

443-
let newMongoshCompleter: MongoDBAutocompleter | undefined;
444-
let oldMongoshCompleter: (
445-
line: string
446-
) => Promise<[string[], string, 'exclusive'] | [string[], string]>;
435+
let newMongoshCompleter: (line: string) => Promise<CompletionResults>;
436+
let oldMongoshCompleter: (line: string) => Promise<CompletionResults>;
447437

448438
if (process.env.USE_NEW_AUTOCOMPLETE) {
449439
// we will lazily instantiate the new autocompleter on first use
450440
} else {
451-
oldMongoshCompleter = completer.bind(
452-
null,
453-
instanceState.getAutocompleteParameters()
454-
);
441+
const autocompleteParams = instanceState.getAutocompleteParameters();
442+
oldMongoshCompleter = completer.bind(null, autocompleteParams);
455443
}
456444

457445
const innerCompleter = async (
@@ -469,22 +457,10 @@ class MongoshNodeRepl implements EvaluationListener {
469457
(async () => {
470458
if (process.env.USE_NEW_AUTOCOMPLETE) {
471459
if (!newMongoshCompleter) {
472-
// only import the autocompleter code the first time we need it to
473-
// hide the time it takes from the initial startup time
474-
const { MongoDBAutocompleter } = await import(
475-
'@mongodb-js/mongodb-ts-autocomplete'
476-
);
477-
478-
const autocompletionContext =
479-
instanceState.getAutocompletionContext();
480-
newMongoshCompleter = new MongoDBAutocompleter({
481-
context: autocompletionContext,
482-
});
460+
newMongoshCompleter = await initNewAutocompleter(instanceState);
483461
}
484462

485-
const results = await newMongoshCompleter.autocomplete(text);
486-
const transformed = transformAutocompleteResults(text, results);
487-
return transformed;
463+
return newMongoshCompleter(text);
488464
} else {
489465
return oldMongoshCompleter(text);
490466
}

0 commit comments

Comments
 (0)