From a290948ad931a1762b89b2bd561c7210929131d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=B5=20V=C4=83n=20Th=C3=A0nh=20=28Jay=29?= <80233350+jthanh8144@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:53:34 +0700 Subject: [PATCH] fix(editor): suggest MongoDB collection methods --- .../src/components/editor/QueryEditor.vue | 2 +- apps/desktop/src/lib/mongo/mongoCompletion.ts | 19 ++++++++- packages/app-tests/mongoCompletion.test.ts | 41 ++++++++++++++++++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 76ef25ffe5..c283cfc2b0 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -3384,7 +3384,7 @@ async function provideMongoCompletions(currentState: import("@codemirror/state") return { from: completionContext.from, options: items.map((item) => completionOptionForItem(item)), - validFor: getMongoCompletionResultValidFor(), + validFor: getMongoCompletionResultValidFor(completionContext), }; } diff --git a/apps/desktop/src/lib/mongo/mongoCompletion.ts b/apps/desktop/src/lib/mongo/mongoCompletion.ts index 6b001ab5b0..e41c7f2595 100644 --- a/apps/desktop/src/lib/mongo/mongoCompletion.ts +++ b/apps/desktop/src/lib/mongo/mongoCompletion.ts @@ -158,6 +158,9 @@ const METHOD_ARG_ROLES: Record = { const CALL_METHOD_PATTERN = new RegExp(`\\.(${Object.keys(METHOD_ARG_ROLES).join("|")})\\s*\\(`, "g"); +/** Modes reached by walking the `db.collection.method` chain, where a `.` switches item source. */ +const DOT_SCOPED_MODES = new Set(["root", "collection", "collectionOrMethod", "method", "cursorMethod"]); + /** Stages whose body is a fixed set of option keys rather than a field map. */ const OPTION_STAGES = new Set(Object.keys(STAGE_OPTION_KEYS)); @@ -325,8 +328,19 @@ export function shouldAutoOpenMongoCompletion(text: string, cursor: number): boo return false; } -export function getMongoCompletionResultValidFor(): RegExp { - return /["']?[\w_$.-]*$/; +/** + * How far the editor may keep re-filtering a result before it has to ask us + * again. In the `db.…` chain every `.` moves the cursor to a different item + * source (root → collection → method → cursor method), so a result must stop + * being valid as soon as the typed text gains a dot it did not have — otherwise + * the editor keeps filtering collection names against `users.` and shows + * nothing. Elsewhere (field paths, quoted collection names) dots are just part + * of the identifier and are safe to keep. + */ +export function getMongoCompletionResultValidFor(context?: MongoCompletionContext): RegExp { + if (!context || !DOT_SCOPED_MODES.has(context.mode)) return /["']?[\w_$.-]*$/; + const segments = context.prefix.split(".").length; + return new RegExp(`["']?[\\w_$-]*${"\\.[\\w_$-]*".repeat(segments - 1)}$`); } export function inferMongoCompletionFields(documents: unknown[]): MongoCompletionField[] { @@ -677,6 +691,7 @@ function collectionOrMethodItems(prefix: string, collections: string[]): MongoCo const methods = methodItems(methodPrefix).map((item) => ({ ...item, apply: `${collectionRef}.${item.apply}`, + filterText: `${collection}.${item.label}`, boost: !hasExactCollection && hasDottedCollectionCandidate ? Math.min(item.boost, 110) : item.boost, })); diff --git a/packages/app-tests/mongoCompletion.test.ts b/packages/app-tests/mongoCompletion.test.ts index 4fdb97e88a..c52367380f 100644 --- a/packages/app-tests/mongoCompletion.test.ts +++ b/packages/app-tests/mongoCompletion.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { test } from "vitest"; -import { buildMongoCompletionItems, getMongoCompletionContext, inferMongoCompletionFields, shouldAutoOpenMongoCompletion } from "../../apps/desktop/src/lib/mongo/mongoCompletion.ts"; +import { buildMongoCompletionItems, getMongoCompletionContext, getMongoCompletionResultValidFor, inferMongoCompletionFields, shouldAutoOpenMongoCompletion } from "../../apps/desktop/src/lib/mongo/mongoCompletion.ts"; import { ACCUMULATORS, EXPRESSION_OPERATORS, PIPELINE_STAGES, PUSH_MODIFIERS, QUERY_OPERATORS, STAGE_OPTION_KEYS, UPDATE_OPERATORS, VALUE_SNIPPETS } from "../../apps/desktop/src/lib/mongo/mongoCompletionTables.ts"; const collections = ["users", "user_events", "order-items", "audit.logs"]; @@ -62,6 +62,45 @@ test("keeps direct collection method completion while resolving dotted names", ( assert.equal(item?.apply, "users.find({})"); }); +// The editor filters options against the document text from `from` to the +// cursor, so every item must be matchable by the whole prefix it replaces — +// a bare `find` label under a `users.fi` prefix is silently dropped. +test("makes collection-qualified methods matchable by the prefix they replace", () => { + for (const text of ["db.users.", "db.users.fi", "db.audit."]) { + const context = getMongoCompletionContext(text, text.length); + const prefix = text.slice(context.from); + for (const item of buildMongoCompletionItems(text, text.length, { collections })) { + const matchable = item.filterText ?? item.label; + assert.ok(matchable.toLowerCase().startsWith(prefix.toLowerCase()), `"${matchable}" is not matchable by the typed "${prefix}"`); + } + } + + const item = buildMongoCompletionItems("db.users.fi", "db.users.fi".length, { collections }).find((candidate) => candidate.label === "find"); + assert.equal(item?.filterText, "users.find"); +}); + +test("stops reusing a completion result once the typed text gains a dot", () => { + const validFor = (text: string) => { + const pattern = getMongoCompletionResultValidFor(getMongoCompletionContext(text, text.length)); + return (typed: string) => new RegExp(`^(?:${pattern.source})$`).test(typed); + }; + + // `db.` lists collections; typing `users` keeps that list usable, `users.` does not. + const afterDb = validFor("db."); + assert.equal(afterDb("users"), true); + assert.equal(afterDb("users."), false); + + // `db.users.` lists methods; narrowing to `users.fi` keeps them, a further dot does not. + const afterCollection = validFor("db.users."); + assert.equal(afterCollection("users.fi"), true); + assert.equal(afterCollection("users.find."), false); + assert.equal(afterCollection("users"), false); + + // Dots inside a quoted collection name are part of the identifier, not a scope change. + const insideGetCollection = validFor('db.getCollection("audit.'); + assert.equal(insideGetCollection('"audit.logs'), true); +}); + test("suggests dotted names inside getCollection", () => { const text = 'db.getCollection("audit.'; const item = buildMongoCompletionItems(text, text.length, { collections }).find((candidate) => candidate.label === "audit.logs");