fix(elasticsearch): add dsl query snippets to completion

This commit is contained in:
zipg authored and GitHub committed 2026-09-02 14:01:25 +08:00
1 parent 245ea3b640
commit 8772f10aa4
2 files changed
+51 -2

No files matched your search

@@ -27,6 +27,7 @@ export interface ElasticsearchCompletionContext {
replaceClosingQuote?: '"';
jsonSlot?: ElasticsearchJsonSlot;
hasKeySeparator?: boolean;
jsonIndent?: string;
}
export interface ElasticsearchCompletionInput {
@@ -88,6 +89,16 @@ const JSON_SNIPPETS = [
apply: '"match_all": {}',
detail: "Match all documents",
},
{
label: "match",
apply: '"match": {\n "${field}": "${value}"\n}',
detail: "Match query",
},
{
label: "term",
apply: '"term": {\n "${field}": "${value}"\n}',
detail: "Term query",
},
{
label: "bool",
apply: '"bool": {\n "must": [\n {}\n ],\n "filter": []\n}',
@@ -98,6 +109,11 @@ const JSON_SNIPPETS = [
apply: '"range": {\n "${field}": {\n "gte": "${value}"\n }\n}',
detail: "Range query",
},
{
label: "exists",
apply: '"exists": {\n "field": "${field}"\n}',
detail: "Exists query",
},
{
label: "terms",
apply: '"terms": {\n "${field}": []\n}',
@@ -117,7 +133,10 @@ export function getElasticsearchCompletionContext(text: string, cursor: number):
// Check the current line before falling back to JSON mode so completion also
// works after leading comments and on subsequent REST requests.
const methodMatch = /^\s*([A-Za-z]*)$/.exec(beforeCursorOnLine);
if (methodMatch) {
// A bare word on a JSON body line is a DSL key, not another HTTP method.
// Without this guard, typing `term` after an opening `{` incorrectly enters
// method mode and produces no JSON suggestions until a quote is added.
if (methodMatch && (lineStart === 0 || !hasOpenJsonContainer(text, safeCursor))) {
const leadingWhitespace = beforeCursorOnLine.length - beforeCursorOnLine.trimStart().length;
return {
mode: "method",
@@ -158,6 +177,7 @@ export function getElasticsearchCompletionContext(text: string, cursor: number):
replaceClosingQuote,
jsonSlot: readJsonSlot(text, jsonPrefix.from),
hasKeySeparator: hasJsonKeySeparator(text, safeCursor),
jsonIndent: beforeCursorOnLine.match(/^\s*/)?.[0] ?? "",
};
}
@@ -275,7 +295,7 @@ function jsonItems(context: ElasticsearchCompletionContext): ElasticsearchComple
label: snippet.label,
type: "snippet" as const,
detail: snippet.detail,
apply: snippet.apply,
apply: indentJsonSnippet(snippet.apply, context.jsonIndent ?? ""),
// Bare snippet labels cannot match a typed opening quote, so let the
// editor filter them on the quoted form it sees in the document.
filterText: quoted ? `"${snippet.label}"` : undefined,
@@ -286,6 +306,11 @@ function jsonItems(context: ElasticsearchCompletionContext): ElasticsearchComple
return dedupeAndSort([...snippetItems, ...keyItems]);
}
function indentJsonSnippet(snippet: string, baseIndent: string): string {
if (!baseIndent) return snippet;
return snippet.replace(/\n/g, `\n${baseIndent}`);
}
// Reads the body of the request the cursor sits in, with strings and comments
// blanked out so their quotes and braces cannot be mistaken for structure.
function readRequestBody(text: string, from: number): string {
@@ -294,6 +319,15 @@ function readRequestBody(text: string, from: number): string {
return before.slice(requestLine?.[0].length ?? 0).replace(/"(?:\\.|[^"\\])*"|(?:#|\/\/)[^\n]*/g, "");
}
function hasOpenJsonContainer(text: string, cursor: number): boolean {
const stack: string[] = [];
for (const char of readRequestBody(text, cursor)) {
if (char === "{" || char === "[") stack.push(char);
else if (char === "}" || char === "]") stack.pop();
}
return stack.length > 0;
}
// Tracks open brackets so an array element or a value after `:` is never
// mistaken for an object key.
function readJsonSlot(text: string, from: number): ElasticsearchJsonSlot {