Skip to content

Commit 6a931fa

Browse files
committed
Initial feature-flagged, draft new autocompleter.
1 parent 40b87a2 commit 6a931fa

File tree

5 files changed

+324
-8
lines changed

5 files changed

+324
-8
lines changed

package-lock.json

Lines changed: 144 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ import { Script, createContext, runInContext } from 'vm';
4949
import { installPasteSupport } from './repl-paste-support';
5050
import util from 'util';
5151

52+
import { MongoDBAutocompleter } from '@mongodb-js/mongodb-ts-autocomplete';
53+
5254
declare const __non_webpack_require__: any;
5355

5456
/**
@@ -131,6 +133,53 @@ type Mutable<T> = {
131133
-readonly [P in keyof T]: T[P];
132134
};
133135

136+
function filterStartingWith({
137+
kind,
138+
name,
139+
trigger,
140+
}: {
141+
kind: string;
142+
name: string;
143+
trigger: string;
144+
}): boolean {
145+
name = name.toLocaleLowerCase();
146+
trigger = trigger.toLocaleLowerCase();
147+
148+
/*
149+
1. If the trigger was blank and the kind is not property/method filter out the
150+
result. This way if you autocomplete db.test.find({ you don't get all the
151+
global variables, just the known collection field names and mql but you can
152+
still autocomplete global variables and functions if you type part of the
153+
name.
154+
2. Don't filter out exact matches (where filter === name) so that we match the
155+
behaviour of the node completer.
156+
3. Make sure the name starts with the trigger, otherwise it will return every
157+
possible property/name that's available at that level. ie. all the "peer"
158+
properties of the things that match.
159+
*/
160+
//console.log(name, kind);
161+
// TODO: This can be improved further if we first see if there are any
162+
// property/method kind completions and then just use those, then if there
163+
// aren't return all completions. The reason is that db.test.find({m makes it
164+
// through this filter and then you get all globals starting with m anyway.
165+
// But to properly solve it we need more context. ie. if you're after { (ie.
166+
// inside an object literal) and you're to the left of a : (or there isn't
167+
// one) then you probably don't want globals regardless. If you're to the
168+
// right of a : it is probably fine because you could be using a variable.
169+
return (
170+
(trigger !== '' || kind === 'property' || kind === 'method') &&
171+
name.startsWith(trigger)
172+
);
173+
}
174+
175+
function transformAutocompleteResults(
176+
line: string,
177+
results: { result: string }[]
178+
): [string[], string] | [string[], string, 'exclusive'] {
179+
// TODO: actually use 'exclusive' when we should
180+
return [results.map((result) => result.result), line];
181+
}
182+
134183
/**
135184
* An instance of a `mongosh` REPL, without any of the actual I/O.
136185
* Specifically, code called by this class should not do any
@@ -430,10 +479,22 @@ class MongoshNodeRepl implements EvaluationListener {
430479
this.outputFinishString += installPasteSupport(repl);
431480

432481
const origReplCompleter = promisify(repl.completer.bind(repl)); // repl.completer is callback-style
433-
const mongoshCompleter = completer.bind(
434-
null,
435-
instanceState.getAutocompleteParameters()
436-
);
482+
let newMongoshCompleter: MongoDBAutocompleter;
483+
let oldMongoshCompleter: (
484+
line: string
485+
) => Promise<[string[], string, 'exclusive'] | [string[], string]>;
486+
if (process.env.USE_NEW_AUTOCOMPLETE) {
487+
const autocompletionContext = instanceState.getAutocompletionContext();
488+
newMongoshCompleter = new MongoDBAutocompleter({
489+
context: autocompletionContext,
490+
autocompleterOptions: { filter: filterStartingWith },
491+
});
492+
} else {
493+
oldMongoshCompleter = completer.bind(
494+
null,
495+
instanceState.getAutocompleteParameters()
496+
);
497+
}
437498
const innerCompleter = async (
438499
text: string
439500
): Promise<[string[], string]> => {
@@ -442,8 +503,19 @@ class MongoshNodeRepl implements EvaluationListener {
442503
[replResults, replOrig],
443504
[mongoshResults, , mongoshResultsExclusive],
444505
] = await Promise.all([
445-
(async () => (await origReplCompleter(text)) || [[]])(),
446-
(async () => await mongoshCompleter(text))(),
506+
(async () => {
507+
const nodeResults = (await origReplCompleter(text)) || [[]];
508+
return nodeResults;
509+
})(),
510+
(async () => {
511+
if (process.env.USE_NEW_AUTOCOMPLETE) {
512+
const results = await newMongoshCompleter.autocomplete(text);
513+
const transformed = transformAutocompleteResults(text, results);
514+
return transformed;
515+
} else {
516+
return oldMongoshCompleter(text);
517+
}
518+
})(),
447519
]);
448520
this.bus.emit('mongosh:autocompletion-complete'); // For testing.
449521

packages/shell-api/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
"@mongosh/history": "2.4.6",
4646
"@mongosh/i18n": "2.9.1",
4747
"@mongosh/service-provider-core": "3.1.0",
48-
"mongodb-redact": "^1.1.5"
48+
"mongodb-redact": "^1.1.5",
49+
"mongodb-schema": "^12.5.2"
4950
},
5051
"devDependencies": {
5152
"@mongodb-js/eslint-config-mongosh": "^1.0.0",

packages/shell-api/src/collection.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export default class Collection extends ShellApiWithMongoClass {
100100
_mongo: Mongo;
101101
_database: Database;
102102
_name: string;
103+
_cachedSampleDocs: Document[] = [];
103104
constructor(mongo: Mongo, database: Database, name: string) {
104105
super();
105106
this._mongo = mongo;
@@ -2497,6 +2498,29 @@ export default class Collection extends ShellApiWithMongoClass {
24972498
definition
24982499
);
24992500
}
2501+
2502+
async _getSampleDocs(): Promise<Document[]> {
2503+
this._cachedSampleDocs = await (await this.aggregate([])).toArray();
2504+
return this._cachedSampleDocs;
2505+
}
2506+
2507+
async _getSampleDocsForCompletion(): Promise<Document[]> {
2508+
return await Promise.race([
2509+
(async () => {
2510+
return await this._getSampleDocs();
2511+
})(),
2512+
(async () => {
2513+
// 200ms should be a good compromise between giving the server a chance
2514+
// to reply and responsiveness for human perception. It's not the end
2515+
// of the world if we end up using the cached results; usually, they
2516+
// are not going to differ from fresh ones, and even if they do, a
2517+
// subsequent autocompletion request will almost certainly have at least
2518+
// the new cached results.
2519+
await new Promise((resolve) => setTimeout(resolve, 200)?.unref?.());
2520+
return this._cachedSampleDocs;
2521+
})(),
2522+
]);
2523+
}
25002524
}
25012525

25022526
export type GetShardDistributionResult = {

0 commit comments

Comments
 (0)