fix(sql-completion): improve MongoDB editor suggestions
This commit is contained in:
parent
b65962d46f
commit
9c8daa2bd4
|
|
@ -1277,7 +1277,7 @@ function executableStatementRangeStartingAt(currentView: EditorViewType, lineFro
|
|||
}
|
||||
|
||||
function currentExecutableStatementRange(currentView: EditorViewType): SqlTextRange | null {
|
||||
if (!supportsExecutionTargetPicker(props.databaseType)) return null;
|
||||
if (!supportsExecutionTargetPicker(props.databaseType) && props.databaseType !== "mongodb") return null;
|
||||
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, currentView.state.doc, props.databaseType, sqlStatementParameterOptions());
|
||||
return executableStatementRangeAtCursor(executableStatementRangeCache, currentView.state.selection.main.head);
|
||||
}
|
||||
|
|
@ -2532,15 +2532,22 @@ function completionOptionForItem(item: QueryCompletionItem) {
|
|||
apply(view: EditorViewType, completionItem: unknown, from: number, to: number) {
|
||||
record();
|
||||
markCompletionAccepted(item);
|
||||
const replaceTo = "replaceClosingQuote" in item && item.replaceClosingQuote === view.state.sliceDoc(to, to + 1) ? to + 1 : to;
|
||||
if (typeof originalApply === "function") {
|
||||
originalApply(view, completionItem as never, from, to);
|
||||
originalApply(view, completionItem as never, from, replaceTo);
|
||||
} else {
|
||||
const insert = String(originalApply ?? item.label);
|
||||
view.dispatch({
|
||||
changes: { from, to, insert },
|
||||
changes: { from, to: replaceTo, insert },
|
||||
selection: { anchor: from + insert.length },
|
||||
});
|
||||
}
|
||||
if (props.databaseType === "mongodb") {
|
||||
const position = view.state.selection.main.head;
|
||||
if (getMongoCompletionContext(view.state.doc.toString(), position).mode === "collectionRef") {
|
||||
scheduleSqlCompletionStart(view, 50);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -2553,16 +2560,17 @@ function completionOptionForItem(item: QueryCompletionItem) {
|
|||
apply(view: EditorViewType, _completionItem: unknown, from: number, to: number) {
|
||||
record();
|
||||
markCompletionAccepted(item);
|
||||
const replaceTo = "replaceClosingQuote" in item && item.replaceClosingQuote === view.state.sliceDoc(to, to + 1) ? to + 1 : to;
|
||||
const insert = appendSqlCompletionSpace(item.apply ?? item.label, {
|
||||
enabled: shouldInsertSqlCompletionSpace() && settingsStore.editorSettings.insertSpaceAfterCompletion,
|
||||
itemType: item.type,
|
||||
nextCharacter: view.state.sliceDoc(to, to + 1),
|
||||
nextCharacter: view.state.sliceDoc(replaceTo, replaceTo + 1),
|
||||
});
|
||||
if (codeMirrorInsertCompletionText) {
|
||||
view.dispatch(codeMirrorInsertCompletionText(view.state, insert, from, to));
|
||||
view.dispatch(codeMirrorInsertCompletionText(view.state, insert, from, replaceTo));
|
||||
} else {
|
||||
view.dispatch({
|
||||
changes: { from, to, insert },
|
||||
changes: { from, to: replaceTo, insert },
|
||||
selection: { anchor: from + insert.length },
|
||||
});
|
||||
}
|
||||
|
|
@ -2855,12 +2863,12 @@ function isEditorComposing(currentView: EditorViewType): boolean {
|
|||
return imeCompositionActive || currentView.compositionStarted || currentView.composing;
|
||||
}
|
||||
|
||||
function scheduleSqlCompletionStart(currentView: EditorViewType) {
|
||||
function scheduleSqlCompletionStart(currentView: EditorViewType, delayMs = 0) {
|
||||
window.setTimeout(() => {
|
||||
if (!codeMirrorStartCompletion || isEditorComposing(currentView)) return;
|
||||
markTypedCompletionActivation();
|
||||
codeMirrorStartCompletion(currentView);
|
||||
}, 0);
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function flushImeComposition() {
|
||||
|
|
@ -2886,6 +2894,9 @@ function shouldStartSqlCompletionAfterInput(insertedText: string, removedText: s
|
|||
const position = currentView.state.selection.main.head;
|
||||
const fullDoc = currentView.state.doc.toString();
|
||||
if (resolveSqlServerUseDatabaseCompletion({ sql: fullDoc, cursor: position, databaseType: props.databaseType })) return true;
|
||||
if (props.databaseType === "mongodb") {
|
||||
return !!(insertedText || removedText) && shouldAutoOpenMongoCompletion(fullDoc, position);
|
||||
}
|
||||
if (!insertedText && removedText) {
|
||||
const completionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
return isTableNameCompletionContext(completionContext) && shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions());
|
||||
|
|
|
|||
|
|
@ -3,6 +3,16 @@ import { describe, expect, it, vi } from "vitest";
|
|||
import { executableStatementRangeAtCursor, executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeParser } from "@/lib/sql/executableStatementRangeCache";
|
||||
|
||||
describe("executableStatementRangeCacheForDoc", () => {
|
||||
it("tracks MongoDB commands for current-statement framing", () => {
|
||||
const sql = 'db.users.find({})\n\ndb.getCollection("audit.logs").countDocuments({})';
|
||||
const doc = Text.of(sql.split("\n"));
|
||||
const cache = executableStatementRangeCacheForDoc(null, doc, "mongodb");
|
||||
|
||||
expect(executableStatementRangeAtCursor(cache, sql.indexOf("users"))?.sql).toBe("db.users.find({})");
|
||||
expect(executableStatementRangeAtCursor(cache, sql.indexOf("audit.logs"))?.sql).toBe('db.getCollection("audit.logs").countDocuments({})');
|
||||
expect(executableStatementRangeAtCursor(cache, doc.line(2).from)).toBeNull();
|
||||
});
|
||||
|
||||
it("reuses parsed executable statement ranges for the same document and database type", () => {
|
||||
const doc = Text.of(["SELECT 1;", "SELECT 2;"]);
|
||||
const parse = vi.fn<ExecutableStatementRangeParser>(() => [
|
||||
|
|
|
|||
|
|
@ -1018,10 +1018,11 @@ describe("currentExecutableStatementRange", () => {
|
|||
expect(currentExecutableStatementRange(sql, indexOf(sql, "comment"), "redis")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not expose current statement framing for MongoDB", () => {
|
||||
const sql = "db.users.find({})";
|
||||
it("uses the current MongoDB command range", () => {
|
||||
const sql = 'db.users.find({})\n\ndb.getCollection("audit.logs").countDocuments({})';
|
||||
|
||||
expect(currentExecutableStatementRange(sql, indexOf(sql, "users"), "mongodb")).toBeNull();
|
||||
expect(currentExecutableStatementRange(sql, indexOf(sql, "users"), "mongodb")?.sql).toBe("db.users.find({})");
|
||||
expect(currentExecutableStatementRange(sql, indexOf(sql, "audit.logs"), "mongodb")?.sql).toBe('db.getCollection("audit.logs").countDocuments({})');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export interface MongoCompletionItem {
|
|||
detail?: string;
|
||||
info?: string;
|
||||
apply?: string;
|
||||
filterText?: string;
|
||||
replaceClosingQuote?: '"' | "'";
|
||||
boost: number;
|
||||
}
|
||||
|
||||
|
|
@ -30,6 +32,8 @@ export interface MongoCompletionContext {
|
|||
mode: MongoCompletionMode;
|
||||
prefix: string;
|
||||
from: number;
|
||||
/** The selected collection name should consume the existing closing quote. */
|
||||
replaceClosingQuote?: '"' | "'";
|
||||
/** Collection the cursor's command targets, used to load field metadata. */
|
||||
collection?: string;
|
||||
/** Enclosing aggregation stage (`$lookup`, `$group`, …), when inside one. */
|
||||
|
|
@ -68,9 +72,36 @@ const COLLECTION_METHODS = [
|
|||
{ label: "drop", detail: "Drop the collection", apply: "drop()" },
|
||||
] as const;
|
||||
|
||||
const COLLECTION_METHOD_BOOST: Record<(typeof COLLECTION_METHODS)[number]["label"], number> = {
|
||||
find: 240,
|
||||
findOne: 230,
|
||||
aggregate: 220,
|
||||
countDocuments: 210,
|
||||
distinct: 200,
|
||||
insertOne: 180,
|
||||
insertMany: 170,
|
||||
updateOne: 160,
|
||||
updateMany: 150,
|
||||
deleteOne: 140,
|
||||
deleteMany: 130,
|
||||
findOneAndUpdate: 120,
|
||||
findOneAndReplace: 115,
|
||||
findOneAndDelete: 110,
|
||||
getIndexes: 100,
|
||||
stats: 95,
|
||||
createIndex: 90,
|
||||
count: 80,
|
||||
dataSize: 75,
|
||||
storageSize: 70,
|
||||
totalIndexSize: 65,
|
||||
dropIndex: 50,
|
||||
dropIndexes: 45,
|
||||
drop: 30,
|
||||
};
|
||||
|
||||
/** Database-level helpers, offered next to the collection names after `db.`. */
|
||||
const DATABASE_METHODS = [
|
||||
{ label: "getCollection", detail: "Reference a collection by name", apply: 'getCollection("${collection}")' },
|
||||
{ label: "getCollection", detail: "Reference a collection by name", apply: 'getCollection("${}")' },
|
||||
{ label: "version", detail: "Show the MongoDB server version", apply: "version()" },
|
||||
] as const;
|
||||
|
||||
|
|
@ -89,11 +120,19 @@ const CURSOR_COUNT_METHOD = { label: "count", detail: "Count the documents match
|
|||
const ROOT_SNIPPETS = [
|
||||
{ label: "db.collection.find", detail: "Find documents", apply: "db.${collection}.find({})" },
|
||||
{ label: "db.collection.aggregate", detail: "Aggregation pipeline", apply: "db.${collection}.aggregate([\n { $match: {} }\n])" },
|
||||
{ label: "db.getCollection", detail: "Reference a collection by name", apply: 'db.getCollection("${collection}")' },
|
||||
{ label: "db.getCollection", detail: "Reference a collection by name", apply: 'db.getCollection("${}")' },
|
||||
{ label: "use", detail: "Switch the active database", apply: "use ${database}" },
|
||||
{ label: "db.version", detail: "Show the MongoDB server version", apply: "db.version()" },
|
||||
] as const;
|
||||
|
||||
const ROOT_SNIPPET_BOOST: Record<(typeof ROOT_SNIPPETS)[number]["label"], number> = {
|
||||
"db.collection.find": 350,
|
||||
"db.collection.aggregate": 340,
|
||||
"db.getCollection": 330,
|
||||
use: 320,
|
||||
"db.version": 310,
|
||||
};
|
||||
|
||||
/** Role of each positional argument, by collection helper. Drives cursor classification. */
|
||||
type MongoArgRole = "filter" | "update" | "replacement" | "document" | "documents" | "pipeline" | "projection" | "keys" | "sortKeys" | "fieldName" | "options";
|
||||
|
||||
|
|
@ -144,14 +183,23 @@ export function getMongoCompletionContext(text: string, cursor: number): MongoCo
|
|||
const beforeCursor = text.slice(0, safeCursor);
|
||||
const collection = extractActiveCollection(text, safeCursor);
|
||||
const { prefix, from } = readPropertyPrefix(text, safeCursor);
|
||||
const at = (mode: MongoCompletionMode, stage?: string): MongoCompletionContext => ({ mode, prefix, from, collection, stage });
|
||||
const replaceClosingQuote = closingQuoteAtCursor(prefix, text, safeCursor);
|
||||
const at = (mode: MongoCompletionMode, stage?: string): MongoCompletionContext => ({ mode, prefix, from, replaceClosingQuote, collection, stage });
|
||||
|
||||
if (isInsideMongoComment(text, safeCursor)) return { mode: "none", prefix: "", from: safeCursor };
|
||||
|
||||
if (beforeCursor.endsWith("db.")) return { mode: "collection", prefix: "", from: safeCursor, collection };
|
||||
|
||||
const getCollectionPrefix = matchGetCollectionPrefix(beforeCursor);
|
||||
if (getCollectionPrefix) return { mode: "collectionRef", prefix: getCollectionPrefix.prefix, from: getCollectionPrefix.from, collection };
|
||||
if (getCollectionPrefix) {
|
||||
return {
|
||||
mode: "collectionRef",
|
||||
prefix: getCollectionPrefix.prefix,
|
||||
from: getCollectionPrefix.from,
|
||||
replaceClosingQuote: closingQuoteAtCursor(getCollectionPrefix.prefix, text, safeCursor),
|
||||
collection,
|
||||
};
|
||||
}
|
||||
|
||||
const collectionPrefix = matchDbCollectionPrefix(beforeCursor);
|
||||
if (collectionPrefix) {
|
||||
|
|
@ -193,46 +241,66 @@ export function buildMongoCompletionItemsFromContext(context: MongoCompletionCon
|
|||
const collections = input.collections ?? [];
|
||||
const fields = input.fields ?? [];
|
||||
|
||||
let items: MongoCompletionItem[];
|
||||
switch (mode) {
|
||||
case "none":
|
||||
return [];
|
||||
items = [];
|
||||
break;
|
||||
case "root":
|
||||
return rootItems(prefix);
|
||||
items = rootItems(prefix);
|
||||
break;
|
||||
case "collection":
|
||||
return collectionItems(prefix, collections);
|
||||
items = collectionItems(prefix, collections);
|
||||
break;
|
||||
case "collectionOrMethod":
|
||||
return collectionOrMethodItems(prefix, collections);
|
||||
items = collectionOrMethodItems(prefix, collections);
|
||||
break;
|
||||
case "collectionRef":
|
||||
return collectionRefItems(prefix, collections);
|
||||
items = collectionRefItems(prefix, collections);
|
||||
break;
|
||||
case "method":
|
||||
return methodItems(prefix);
|
||||
items = methodItems(prefix);
|
||||
break;
|
||||
case "cursorMethod":
|
||||
return cursorMethodItems(prefix, context.stage === "countable");
|
||||
items = cursorMethodItems(prefix, context.stage === "countable");
|
||||
break;
|
||||
case "field":
|
||||
return fieldItems(prefix, fields);
|
||||
items = fieldItems(prefix, fields);
|
||||
break;
|
||||
case "fieldPath":
|
||||
return fieldPathItems(prefix, fields);
|
||||
items = fieldPathItems(prefix, fields);
|
||||
break;
|
||||
case "fieldRef":
|
||||
return fieldRefItems(prefix, fields);
|
||||
items = fieldRefItems(prefix, fields);
|
||||
break;
|
||||
case "value":
|
||||
return specItems(VALUE_SNIPPETS, prefix, "value", 100);
|
||||
items = specItems(VALUE_SNIPPETS, prefix, "value", 100);
|
||||
break;
|
||||
case "queryOperator":
|
||||
return specItems(QUERY_OPERATORS, prefix, "query operator", 100);
|
||||
items = specItems(QUERY_OPERATORS, prefix, "query operator", 100);
|
||||
break;
|
||||
case "updateOperator":
|
||||
return specItems(UPDATE_OPERATORS, prefix, "update operator", 100);
|
||||
items = specItems(UPDATE_OPERATORS, prefix, "update operator", 100);
|
||||
break;
|
||||
case "pushModifier":
|
||||
return specItems(PUSH_MODIFIERS, prefix, "array update modifier", 100);
|
||||
items = specItems(PUSH_MODIFIERS, prefix, "array update modifier", 100);
|
||||
break;
|
||||
case "expression":
|
||||
return [...specItems(EXPRESSION_OPERATORS, prefix, "aggregation expression", 100), ...fieldRefItems(prefix, fields, 80)];
|
||||
items = [...specItems(EXPRESSION_OPERATORS, prefix, "aggregation expression", 100), ...fieldRefItems(prefix, fields, 80)];
|
||||
break;
|
||||
case "accumulator":
|
||||
return specItems(ACCUMULATORS, prefix, "accumulator", 100);
|
||||
items = specItems(ACCUMULATORS, prefix, "accumulator", 100);
|
||||
break;
|
||||
case "stage":
|
||||
return specItems(PIPELINE_STAGES, prefix, "aggregation stage", 100);
|
||||
items = specItems(PIPELINE_STAGES, prefix, "aggregation stage", 100);
|
||||
break;
|
||||
case "stageOption":
|
||||
return specItems(STAGE_OPTION_KEYS[context.stage ?? ""] ?? [], prefix, `${context.stage} option`, 100);
|
||||
items = specItems(STAGE_OPTION_KEYS[context.stage ?? ""] ?? [], prefix, `${context.stage} option`, 100);
|
||||
break;
|
||||
default:
|
||||
return [];
|
||||
items = [];
|
||||
}
|
||||
return finalizeQuotedMongoCompletionItems(context, items);
|
||||
}
|
||||
|
||||
/** Modes whose items are built from the target collection's sampled fields. */
|
||||
|
|
@ -562,14 +630,14 @@ function rootItems(prefix: string): MongoCompletionItem[] {
|
|||
type: "snippet" as const,
|
||||
detail: snippet.detail,
|
||||
apply: snippet.apply,
|
||||
boost: 120,
|
||||
boost: ROOT_SNIPPET_BOOST[snippet.label],
|
||||
}));
|
||||
const methods = COLLECTION_METHODS.filter((method) => matchesFuzzyPrefix(method.label, prefix)).map((method) => ({
|
||||
label: method.label,
|
||||
type: "function" as const,
|
||||
detail: method.detail,
|
||||
apply: method.apply,
|
||||
boost: 100,
|
||||
boost: COLLECTION_METHOD_BOOST[method.label],
|
||||
}));
|
||||
return dedupeAndSort([...snippets, ...methods]);
|
||||
}
|
||||
|
|
@ -603,11 +671,13 @@ function collectionOrMethodItems(prefix: string, collections: string[]): MongoCo
|
|||
const dot = prefix.lastIndexOf(".");
|
||||
const collection = prefix.slice(0, dot);
|
||||
const methodPrefix = prefix.slice(dot + 1);
|
||||
const collectionRef = needsGetCollectionSyntax(collection) && collections.includes(collection) ? `getCollection("${escapeDoubleQuoted(collection)}")` : collection;
|
||||
const hasExactCollection = collections.includes(collection);
|
||||
const hasDottedCollectionCandidate = collections.some((item) => item.startsWith(`${collection}.`));
|
||||
const collectionRef = needsGetCollectionSyntax(collection) && hasExactCollection ? `getCollection("${escapeDoubleQuoted(collection)}")` : collection;
|
||||
const methods = methodItems(methodPrefix).map((item) => ({
|
||||
...item,
|
||||
apply: `${collectionRef}.${item.apply}`,
|
||||
boost: Math.min(item.boost, 110),
|
||||
boost: !hasExactCollection && hasDottedCollectionCandidate ? Math.min(item.boost, 110) : item.boost,
|
||||
}));
|
||||
|
||||
return dedupeAndSort([...collectionNameItems(prefix, collections, 150), ...methods]);
|
||||
|
|
@ -617,13 +687,16 @@ function collectionRefItems(prefix: string, collections: string[]): MongoComplet
|
|||
return collections
|
||||
.filter((collection) => matchesFuzzyPrefix(collection, prefix))
|
||||
.slice(0, 100)
|
||||
.map((collection) => ({
|
||||
label: collection,
|
||||
type: "table" as const,
|
||||
detail: "collection",
|
||||
apply: quoteMongoString(collection, prefix),
|
||||
boost: startsWithPrefix(collection, prefix) ? 120 : 90,
|
||||
}));
|
||||
.map((collection) => {
|
||||
const apply = quoteMongoString(collection, prefix);
|
||||
return {
|
||||
label: collection,
|
||||
type: "table" as const,
|
||||
detail: "collection",
|
||||
apply,
|
||||
boost: startsWithPrefix(collection, prefix) ? 120 : 90,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function methodItems(prefix: string): MongoCompletionItem[] {
|
||||
|
|
@ -633,7 +706,7 @@ function methodItems(prefix: string): MongoCompletionItem[] {
|
|||
type: "function" as const,
|
||||
detail: method.detail,
|
||||
apply: method.apply,
|
||||
boost: method.label === "find" || method.label === "aggregate" ? 130 : 100,
|
||||
boost: COLLECTION_METHOD_BOOST[method.label],
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
|
@ -648,7 +721,7 @@ function cursorMethodItems(prefix: string, countable: boolean): MongoCompletionI
|
|||
type: "function" as const,
|
||||
detail: method.detail,
|
||||
apply: method.apply,
|
||||
boost: method.label === "limit" ? 130 : 110,
|
||||
boost: method.label === "limit" ? 150 : method.label === "sort" ? 140 : method.label === "skip" ? 130 : 120,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
|
@ -721,6 +794,24 @@ function describeField(field: MongoCompletionField, label: string): string {
|
|||
return field.type ? `${label} · ${field.type}` : label;
|
||||
}
|
||||
|
||||
function finalizeQuotedMongoCompletionItems(context: MongoCompletionContext, items: MongoCompletionItem[]): MongoCompletionItem[] {
|
||||
const quote = context.prefix[0];
|
||||
if (quote !== '"' && quote !== "'") return items;
|
||||
|
||||
return items.map((item) => {
|
||||
let apply = item.apply;
|
||||
if (apply?.startsWith(`${item.label}:`)) {
|
||||
apply = `${quote}${item.label}${quote}${apply.slice(item.label.length)}`;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
apply,
|
||||
filterText: apply ?? `${quote}${item.label}${quote}`,
|
||||
replaceClosingQuote: context.replaceClosingQuote,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Text helpers
|
||||
* ------------------------------------------------------------------ */
|
||||
|
|
@ -732,6 +823,11 @@ function readPropertyPrefix(text: string, cursor: number): { prefix: string; fro
|
|||
return { prefix: text.slice(from, cursor), from };
|
||||
}
|
||||
|
||||
function closingQuoteAtCursor(prefix: string, text: string, cursor: number): '"' | "'" | undefined {
|
||||
const quote = prefix[0];
|
||||
return (quote === '"' || quote === "'") && text[cursor] === quote ? quote : undefined;
|
||||
}
|
||||
|
||||
function readMethodPrefix(beforeCursor: string): { prefix: string; from: number } {
|
||||
const dot = beforeCursor.lastIndexOf(".");
|
||||
const from = dot >= 0 ? dot + 1 : beforeCursor.length;
|
||||
|
|
|
|||
|
|
@ -2005,7 +2005,7 @@ export function executableStatementRanges(sql: string, databaseType?: DatabaseTy
|
|||
|
||||
export function currentExecutableStatementRange(sql: string, cursorPos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): SqlTextRange | null {
|
||||
if (databaseType === "redis") return redisCommandRangeAtCursor(sql, cursorPos);
|
||||
if (databaseType === "mongodb") return null;
|
||||
if (databaseType === "mongodb") return mongoCommandRangeAtCursor(sql, cursorPos);
|
||||
return statementRangeAtCursor(sql, cursorPos, databaseType, parameterOptions);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@ test("suggests MongoDB root snippets and methods", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("continues getCollection snippets with collection-name completion", () => {
|
||||
const rootItem = buildMongoCompletionItems("", 0).find((item) => item.label === "db.getCollection");
|
||||
const methodItem = buildMongoCompletionItems("db.getC", "db.getC".length).find((item) => item.label === "getCollection");
|
||||
|
||||
assert.equal(rootItem?.apply, 'db.getCollection("${}")');
|
||||
assert.equal(methodItem?.apply, 'getCollection("${}")');
|
||||
});
|
||||
|
||||
test("suggests collections after db dot", () => {
|
||||
const items = buildMongoCompletionItems("db.us", "db.us".length, { collections });
|
||||
|
||||
|
|
@ -59,6 +67,22 @@ test("suggests dotted names inside getCollection", () => {
|
|||
const item = buildMongoCompletionItems(text, text.length, { collections }).find((candidate) => candidate.label === "audit.logs");
|
||||
|
||||
assert.equal(item?.apply, '"audit.logs"');
|
||||
assert.equal(item?.filterText, '"audit.logs"');
|
||||
});
|
||||
|
||||
test("replaces an existing closing quote when completing an emptied collection name", () => {
|
||||
const text = 'db.getCollection("")';
|
||||
const cursor = text.indexOf('""') + 1;
|
||||
const context = getMongoCompletionContext(text, cursor);
|
||||
const item = buildMongoCompletionItems(text, cursor, { collections }).find((candidate) => candidate.label === "users");
|
||||
|
||||
assert.equal(context.mode, "collectionRef");
|
||||
assert.equal(context.from, text.indexOf('""'));
|
||||
assert.equal(context.replaceClosingQuote, '"');
|
||||
assert.equal(item?.replaceClosingQuote, '"');
|
||||
assert.equal(item?.filterText, '"users"');
|
||||
assert.equal(text.slice(0, context.from) + item?.apply + text.slice(cursor + 1), 'db.getCollection("users")');
|
||||
assert.equal(shouldAutoOpenMongoCompletion(text, cursor), true);
|
||||
});
|
||||
|
||||
test("suggests collection methods after direct and getCollection references", () => {
|
||||
|
|
@ -66,6 +90,22 @@ test("suggests collection methods after direct and getCollection references", ()
|
|||
assert.ok(labels('db.getCollection("users").ag').includes("aggregate"));
|
||||
});
|
||||
|
||||
test("prioritizes common read helpers and keeps destructive helpers last", () => {
|
||||
const methodLabels = labels("db.users.");
|
||||
const getCollectionMethodLabels = labels('db.getCollection("order-events").');
|
||||
|
||||
assert.deepEqual(labels("").slice(0, 5), ["db.collection.find", "db.collection.aggregate", "db.getCollection", "use", "db.version"]);
|
||||
assert.deepEqual(methodLabels.slice(0, 5), ["find", "findOne", "aggregate", "countDocuments", "distinct"]);
|
||||
assert.deepEqual(getCollectionMethodLabels.slice(0, 5), ["find", "findOne", "aggregate", "countDocuments", "distinct"]);
|
||||
assert.deepEqual(methodLabels.slice(-3), ["dropIndex", "dropIndexes", "drop"]);
|
||||
assert.deepEqual(labels("db.users.find({})."), ["limit", "sort", "skip", "count"]);
|
||||
});
|
||||
|
||||
test("keeps dotted collection names ahead of methods until the collection is resolved", () => {
|
||||
assert.equal(labels("db.audit.", { collections })[0], "audit.logs");
|
||||
assert.equal(labels("db.users.", { collections })[0], "find");
|
||||
});
|
||||
|
||||
test("suggests collection stats methods after a collection reference", () => {
|
||||
const methodLabels = labels("db.users.");
|
||||
for (const method of ["stats", "dataSize", "storageSize", "totalIndexSize"]) {
|
||||
|
|
@ -83,7 +123,7 @@ test("suggests cursor methods after find result chains", () => {
|
|||
|
||||
assert.deepEqual(
|
||||
allItems.map((item) => item.label),
|
||||
["limit", "count", "skip", "sort"],
|
||||
["limit", "sort", "skip", "count"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
prefixedItems.map((item) => item.label),
|
||||
|
|
@ -91,7 +131,7 @@ test("suggests cursor methods after find result chains", () => {
|
|||
);
|
||||
assert.deepEqual(
|
||||
formattedChainItems.map((item) => item.label),
|
||||
["limit", "count", "skip", "sort"],
|
||||
["limit", "sort", "skip", "count"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
formattedPrefixedItems.map((item) => item.label),
|
||||
|
|
@ -243,6 +283,32 @@ test("suggests query operators against a field inside a $match stage", () => {
|
|||
assert.equal(items.includes("$group"), false, "a stage is not valid inside a $match constraint");
|
||||
});
|
||||
|
||||
test("filters and replaces quoted fields, field references and operators", () => {
|
||||
const quotedFieldText = 'db.users.find({ "" })';
|
||||
const quotedFieldCursor = quotedFieldText.indexOf('""') + 1;
|
||||
const quotedField = buildMongoCompletionItems(quotedFieldText, quotedFieldCursor, { fields }).find((item) => item.label === "name");
|
||||
assert.equal(quotedField?.apply, '"name": ');
|
||||
assert.equal(quotedField?.filterText, '"name": ');
|
||||
assert.equal(quotedField?.replaceClosingQuote, '"');
|
||||
assert.equal(quotedFieldText.slice(0, getMongoCompletionContext(quotedFieldText, quotedFieldCursor).from) + quotedField?.apply + quotedFieldText.slice(quotedFieldCursor + 1), 'db.users.find({ "name": })');
|
||||
|
||||
const quotedRefText = 'db.users.aggregate([{ $group: { _id: "" } }])';
|
||||
const quotedRefCursor = quotedRefText.indexOf('""') + 1;
|
||||
const quotedRef = buildMongoCompletionItems(quotedRefText, quotedRefCursor, { fields }).find((item) => item.label === "$name");
|
||||
assert.equal(quotedRef?.apply, '"$name"');
|
||||
assert.equal(quotedRef?.filterText, '"$name"');
|
||||
assert.equal(quotedRef?.replaceClosingQuote, '"');
|
||||
assert.equal(quotedRefText.slice(0, getMongoCompletionContext(quotedRefText, quotedRefCursor).from) + quotedRef?.apply + quotedRefText.slice(quotedRefCursor + 1), 'db.users.aggregate([{ $group: { _id: "$name" } }])');
|
||||
|
||||
const quotedOperatorText = 'db.users.find({ age: { "" } })';
|
||||
const quotedOperatorCursor = quotedOperatorText.indexOf('""') + 1;
|
||||
const quotedOperator = buildMongoCompletionItems(quotedOperatorText, quotedOperatorCursor).find((item) => item.label === "$gte");
|
||||
assert.equal(quotedOperator?.apply, '"$gte": ${}');
|
||||
assert.equal(quotedOperator?.filterText, '"$gte": ${}');
|
||||
assert.equal(quotedOperator?.replaceClosingQuote, '"');
|
||||
assert.equal(quotedOperatorText.slice(0, getMongoCompletionContext(quotedOperatorText, quotedOperatorCursor).from) + quotedOperator?.apply + quotedOperatorText.slice(quotedOperatorCursor + 1), 'db.users.find({ age: { "$gte": ${} } })');
|
||||
});
|
||||
|
||||
test("suggests accumulators, not stages, for a $group output field", () => {
|
||||
const items = labels('db.users.aggregate([{ $group: { _id: "$name", total: { $', { fields });
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue