Skip to content

feat: initial feature-flagged, new autocompleter MONGOSH-2036 #2424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
May 27, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6a931fa
Initial feature-flagged, draft new autocompleter.
lerouxb Mar 31, 2025
88edcd3
use the specified connection and db name
lerouxb Apr 2, 2025
70168e6
port some tests to autocomplete
lerouxb Apr 2, 2025
fd845e6
this one just worked
lerouxb Apr 2, 2025
b1ccc97
also just works
lerouxb Apr 2, 2025
7fecfac
use the new mongodb-schema
lerouxb Apr 4, 2025
30ad07e
Merge branch 'main' into use-new-autocomplete
lerouxb May 23, 2025
16242e0
backport the circular ref fix
lerouxb May 23, 2025
4c2a001
just use an incrementing unique connection id
lerouxb May 26, 2025
7731cd8
better event based waiting
lerouxb May 26, 2025
824ea47
add ticket number
lerouxb May 26, 2025
df1c303
add ticket number
lerouxb May 26, 2025
f354fe3
add ticket number
lerouxb May 26, 2025
1e92a11
Merge branch 'main' into use-new-autocomplete
lerouxb May 26, 2025
06b5b66
Merge branch 'use-new-autocomplete' of https://github.com/mongodb-js/…
lerouxb May 26, 2025
729b4c1
put mongodb-schema as a dep in the right place
lerouxb May 26, 2025
691fdfe
put mongodb-schema as a dep in the right place
lerouxb May 26, 2025
b7bba92
don't add getConnectionId to the public interface
lerouxb May 26, 2025
1edc212
move it back
lerouxb May 26, 2025
8b859ce
remove the TODO again, can just use the constant
lerouxb May 26, 2025
001c1d0
add some options to the sample aggregate
lerouxb May 26, 2025
7e39d40
'new', not true
lerouxb May 26, 2025
b3dd61b
unnecessary TODO
lerouxb May 26, 2025
cf99e0b
add an integration test
lerouxb May 26, 2025
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
145 changes: 144 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 78 additions & 6 deletions packages/cli-repl/src/mongosh-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import { Script, createContext, runInContext } from 'vm';
import { installPasteSupport } from './repl-paste-support';
import util from 'util';

import { MongoDBAutocompleter } from '@mongodb-js/mongodb-ts-autocomplete';

declare const __non_webpack_require__: any;

/**
Expand Down Expand Up @@ -131,6 +133,53 @@ type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

function filterStartingWith({
kind,
name,
trigger,
}: {
kind: string;
name: string;
trigger: string;
}): boolean {
name = name.toLocaleLowerCase();
trigger = trigger.toLocaleLowerCase();

/*
1. If the trigger was blank and the kind is not property/method filter out the
result. This way if you autocomplete db.test.find({ you don't get all the
global variables, just the known collection field names and mql but you can
still autocomplete global variables and functions if you type part of the
name.
2. Don't filter out exact matches (where filter === name) so that we match the
behaviour of the node completer.
3. Make sure the name starts with the trigger, otherwise it will return every
possible property/name that's available at that level. ie. all the "peer"
properties of the things that match.
*/
//console.log(name, kind);
// TODO: This can be improved further if we first see if there are any
// property/method kind completions and then just use those, then if there
// aren't return all completions. The reason is that db.test.find({m makes it
// through this filter and then you get all globals starting with m anyway.
// But to properly solve it we need more context. ie. if you're after { (ie.
// inside an object literal) and you're to the left of a : (or there isn't
// one) then you probably don't want globals regardless. If you're to the
// right of a : it is probably fine because you could be using a variable.
return (
(trigger !== '' || kind === 'property' || kind === 'method') &&
name.startsWith(trigger)
);
}

function transformAutocompleteResults(
line: string,
results: { result: string }[]
): [string[], string] | [string[], string, 'exclusive'] {
// TODO: actually use 'exclusive' when we should
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
Expand Down Expand Up @@ -430,10 +479,22 @@ class MongoshNodeRepl implements EvaluationListener {
this.outputFinishString += installPasteSupport(repl);

const origReplCompleter = promisify(repl.completer.bind(repl)); // repl.completer is callback-style
const mongoshCompleter = completer.bind(
null,
instanceState.getAutocompleteParameters()
);
let newMongoshCompleter: MongoDBAutocompleter;
let oldMongoshCompleter: (
line: string
) => Promise<[string[], string, 'exclusive'] | [string[], string]>;
if (process.env.USE_NEW_AUTOCOMPLETE) {
const autocompletionContext = instanceState.getAutocompletionContext();
newMongoshCompleter = new MongoDBAutocompleter({
context: autocompletionContext,
autocompleterOptions: { filter: filterStartingWith },
});
} else {
oldMongoshCompleter = completer.bind(
null,
instanceState.getAutocompleteParameters()
);
}
const innerCompleter = async (
text: string
): Promise<[string[], string]> => {
Expand All @@ -442,8 +503,19 @@ class MongoshNodeRepl implements EvaluationListener {
[replResults, replOrig],
[mongoshResults, , mongoshResultsExclusive],
] = await Promise.all([
(async () => (await origReplCompleter(text)) || [[]])(),
(async () => await mongoshCompleter(text))(),
(async () => {
const nodeResults = (await origReplCompleter(text)) || [[]];
return nodeResults;
})(),
(async () => {
if (process.env.USE_NEW_AUTOCOMPLETE) {
const results = await newMongoshCompleter.autocomplete(text);
const transformed = transformAutocompleteResults(text, results);
return transformed;
} else {
return oldMongoshCompleter(text);
}
})(),
]);
this.bus.emit('mongosh:autocompletion-complete'); // For testing.

Expand Down
3 changes: 2 additions & 1 deletion packages/shell-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"@mongosh/history": "2.4.6",
"@mongosh/i18n": "2.9.1",
"@mongosh/service-provider-core": "3.1.0",
"mongodb-redact": "^1.1.5"
"mongodb-redact": "^1.1.5",
"mongodb-schema": "^12.5.2"
},
"devDependencies": {
"@mongodb-js/eslint-config-mongosh": "^1.0.0",
Expand Down
24 changes: 24 additions & 0 deletions packages/shell-api/src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export default class Collection extends ShellApiWithMongoClass {
_mongo: Mongo;
_database: Database;
_name: string;
_cachedSampleDocs: Document[] = [];
constructor(mongo: Mongo, database: Database, name: string) {
super();
this._mongo = mongo;
Expand Down Expand Up @@ -2497,6 +2498,29 @@ export default class Collection extends ShellApiWithMongoClass {
definition
);
}

async _getSampleDocs(): Promise<Document[]> {
this._cachedSampleDocs = await (await this.aggregate([])).toArray();
return this._cachedSampleDocs;
}

async _getSampleDocsForCompletion(): Promise<Document[]> {
return await Promise.race([
(async () => {
return await this._getSampleDocs();
})(),
(async () => {
// 200ms should be a good compromise between giving the server a chance
// to reply and responsiveness for human perception. It's not the end
// of the world if we end up using the cached results; usually, they
// are not going to differ from fresh ones, and even if they do, a
// subsequent autocompletion request will almost certainly have at least
// the new cached results.
await new Promise((resolve) => setTimeout(resolve, 200)?.unref?.());
return this._cachedSampleDocs;
})(),
]);
}
}

export type GetShardDistributionResult = {
Expand Down
Loading
Loading