Skip to content

Commit 1bd47fe

Browse files
authored
feat(autocomplete): add show/use completions MONGOSH-563 (#829)
Add support for autocompletion for shell commands, in particular, `show` and `use`. Autocompletion for `use` works similar to autocompletion for `db.<collection>`. The implementation details for use/show autocompletion here have been left to the shell-api package, so that changes to them do not require any autocompletion changes.
1 parent 831220a commit 1bd47fe

25 files changed

+341
-58
lines changed

packages/autocomplete/index.spec.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import { signatures as shellSignatures, Topologies } from '@mongosh/shell-api';
44
import { expect } from 'chai';
55

66
let collections: string[];
7+
let databases: string[];
78
const standalone440 = {
89
topology: () => Topologies.Standalone,
910
connectionInfo: () => ({
1011
is_atlas: false,
1112
is_data_lake: false,
1213
server_version: '4.4.0'
1314
}),
14-
getCollectionCompletionsForCurrentDb: () => collections
15+
getCollectionCompletionsForCurrentDb: () => collections,
16+
getDatabaseCompletions: () => databases
1517
};
1618
const sharded440 = {
1719
topology: () => Topologies.Sharded,
@@ -20,7 +22,8 @@ const sharded440 = {
2022
is_data_lake: false,
2123
server_version: '4.4.0'
2224
}),
23-
getCollectionCompletionsForCurrentDb: () => collections
25+
getCollectionCompletionsForCurrentDb: () => collections,
26+
getDatabaseCompletions: () => databases
2427
};
2528

2629
const standalone300 = {
@@ -30,7 +33,8 @@ const standalone300 = {
3033
is_data_lake: false,
3134
server_version: '3.0.0'
3235
}),
33-
getCollectionCompletionsForCurrentDb: () => collections
36+
getCollectionCompletionsForCurrentDb: () => collections,
37+
getDatabaseCompletions: () => databases
3438
};
3539
const datalake440 = {
3640
topology: () => Topologies.Sharded,
@@ -39,13 +43,15 @@ const datalake440 = {
3943
is_data_lake: true,
4044
server_version: '4.4.0'
4145
}),
42-
getCollectionCompletionsForCurrentDb: () => collections
46+
getCollectionCompletionsForCurrentDb: () => collections,
47+
getDatabaseCompletions: () => databases
4348
};
4449

4550
const noParams = {
4651
topology: () => Topologies.Standalone,
4752
connectionInfo: () => undefined,
48-
getCollectionCompletionsForCurrentDb: () => collections
53+
getCollectionCompletionsForCurrentDb: () => collections,
54+
getDatabaseCompletions: () => databases
4955
};
5056

5157
describe('completer.completer', () => {
@@ -66,7 +72,7 @@ describe('completer.completer', () => {
6672

6773
it('is an exact match to one of shell completions', async() => {
6874
const i = 'use';
69-
expect(await completer(standalone440, i)).to.deep.equal([[i], i]);
75+
expect(await completer(standalone440, i)).to.deep.equal([[], i, 'exclusive']);
7076
});
7177
});
7278

@@ -482,4 +488,50 @@ describe('completer.completer', () => {
482488
expect(await completer(standalone440, i)).to.deep.equal([[], i]);
483489
});
484490
});
491+
492+
context('for shell commands', () => {
493+
it('completes partial commands', async() => {
494+
const i = 'sho';
495+
expect(await completer(noParams, i))
496+
.to.deep.equal([['show'], i]);
497+
});
498+
499+
it('completes partial commands', async() => {
500+
const i = 'show';
501+
const result = await completer(noParams, i);
502+
expect(result[0]).to.contain('show databases');
503+
});
504+
505+
it('completes show databases', async() => {
506+
const i = 'show d';
507+
expect(await completer(noParams, i))
508+
.to.deep.equal([['show databases'], i, 'exclusive']);
509+
});
510+
511+
it('completes show profile', async() => {
512+
const i = 'show pr';
513+
expect(await completer(noParams, i))
514+
.to.deep.equal([['show profile'], i, 'exclusive']);
515+
});
516+
517+
it('completes use db', async() => {
518+
databases = ['db1', 'db2'];
519+
const i = 'use';
520+
expect(await completer(noParams, i))
521+
.to.deep.equal([['use db1', 'use db2'], i, 'exclusive']);
522+
});
523+
524+
it('does not try to complete over-long commands', async() => {
525+
databases = ['db1', 'db2'];
526+
const i = 'use db1 d';
527+
expect(await completer(noParams, i))
528+
.to.deep.equal([[], i, 'exclusive']);
529+
});
530+
531+
it('completes commands like exit', async() => {
532+
const i = 'exi';
533+
expect(await completer(noParams, i))
534+
.to.deep.equal([['exit'], i]);
535+
});
536+
});
485537
});

packages/autocomplete/index.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface AutocompleteParameters {
2424
server_version: string;
2525
},
2626
getCollectionCompletionsForCurrentDb: (collName: string) => string[] | Promise<string[]>;
27+
getDatabaseCompletions: (dbName: string) => string[] | Promise<string[]>;
2728
}
2829

2930
export const BASE_COMPLETIONS = EXPRESSION_OPERATORS.concat(
@@ -50,7 +51,7 @@ const GROUP = '$group';
5051
*
5152
* @returns {array} Matching Completions, Current User Input.
5253
*/
53-
async function completer(params: AutocompleteParameters, line: string): Promise<[string[], string]> {
54+
async function completer(params: AutocompleteParameters, line: string): Promise<[string[], string, 'exclusive'] | [string[], string]> {
5455
const SHELL_COMPLETIONS = shellSignatures.ShellApi.attributes as TypeSignatureAttributes;
5556
const COLL_COMPLETIONS = shellSignatures.Collection.attributes as TypeSignatureAttributes;
5657
const DB_COMPLETIONS = shellSignatures.Database.attributes as TypeSignatureAttributes;
@@ -60,6 +61,26 @@ async function completer(params: AutocompleteParameters, line: string): Promise<
6061
const CONFIG_COMPLETIONS = shellSignatures.ShellConfig.attributes as TypeSignatureAttributes;
6162
const SHARD_COMPLETE = shellSignatures.Shard.attributes as TypeSignatureAttributes;
6263

64+
const splitLineWhitespace = line.split(' ');
65+
const command = splitLineWhitespace[0];
66+
if (SHELL_COMPLETIONS[command]?.isDirectShellCommand) {
67+
// If we encounter a direct shell commmand, we know that we want completions
68+
// specific to that command, so we set the 'exclusive' flag on the result.
69+
// If the shell API provides us with a completer, use it.
70+
const completer = SHELL_COMPLETIONS[command].shellCommandCompleter;
71+
if (completer) {
72+
if (splitLineWhitespace.length === 1) {
73+
splitLineWhitespace.push(''); // Treat e.g. 'show' like 'show '.
74+
}
75+
const hits = await completer(params, splitLineWhitespace) || [];
76+
// Adjust to full input, because `completer` only completed the last item
77+
// in the line, e.g. ['profile'] -> ['show profile']
78+
const fullLineHits = hits.map(hit => [...splitLineWhitespace.slice(0, -1), hit].join(' '));
79+
return [fullLineHits, line, 'exclusive'];
80+
}
81+
return [[line], line, 'exclusive'];
82+
}
83+
6384
// keep initial line param intact to always return in return statement
6485
// check for contents of line with:
6586
const splitLine = line.split('.');

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ const standalone440 = {
99
is_data_lake: false,
1010
server_version: '4.4.0'
1111
}),
12-
getCollectionCompletionsForCurrentDb: () => ['bananas']
12+
getCollectionCompletionsForCurrentDb: () => ['bananas'],
13+
getDatabaseCompletions: () => ['databaseOne']
1314
};
1415

1516
describe('Autocompleter', () => {
@@ -43,6 +44,14 @@ describe('Autocompleter', () => {
4344
completion: 'db.bananas'
4445
});
4546
});
47+
48+
it('returns database names after use', async() => {
49+
const completions = await autocompleter.getCompletions('use da');
50+
51+
expect(completions).to.deep.contain({
52+
completion: 'use databaseOne'
53+
});
54+
});
4655
});
4756
});
4857

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

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,8 @@ describe('CliRepl', () => {
403403
testServer: null,
404404
wantWatch: true,
405405
wantShardDistribution: true,
406-
hasCollectionNames: false
406+
hasCollectionNames: false,
407+
hasDatabaseNames: false
407408
});
408409
});
409410

@@ -566,7 +567,8 @@ describe('CliRepl', () => {
566567
testServer: testServer,
567568
wantWatch: false,
568569
wantShardDistribution: false,
569-
hasCollectionNames: true
570+
hasCollectionNames: true,
571+
hasDatabaseNames: true
570572
});
571573

572574
context('analytics integration', () => {
@@ -765,7 +767,8 @@ describe('CliRepl', () => {
765767
testServer: startTestServer('not-shared', '--replicaset', '--nodes', '1'),
766768
wantWatch: true,
767769
wantShardDistribution: false,
768-
hasCollectionNames: true
770+
hasCollectionNames: true,
771+
hasDatabaseNames: true
769772
});
770773
});
771774

@@ -774,7 +777,8 @@ describe('CliRepl', () => {
774777
testServer: startTestServer('not-shared', '--replicaset', '--sharded', '0'),
775778
wantWatch: true,
776779
wantShardDistribution: true,
777-
hasCollectionNames: false // We're only spinning up a mongos here
780+
hasCollectionNames: false, // We're only spinning up a mongos here
781+
hasDatabaseNames: true
778782
});
779783
});
780784

@@ -783,15 +787,17 @@ describe('CliRepl', () => {
783787
testServer: startTestServer('not-shared', '--auth'),
784788
wantWatch: false,
785789
wantShardDistribution: false,
786-
hasCollectionNames: false
790+
hasCollectionNames: false,
791+
hasDatabaseNames: false
787792
});
788793
});
789794

790-
function verifyAutocompletion({ testServer, wantWatch, wantShardDistribution, hasCollectionNames }: {
795+
function verifyAutocompletion({ testServer, wantWatch, wantShardDistribution, hasCollectionNames, hasDatabaseNames }: {
791796
testServer: MongodSetup | null,
792797
wantWatch: boolean,
793798
wantShardDistribution: boolean,
794-
hasCollectionNames: boolean
799+
hasCollectionNames: boolean,
800+
hasDatabaseNames: boolean
795801
}): void {
796802
describe('autocompletion', () => {
797803
let cliRepl: CliRepl;
@@ -871,6 +877,24 @@ describe('CliRepl', () => {
871877
expect(output).not.to.include('JSON.stringify');
872878
expect(output).not.to.include('rawValue');
873879
});
880+
881+
it('completes shell commands', async() => {
882+
input.write('const dSomeVariableStartingWithD = 10;\n');
883+
await waitEval(cliRepl.bus);
884+
885+
output = '';
886+
input.write(`show d${tab}`);
887+
await waitCompletion(cliRepl.bus);
888+
expect(output).to.include('show databases');
889+
expect(output).not.to.include('dSomeVariableStartingWithD');
890+
});
891+
892+
it('completes use <db>', async() => {
893+
if (!hasDatabaseNames) return;
894+
input.write(`use adm${tab}`);
895+
await waitCompletion(cliRepl.bus);
896+
expect(output).to.include('use admin');
897+
});
874898
});
875899
}
876900
});

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,19 @@ class MongoshNodeRepl implements EvaluationListener {
137137
this.insideAutoComplete = true;
138138
try {
139139
// Merge the results from the repl completer and the mongosh completer.
140-
const [ [replResults], [mongoshResults] ] = await Promise.all([
140+
const [ [replResults], [mongoshResults,, mongoshResultsExclusive] ] = await Promise.all([
141141
(async() => await origReplCompleter(text) || [[]])(),
142142
(async() => await mongoshCompleter(text))()
143143
]);
144144
this.bus.emit('mongosh:autocompletion-complete'); // For testing.
145+
146+
// Sometimes the mongosh completion knows that what it is doing is right,
147+
// and that autocompletion based on inspecting the actual objects that
148+
// are being accessed will not be helpful, e.g. in `use a<tab>`, we know
149+
// that we want *only* database names and not e.g. `assert`.
150+
if (mongoshResultsExclusive) {
151+
return [mongoshResults, text];
152+
}
145153
// Remove duplicates, because shell API methods might otherwise show
146154
// up in both completions.
147155
const deduped = [...new Set([...replResults, ...mongoshResults])];

packages/shell-api/src/aggregation-cursor.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ describe('AggregationCursor', () => {
2828
returnType: 'AggregationCursor',
2929
platforms: ALL_PLATFORMS,
3030
topologies: ALL_TOPOLOGIES,
31-
serverVersions: ALL_SERVER_VERSIONS
31+
serverVersions: ALL_SERVER_VERSIONS,
32+
isDirectShellCommand: false,
33+
shellCommandCompleter: undefined
3234
});
3335
});
3436
});

packages/shell-api/src/bulk.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ describe('Bulk API', () => {
3939
returnType: 'BulkFindOp',
4040
platforms: ALL_PLATFORMS,
4141
topologies: ALL_TOPOLOGIES,
42-
serverVersions: ALL_SERVER_VERSIONS
42+
serverVersions: ALL_SERVER_VERSIONS,
43+
isDirectShellCommand: false,
44+
shellCommandCompleter: undefined
4345
});
4446
});
4547
it('hasAsyncChild', () => {
@@ -241,7 +243,9 @@ describe('Bulk API', () => {
241243
returnType: 'BulkFindOp',
242244
platforms: ALL_PLATFORMS,
243245
topologies: ALL_TOPOLOGIES,
244-
serverVersions: ALL_SERVER_VERSIONS
246+
serverVersions: ALL_SERVER_VERSIONS,
247+
isDirectShellCommand: false,
248+
shellCommandCompleter: undefined
245249
});
246250
});
247251
it('hasAsyncChild', () => {

packages/shell-api/src/change-stream-cursor.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ describe('ChangeStreamCursor', () => {
3434
returnType: { type: 'unknown', attributes: {} },
3535
platforms: ALL_PLATFORMS,
3636
topologies: ALL_TOPOLOGIES,
37-
serverVersions: ALL_SERVER_VERSIONS
37+
serverVersions: ALL_SERVER_VERSIONS,
38+
isDirectShellCommand: false,
39+
shellCommandCompleter: undefined
3840
});
3941
});
4042
});

packages/shell-api/src/collection.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ describe('Collection', () => {
4444
returnType: 'AggregationCursor',
4545
platforms: ALL_PLATFORMS,
4646
topologies: ALL_TOPOLOGIES,
47-
serverVersions: ALL_SERVER_VERSIONS
47+
serverVersions: ALL_SERVER_VERSIONS,
48+
isDirectShellCommand: false,
49+
shellCommandCompleter: undefined
4850
});
4951
});
5052
it('hasAsyncChild', () => {

packages/shell-api/src/cursor.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ describe('Cursor', () => {
3232
returnType: 'Cursor',
3333
platforms: ALL_PLATFORMS,
3434
topologies: ALL_TOPOLOGIES,
35-
serverVersions: ALL_SERVER_VERSIONS
35+
serverVersions: ALL_SERVER_VERSIONS,
36+
isDirectShellCommand: false,
37+
shellCommandCompleter: undefined
3638
});
3739
});
3840
});

0 commit comments

Comments
 (0)