fix(mongodb): stay quiet after a dot on a non-cursor result

This commit is contained in:
Loc Nguyen
2026-10-04 16:49:59 +08:00
committed by GitHub
parent a28b5d5280
commit b12e71455c
2 changed files with 112 additions and 3 deletions
+15 -3
View File
@@ -432,10 +432,10 @@ export function getMongoCompletionContext(text: string, cursor: number): MongoCo
// Top-level snippets belong at the start of a command. Inside an argument list — of a method
// this engine does not model (`limit(`, `drop(`, `renameCollection(`, `runCommand({`, …) or after a
// `use` — they are noise: `db.collection.find` is not something you can type there.
if (!call) return isInsideCallArguments(beforeCursor) || isAfterUseKeyword(beforeCursor) || isAfterShowKeyword(beforeCursor) ? at("none") : at("root");
if (!call) return isInsideCallArguments(beforeCursor) || isAfterUseKeyword(beforeCursor) || isAfterShowKeyword(beforeCursor) || isAfterCallResultDot(beforeCursor) ? at("none") : at("root");
const scan = scanMongoCallArguments(text, call.openParenIndex + 1, safeCursor);
if (!scan) return isInsideCallArguments(beforeCursor) || isAfterUseKeyword(beforeCursor) || isAfterShowKeyword(beforeCursor) ? at("none") : at("root");
if (!scan) return isInsideCallArguments(beforeCursor) || isAfterUseKeyword(beforeCursor) || isAfterShowKeyword(beforeCursor) || isAfterCallResultDot(beforeCursor) ? at("none") : at("root");
const classified = classifyCursorInCall(call.method, scan);
const variables = classified.mode === "fieldRef" || classified.mode === "expression" ? collectScopeVariables(scan) : undefined;
@@ -599,7 +599,10 @@ export function shouldAutoOpenMongoCompletion(text: string, cursor: number): boo
if (text.slice(0, cursor).endsWith("db.")) return true;
// `use ` and `show ` name a database or subcommand next; open the list as soon as the space is typed.
if (previousChar === " " && (matchUseDatabasePrefix(text.slice(0, cursor)) || matchShowSubcommandPrefix(text.slice(0, cursor)))) return true;
if (previousChar === "$" || previousChar === "." || previousChar === '"' || previousChar === "'") return true;
if (previousChar === ".") {
return getMongoCompletionContext(text, cursor).mode !== "none";
}
if (previousChar === "$" || previousChar === '"' || previousChar === "'") return true;
if (/[{,[:]/.test(previousChar) || /[{,[:]\s+$/.test(text.slice(0, cursor))) {
return getMongoCompletionContext(text, cursor).mode !== "none";
}
@@ -2158,6 +2161,15 @@ function isAfterShowKeyword(beforeCursor: string): boolean {
return /(?:^|[\s;])show\s+[\w$-]*(?:\s+[\w$-]*)*$/i.test(currentLine);
}
/** After a closing parenthesis and a dot on a call chain that is not a modeled cursor chain. */
function isAfterCallResultDot(beforeCursor: string): boolean {
const masked = maskMongoLiterals(beforeCursor);
if (!/\)\s*\.\s*[\w$-]*$/.test(masked)) return false;
if (/(?:getCollection|getSiblingDB)\s*\([^()]*\)\s*\.\s*[\w$-]*$/.test(masked)) return false;
const cursorChain = matchCursorMethodDot(beforeCursor);
return !cursorChain || cursorChain.terminal === true;
}
/** Blank out string/comment CONTENT (preserving length, so offsets stay valid) before pattern matching. */
function maskMongoLiterals(text: string): string {
const chars = [...text];
+97
View File
@@ -2070,3 +2070,100 @@ test("keeps the find chain completable when a comment holds an unbalanced parent
assert.deepEqual(labels(lineComment, { fields }), ["limit"]);
assert.deepEqual(labels('db.users.find({ note: "(x" }).li', { fields }), ["limit"]);
});
test("suppresses completion after a dot following findOne", () => {
const text = "db.users.findOne({}).";
const context = getMongoCompletionContext(text, text.length);
assert.equal(context.mode, "none");
assert.deepEqual(labels(text, { collections, fields }), []);
assert.equal(shouldAutoOpenMongoCompletion(text, text.length), false);
});
test("suppresses completion after a dot following countDocuments", () => {
const text = "db.users.countDocuments({}).";
const context = getMongoCompletionContext(text, text.length);
assert.equal(context.mode, "none");
assert.deepEqual(labels(text, { collections, fields }), []);
assert.equal(shouldAutoOpenMongoCompletion(text, text.length), false);
});
test("suppresses completion after a dot following explain", () => {
const text = "db.users.explain().";
const context = getMongoCompletionContext(text, text.length);
assert.equal(context.mode, "none");
assert.deepEqual(labels(text, { collections, fields }), []);
assert.equal(shouldAutoOpenMongoCompletion(text, text.length), false);
});
test("suppresses completion after a dot and typed prefix following findOne", () => {
const text = "db.users.findOne({}).na";
const context = getMongoCompletionContext(text, text.length);
assert.equal(context.mode, "none");
assert.deepEqual(labels(text, { collections, fields }), []);
});
test("suppresses completion after a dot following a non-cursor call with masked parens in literals", () => {
for (const text of ['db.users.findOne({ note: "(x)" }).', "db.users.findOne({ /* ) */ }).", "db.users.findOne({}) ."]) {
const context = getMongoCompletionContext(text, text.length);
assert.equal(context.mode, "none", text);
assert.deepEqual(labels(text, { collections, fields }), [], text);
assert.equal(shouldAutoOpenMongoCompletion(text, text.length), false, text);
}
const typed = "db.users.findOne({}) .na";
assert.equal(getMongoCompletionContext(typed, typed.length).mode, "none");
assert.deepEqual(labels(typed, { collections, fields }), []);
});
test("preserves completion behavior for cursor chains, root triggers, and multiline statements", () => {
// cursor methods
assert.equal(getMongoCompletionContext("db.users.find({}).", "db.users.find({}).".length).mode, "cursorMethod");
assert.ok(labels("db.users.find({}).", { fields }).includes("sort"));
assert.equal(getMongoCompletionContext("db.users.find({}).sort({ name: 1 }).", "db.users.find({}).sort({ name: 1 }).".length).mode, "cursorMethod");
assert.ok(labels("db.users.find({}).sort({ name: 1 }).", { fields }).includes("limit"));
// aggregate cursor methods
assert.equal(getMongoCompletionContext("db.users.aggregate([]).", "db.users.aggregate([]).".length).mode, "cursorMethod");
assert.deepEqual(labels("db.users.aggregate([]).").sort(), ["pretty", "toArray"]);
// terminal cursor method count()
assert.equal(getMongoCompletionContext("db.users.find({}).count().", "db.users.find({}).count().".length).mode, "none");
assert.deepEqual(labels("db.users.find({}).count()."), []);
// db root dot / collection prefix / use / show / sh / empty doc
assert.equal(getMongoCompletionContext("db.", "db.".length).mode, "collection");
assert.ok(labels("db.", { collections }).includes("users"));
assert.equal(getMongoCompletionContext("db.us", "db.us".length).mode, "collection");
assert.ok(labels("db.us", { collections }).includes("users"));
assert.ok(labels("db.us", { collections }).includes("user_events"));
assert.equal(getMongoCompletionContext("use ", "use ".length).mode, "database");
assert.equal(getMongoCompletionContext("show ", "show ".length).mode, "showSubcommand");
assert.equal(getMongoCompletionContext("sh", "sh".length).mode, "root");
assert.equal(getMongoCompletionContext("", 0).mode, "root");
assert.equal(getMongoCompletionContext("db.users.find({ name: 'x' });\ndb.", "db.users.find({ name: 'x' });\ndb.".length).mode, "collection");
assert.ok(labels("db.users.find({ name: 'x' });\ndb.", { collections }).includes("users"));
// statement on a new line after a finished statement
assert.equal(getMongoCompletionContext("db.users.findOne({})\ndb.", "db.users.findOne({})\ndb.".length).mode, "collection");
assert.ok(labels("db.users.findOne({})\ndb.", { collections }).includes("users"));
assert.equal(getMongoCompletionContext("db.users.findOne({})\nfi", "db.users.findOne({})\nfi".length).mode, "root");
assert.ok(labels("db.users.findOne({})\nfi").includes("find"));
});
test("preserves auto-trigger and completion for getCollection and getSiblingDB dot chains", () => {
const getCollectionDot = 'db.getCollection("users").';
assert.equal(getMongoCompletionContext(getCollectionDot, getCollectionDot.length).mode, "method");
assert.equal(shouldAutoOpenMongoCompletion(getCollectionDot, getCollectionDot.length), true);
const siblingDbDot = 'db.getSiblingDB("shop").';
assert.equal(getMongoCompletionContext(siblingDbDot, siblingDbDot.length).mode, "collection");
assert.equal(shouldAutoOpenMongoCompletion(siblingDbDot, siblingDbDot.length), true);
const siblingGetCollectionDot = 'db.getSiblingDB("shop").getCollection("a").';
assert.equal(getMongoCompletionContext(siblingGetCollectionDot, siblingGetCollectionDot.length).mode, "method");
assert.equal(shouldAutoOpenMongoCompletion(siblingGetCollectionDot, siblingGetCollectionDot.length), true);
});