Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 58 additions & 6 deletions packages/autocomplete/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { signatures as shellSignatures, Topologies } from '@mongosh/shell-api';
import { expect } from 'chai';

let collections: string[];
let databases: string[];
const standalone440 = {
topology: () => Topologies.Standalone,
connectionInfo: () => ({
is_atlas: false,
is_data_lake: false,
server_version: '4.4.0'
}),
getCollectionCompletionsForCurrentDb: () => collections
getCollectionCompletionsForCurrentDb: () => collections,
getDatabaseCompletions: () => databases
};
const sharded440 = {
topology: () => Topologies.Sharded,
Expand All @@ -20,7 +22,8 @@ const sharded440 = {
is_data_lake: false,
server_version: '4.4.0'
}),
getCollectionCompletionsForCurrentDb: () => collections
getCollectionCompletionsForCurrentDb: () => collections,
getDatabaseCompletions: () => databases
};

const standalone300 = {
Expand All @@ -30,7 +33,8 @@ const standalone300 = {
is_data_lake: false,
server_version: '3.0.0'
}),
getCollectionCompletionsForCurrentDb: () => collections
getCollectionCompletionsForCurrentDb: () => collections,
getDatabaseCompletions: () => databases
};
const datalake440 = {
topology: () => Topologies.Sharded,
Expand All @@ -39,13 +43,15 @@ const datalake440 = {
is_data_lake: true,
server_version: '4.4.0'
}),
getCollectionCompletionsForCurrentDb: () => collections
getCollectionCompletionsForCurrentDb: () => collections,
getDatabaseCompletions: () => databases
};

const noParams = {
topology: () => Topologies.Standalone,
connectionInfo: () => undefined,
getCollectionCompletionsForCurrentDb: () => collections
getCollectionCompletionsForCurrentDb: () => collections,
getDatabaseCompletions: () => databases
};

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

it('is an exact match to one of shell completions', async() => {
const i = 'use';
expect(await completer(standalone440, i)).to.deep.equal([[i], i]);
expect(await completer(standalone440, i)).to.deep.equal([[], i, 'exclusive']);
});
});

Expand Down Expand Up @@ -482,4 +488,50 @@ describe('completer.completer', () => {
expect(await completer(standalone440, i)).to.deep.equal([[], i]);
});
});

context('for shell commands', () => {
it('completes partial commands', async() => {
const i = 'sho';
expect(await completer(noParams, i))
.to.deep.equal([['show'], i]);
});

it('completes partial commands', async() => {
const i = 'show';
const result = await completer(noParams, i);
expect(result[0]).to.contain('show databases');
});

it('completes show databases', async() => {
const i = 'show d';
expect(await completer(noParams, i))
.to.deep.equal([['show databases'], i, 'exclusive']);
});

it('completes show profile', async() => {
const i = 'show pr';
expect(await completer(noParams, i))
.to.deep.equal([['show profile'], i, 'exclusive']);
});

it('completes use db', async() => {
databases = ['db1', 'db2'];
const i = 'use';
expect(await completer(noParams, i))
.to.deep.equal([['use db1', 'use db2'], i, 'exclusive']);
});

it('does not try to complete over-long commands', async() => {
databases = ['db1', 'db2'];
const i = 'use db1 d';
expect(await completer(noParams, i))
.to.deep.equal([[], i, 'exclusive']);
});

it('completes commands like exit', async() => {
const i = 'exi';
expect(await completer(noParams, i))
.to.deep.equal([['exit'], i]);
});
});
});
23 changes: 22 additions & 1 deletion packages/autocomplete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface AutocompleteParameters {
server_version: string;
},
getCollectionCompletionsForCurrentDb: (collName: string) => string[] | Promise<string[]>;
getDatabaseCompletions: (dbName: string) => string[] | Promise<string[]>;
}

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

const splitLineWhitespace = line.split(' ');
const command = splitLineWhitespace[0];
if (SHELL_COMPLETIONS[command]?.isDirectShellCommand) {
// If we encounter a direct shell commmand, we know that we want completions
// specific to that command, so we set the 'exclusive' flag on the result.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary? Will this also work in the browser?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcasimir Are you referring to the 'exclusive' flag here?

The mongosh-repl.ts changes have a somewhat more specific context here, but the gist of it is that we run both the Node.js default autocompleter and our own there and combine the results; but the Node.js REPL thinks that it should complete e.g. use a to use assert, because it just looks at the a and finds global variables with the same name.

Will this also work in the browser?

I don’t think the extra array entry should hurt, we’re only using the first entry anyway:

const entries = completions[0].map((completion) => {

// If the shell API provides us with a completer, use it.
const completer = SHELL_COMPLETIONS[command].shellCommandCompleter;
if (completer) {
if (splitLineWhitespace.length === 1) {
splitLineWhitespace.push(''); // Treat e.g. 'show' like 'show '.
}
const hits = await completer(params, splitLineWhitespace) || [];
// 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, line, 'exclusive'];
}
return [[line], line, 'exclusive'];
}

// keep initial line param intact to always return in return statement
// check for contents of line with:
const splitLine = line.split('.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const standalone440 = {
is_data_lake: false,
server_version: '4.4.0'
}),
getCollectionCompletionsForCurrentDb: () => ['bananas']
getCollectionCompletionsForCurrentDb: () => ['bananas'],
getDatabaseCompletions: () => ['databaseOne']
};

describe('Autocompleter', () => {
Expand Down Expand Up @@ -43,6 +44,14 @@ describe('Autocompleter', () => {
completion: 'db.bananas'
});
});

it('returns database names after use', async() => {
const completions = await autocompleter.getCompletions('use da');

expect(completions).to.deep.contain({
completion: 'use databaseOne'
});
});
});
});

Expand Down
38 changes: 31 additions & 7 deletions packages/cli-repl/src/cli-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,8 @@ describe('CliRepl', () => {
testServer: null,
wantWatch: true,
wantShardDistribution: true,
hasCollectionNames: false
hasCollectionNames: false,
hasDatabaseNames: false
});
});

Expand Down Expand Up @@ -566,7 +567,8 @@ describe('CliRepl', () => {
testServer: testServer,
wantWatch: false,
wantShardDistribution: false,
hasCollectionNames: true
hasCollectionNames: true,
hasDatabaseNames: true
});

context('analytics integration', () => {
Expand Down Expand Up @@ -765,7 +767,8 @@ describe('CliRepl', () => {
testServer: startTestServer('not-shared', '--replicaset', '--nodes', '1'),
wantWatch: true,
wantShardDistribution: false,
hasCollectionNames: true
hasCollectionNames: true,
hasDatabaseNames: true
});
});

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

Expand All @@ -783,15 +787,17 @@ describe('CliRepl', () => {
testServer: startTestServer('not-shared', '--auth'),
wantWatch: false,
wantShardDistribution: false,
hasCollectionNames: false
hasCollectionNames: false,
hasDatabaseNames: false
});
});

function verifyAutocompletion({ testServer, wantWatch, wantShardDistribution, hasCollectionNames }: {
function verifyAutocompletion({ testServer, wantWatch, wantShardDistribution, hasCollectionNames, hasDatabaseNames }: {
testServer: MongodSetup | null,
wantWatch: boolean,
wantShardDistribution: boolean,
hasCollectionNames: boolean
hasCollectionNames: boolean,
hasDatabaseNames: boolean
}): void {
describe('autocompletion', () => {
let cliRepl: CliRepl;
Expand Down Expand Up @@ -871,6 +877,24 @@ describe('CliRepl', () => {
expect(output).not.to.include('JSON.stringify');
expect(output).not.to.include('rawValue');
});

it('completes shell commands', async() => {
input.write('const dSomeVariableStartingWithD = 10;\n');
await waitEval(cliRepl.bus);

output = '';
input.write(`show d${tab}`);
await waitCompletion(cliRepl.bus);
expect(output).to.include('show databases');
expect(output).not.to.include('dSomeVariableStartingWithD');
});

it('completes use <db>', async() => {
if (!hasDatabaseNames) return;
input.write(`use adm${tab}`);
await waitCompletion(cliRepl.bus);
expect(output).to.include('use admin');
});
});
}
});
Expand Down
10 changes: 9 additions & 1 deletion packages/cli-repl/src/mongosh-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,19 @@ class MongoshNodeRepl implements EvaluationListener {
this.insideAutoComplete = true;
try {
// Merge the results from the repl completer and the mongosh completer.
const [ [replResults], [mongoshResults] ] = await Promise.all([
const [ [replResults], [mongoshResults,, mongoshResultsExclusive] ] = await Promise.all([
(async() => await origReplCompleter(text) || [[]])(),
(async() => await mongoshCompleter(text))()
]);
this.bus.emit('mongosh:autocompletion-complete'); // For testing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I've got a bit better the context. Makes sense.

// Sometimes the mongosh completion knows that what it is doing is right,
// and that autocompletion based on inspecting the actual objects that
// are being accessed will not be helpful, e.g. in `use a<tab>`, we know
// that we want *only* database names and not e.g. `assert`.
if (mongoshResultsExclusive) {
return [mongoshResults, text];
}
// Remove duplicates, because shell API methods might otherwise show
// up in both completions.
const deduped = [...new Set([...replResults, ...mongoshResults])];
Expand Down
4 changes: 3 additions & 1 deletion packages/shell-api/src/aggregation-cursor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ describe('AggregationCursor', () => {
returnType: 'AggregationCursor',
platforms: ALL_PLATFORMS,
topologies: ALL_TOPOLOGIES,
serverVersions: ALL_SERVER_VERSIONS
serverVersions: ALL_SERVER_VERSIONS,
isDirectShellCommand: false,
shellCommandCompleter: undefined
});
});
});
Expand Down
8 changes: 6 additions & 2 deletions packages/shell-api/src/bulk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ describe('Bulk API', () => {
returnType: 'BulkFindOp',
platforms: ALL_PLATFORMS,
topologies: ALL_TOPOLOGIES,
serverVersions: ALL_SERVER_VERSIONS
serverVersions: ALL_SERVER_VERSIONS,
isDirectShellCommand: false,
shellCommandCompleter: undefined
});
});
it('hasAsyncChild', () => {
Expand Down Expand Up @@ -241,7 +243,9 @@ describe('Bulk API', () => {
returnType: 'BulkFindOp',
platforms: ALL_PLATFORMS,
topologies: ALL_TOPOLOGIES,
serverVersions: ALL_SERVER_VERSIONS
serverVersions: ALL_SERVER_VERSIONS,
isDirectShellCommand: false,
shellCommandCompleter: undefined
});
});
it('hasAsyncChild', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/shell-api/src/change-stream-cursor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ describe('ChangeStreamCursor', () => {
returnType: { type: 'unknown', attributes: {} },
platforms: ALL_PLATFORMS,
topologies: ALL_TOPOLOGIES,
serverVersions: ALL_SERVER_VERSIONS
serverVersions: ALL_SERVER_VERSIONS,
isDirectShellCommand: false,
shellCommandCompleter: undefined
});
});
});
Expand Down
4 changes: 3 additions & 1 deletion packages/shell-api/src/collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ describe('Collection', () => {
returnType: 'AggregationCursor',
platforms: ALL_PLATFORMS,
topologies: ALL_TOPOLOGIES,
serverVersions: ALL_SERVER_VERSIONS
serverVersions: ALL_SERVER_VERSIONS,
isDirectShellCommand: false,
shellCommandCompleter: undefined
});
});
it('hasAsyncChild', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/shell-api/src/cursor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ describe('Cursor', () => {
returnType: 'Cursor',
platforms: ALL_PLATFORMS,
topologies: ALL_TOPOLOGIES,
serverVersions: ALL_SERVER_VERSIONS
serverVersions: ALL_SERVER_VERSIONS,
isDirectShellCommand: false,
shellCommandCompleter: undefined
});
});
});
Expand Down
4 changes: 3 additions & 1 deletion packages/shell-api/src/database.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ describe('Database', () => {
returnType: 'AggregationCursor',
platforms: ALL_PLATFORMS,
topologies: ALL_TOPOLOGIES,
serverVersions: ALL_SERVER_VERSIONS
serverVersions: ALL_SERVER_VERSIONS,
isDirectShellCommand: false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a custom "autocompleter" for arguments? And this will work with a worker thread as well cause the function will be called inside the runtime right?

Slowly getting it :).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a custom "autocompleter" for arguments?

The entry below is, yes.

And this will work with a worker thread as well cause the function will be called inside the runtime right?

I think so, yes. The autocompleter in the browser also lives inside the same environment as e.g. the ShellInternalState object, and only the caller-facing getCompletions() method of the runtime is forwarded to the parent/rendering process, so this should work.

shellCommandCompleter: undefined
});
});
it('hasAsyncChild', () => {
Expand Down
Loading