fix(editor): distinguish SELECT wildcards from multiplication
This commit is contained in:
1 parent
24179cb45d
commit
ca8bc9bf12
5 files changed
+185
-14
No files matched your search
@@ -164,7 +164,7 @@ import { buildQueryEditorLineNumbersExtension, createQueryEditorLineNumberAlignm
|
||||
import { searchKeymapWithoutModD } from "@/lib/editor/codemirrorSearchKeymap";
|
||||
import { defaultKeymapForGlobalShortcuts } from "@/lib/editor/codemirrorDefaultKeymap";
|
||||
import { appendSqlCompletionSpace } from "@/lib/editor/sqlCompletionInsertion";
|
||||
import { batchColumnSelectionColumnList, batchColumnSelectionInsertReplacement, batchColumnSelectionReplaceTo, isBatchColumnSelectionCompletionActive, shouldResolveSqlColumnCompletion } from "@/lib/editor/batchColumnSelection";
|
||||
import { batchColumnSelectionColumnList, batchColumnSelectionInsertReplacement, batchColumnSelectionReplaceTo, completionReplacementTo, isBatchColumnSelectionCompletionActive, shouldResolveSqlColumnCompletion } from "@/lib/editor/batchColumnSelection";
|
||||
import { compareSqlCompletions, completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
|
||||
import { clampEditorFontSize, createEditorWheelZoomGestureGuard, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom";
|
||||
import { buildSqlShortcutExecutionSql, enabledSqlShortcutActions, resolveSqlShortcutForDatabase, uniqueSqlShortcutBindings } from "@/lib/sql/sqlShortcutActions";
|
||||
@@ -4206,6 +4206,7 @@ interface BatchColumnSelectionSession {
|
||||
from: number;
|
||||
to: number;
|
||||
replaceClosingQuote?: SqlCompletionItem["replaceClosingQuote"];
|
||||
replaceSelectWildcard?: true;
|
||||
candidates: BatchColumnSelectionCandidate[];
|
||||
selectedKeys: Set<string>;
|
||||
completionOptions: Map<string, QueryCompletionOption>;
|
||||
@@ -4405,6 +4406,7 @@ function prepareBatchColumnSelectionSession(items: SqlCompletionItem[], document
|
||||
from,
|
||||
to,
|
||||
replaceClosingQuote: selectableItems[0]!.replaceClosingQuote,
|
||||
replaceSelectWildcard: selectableItems[0]!.replaceSelectWildcard,
|
||||
candidates,
|
||||
selectedKeys: new Set(),
|
||||
completionOptions: new Map(),
|
||||
@@ -4649,10 +4651,12 @@ function applyBatchColumnSelection(view: EditorViewType, item: BatchColumnSelect
|
||||
session.qualifier,
|
||||
);
|
||||
let replaceTo = batchColumnSelectionReplaceTo({
|
||||
from,
|
||||
to,
|
||||
mode: session.mode,
|
||||
nextCharacter: view.state.sliceDoc(to, to + 1),
|
||||
replaceClosingQuote: session.replaceClosingQuote,
|
||||
replaceSelectWildcard: session.replaceSelectWildcard,
|
||||
});
|
||||
let insert = columns;
|
||||
if (session.mode === "insert") {
|
||||
@@ -4857,7 +4861,14 @@ function completionOptionForItem(item: QueryCompletionItem | BatchColumnSelectio
|
||||
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 nextCharacter = view.state.sliceDoc(to, to + 1);
|
||||
const replaceTo = completionReplacementTo({
|
||||
from,
|
||||
to,
|
||||
nextCharacter,
|
||||
replaceClosingQuote: "replaceClosingQuote" in item ? item.replaceClosingQuote : undefined,
|
||||
replaceSelectWildcard: "replaceSelectWildcard" in item ? item.replaceSelectWildcard : undefined,
|
||||
});
|
||||
if (typeof originalApply === "function") {
|
||||
originalApply(view, completionItem as never, from, replaceTo);
|
||||
} else {
|
||||
@@ -4887,7 +4898,14 @@ function completionOptionForItem(item: QueryCompletionItem | BatchColumnSelectio
|
||||
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 nextCharacterAtCursor = view.state.sliceDoc(to, to + 1);
|
||||
const replaceTo = completionReplacementTo({
|
||||
from,
|
||||
to,
|
||||
nextCharacter: nextCharacterAtCursor,
|
||||
replaceClosingQuote: "replaceClosingQuote" in item ? item.replaceClosingQuote : undefined,
|
||||
replaceSelectWildcard: "replaceSelectWildcard" in item ? item.replaceSelectWildcard : undefined,
|
||||
});
|
||||
const insert = appendSqlCompletionSpace(item.apply ?? item.label, {
|
||||
enabled: ("appendSpace" in item && item.appendSpace === true) || (shouldInsertSqlCompletionSpace() && settingsStore.editorSettings.insertSpaceAfterCompletion),
|
||||
itemType: item.type,
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { snippetCompletion } from "@codemirror/autocomplete";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { batchColumnSelectionColumnList, batchColumnSelectionInsertReplacement, batchColumnSelectionReplaceTo, isBatchColumnSelectionCompletionActive, shouldResolveSqlColumnCompletion } from "@/lib/editor/batchColumnSelection";
|
||||
import { batchColumnSelectionColumnList, batchColumnSelectionInsertReplacement, batchColumnSelectionReplaceTo, completionReplacementTo, isBatchColumnSelectionCompletionActive, shouldResolveSqlColumnCompletion, shouldSwallowSelectStar } from "@/lib/editor/batchColumnSelection";
|
||||
import { getSqlCompletionContext, prepareSqlCompletionReplacement } from "@/lib/sql/sqlCompletion";
|
||||
|
||||
const columnItem = { label: "price", type: "column" as const, apply: "price", boost: 0 };
|
||||
|
||||
function replaceSelectWildcardFor(document: string, to: number): boolean {
|
||||
const context = getSqlCompletionContext(document, to);
|
||||
return prepareSqlCompletionReplacement(document, to, context, [columnItem]).items[0]?.replaceSelectWildcard === true;
|
||||
}
|
||||
|
||||
describe("batchColumnSelectionColumnList", () => {
|
||||
it("keeps a typed qualifier on every projection after the first", () => {
|
||||
@@ -13,15 +26,79 @@ describe("batchColumnSelectionColumnList", () => {
|
||||
|
||||
describe("batchColumnSelectionReplaceTo", () => {
|
||||
it("consumes the auto-inserted INSERT closing parenthesis", () => {
|
||||
expect(batchColumnSelectionReplaceTo({ to: 20, mode: "insert", nextCharacter: ")" })).toBe(21);
|
||||
expect(batchColumnSelectionReplaceTo({ from: 20, to: 20, mode: "insert", nextCharacter: ")" })).toBe(21);
|
||||
});
|
||||
|
||||
it("keeps the replacement boundary when INSERT has no closing parenthesis", () => {
|
||||
expect(batchColumnSelectionReplaceTo({ to: 20, mode: "insert", nextCharacter: "" })).toBe(20);
|
||||
expect(batchColumnSelectionReplaceTo({ from: 20, to: 20, mode: "insert", nextCharacter: "" })).toBe(20);
|
||||
});
|
||||
|
||||
it("continues consuming a matching closing identifier quote", () => {
|
||||
expect(batchColumnSelectionReplaceTo({ to: 20, mode: "select", nextCharacter: '"', replaceClosingQuote: '"' })).toBe(21);
|
||||
expect(batchColumnSelectionReplaceTo({ from: 20, to: 20, mode: "select", nextCharacter: '"', replaceClosingQuote: '"' })).toBe(21);
|
||||
});
|
||||
|
||||
it("consumes a lone SELECT * immediately after an empty-prefix completion (#9433-adjacent)", () => {
|
||||
expect(batchColumnSelectionReplaceTo({ from: 7, to: 7, mode: "select", nextCharacter: "*", replaceSelectWildcard: true })).toBe(8);
|
||||
});
|
||||
|
||||
it("does not consume a `*` that is a multiplication operator, not the wildcard", () => {
|
||||
expect(batchColumnSelectionReplaceTo({ from: 7, to: 7, mode: "select", nextCharacter: "*", replaceSelectWildcard: false })).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldSwallowSelectStar", () => {
|
||||
it("swallows a bare `*` only with an explicit SELECT wildcard marker", () => {
|
||||
expect(shouldSwallowSelectStar(7, 7, "*", true)).toBe(true);
|
||||
expect(shouldSwallowSelectStar(7, 7, "*", false)).toBe(false);
|
||||
expect(shouldSwallowSelectStar(7, 7, "*")).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves a `*` alone once a real prefix was replaced", () => {
|
||||
expect(shouldSwallowSelectStar(7, 10, "*", true)).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves any other trailing character alone", () => {
|
||||
expect(shouldSwallowSelectStar(7, 7, " ", true)).toBe(false);
|
||||
expect(shouldSwallowSelectStar(7, 7, "", true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("completion wildcard acceptance", () => {
|
||||
it.each([
|
||||
["SELECT * FROM users", "SELECT price FROM users"],
|
||||
["SELECT *qty FROM users", "SELECT price*qty FROM users"],
|
||||
])("applies ordinary completion without deleting multiplication in %s", (document, expected) => {
|
||||
const from = document.indexOf("*");
|
||||
const replaceTo = completionReplacementTo({ from, to: from, nextCharacter: "*", replaceSelectWildcard: replaceSelectWildcardFor(document, from) });
|
||||
const transaction = EditorState.create({ doc: document }).update({ changes: { from, to: replaceTo, insert: "price" } });
|
||||
|
||||
expect(transaction.state.doc.toString()).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["SELECT * FROM users", "SELECT price FROM users"],
|
||||
["SELECT *qty FROM users", "SELECT price*qty FROM users"],
|
||||
])("applies snippet completion without deleting multiplication in %s", (document, expected) => {
|
||||
const from = document.indexOf("*");
|
||||
const replaceTo = completionReplacementTo({ from, to: from, nextCharacter: "*", replaceSelectWildcard: replaceSelectWildcardFor(document, from) });
|
||||
const view = new EditorView({ parent: window.document.createElement("div"), state: EditorState.create({ doc: document }) });
|
||||
const snippet = snippetCompletion("price", { label: "price", type: "function" });
|
||||
|
||||
expect(typeof snippet.apply).toBe("function");
|
||||
if (typeof snippet.apply === "function") snippet.apply(view, snippet, from, replaceTo);
|
||||
expect(view.state.doc.toString()).toBe(expected);
|
||||
view.destroy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["SELECT * FROM users", "SELECT price, qty FROM users"],
|
||||
["SELECT *qty FROM users", "SELECT price, qty*qty FROM users"],
|
||||
])("applies batch completion without deleting multiplication in %s", (document, expected) => {
|
||||
const from = document.indexOf("*");
|
||||
const replaceTo = batchColumnSelectionReplaceTo({ from, to: from, mode: "select", nextCharacter: "*", replaceSelectWildcard: replaceSelectWildcardFor(document, from) });
|
||||
const transaction = EditorState.create({ doc: document }).update({ changes: { from, to: replaceTo, insert: "price, qty" } });
|
||||
|
||||
expect(transaction.state.doc.toString()).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
buildSqlCompletionItemsFromContext,
|
||||
getPostgresSequenceLiteralCompletionContext,
|
||||
getSqlCompletionContext,
|
||||
prepareSqlCompletionReplacement,
|
||||
selectStarResultColumnsMatch,
|
||||
shouldAutoOpenSqlCompletion,
|
||||
} from "@/lib/sql/sqlCompletion";
|
||||
@@ -13,6 +14,42 @@ import { sqlCompletionContextFromSemantic } from "@/lib/sql/semantic/completion"
|
||||
import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
|
||||
import { originForSqlCompletionProvider, originForTypedSqlCompletionStart, shouldAllowSqlCompletionTrigger, type SqlCompletionTriggerFacts } from "@/lib/sql/sqlCompletionTriggerPolicy";
|
||||
|
||||
describe("SQL completion replacement", () => {
|
||||
const columnItem = { label: "price", type: "column" as const, apply: "price", boost: 0 };
|
||||
|
||||
it("marks a standalone SELECT wildcard for replacement", () => {
|
||||
const sql = "SELECT * FROM users";
|
||||
const cursor = sql.indexOf("*");
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
|
||||
expect(prepareSqlCompletionReplacement(sql, cursor, context, [columnItem]).items[0]).toMatchObject({ replaceSelectWildcard: true });
|
||||
});
|
||||
|
||||
it("does not mark the multiplication operator before an untyped right operand", () => {
|
||||
const sql = "SELECT *qty FROM users";
|
||||
const cursor = sql.indexOf("*");
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
|
||||
expect(prepareSqlCompletionReplacement(sql, cursor, context, [columnItem]).items[0]).not.toHaveProperty("replaceSelectWildcard");
|
||||
});
|
||||
|
||||
it("does not mark an expression operator after another projection operand", () => {
|
||||
const sql = "SELECT amount + * FROM users";
|
||||
const cursor = sql.indexOf("*");
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
|
||||
expect(prepareSqlCompletionReplacement(sql, cursor, context, [columnItem]).items[0]).not.toHaveProperty("replaceSelectWildcard");
|
||||
});
|
||||
|
||||
it("marks a standalone wildcard after a SELECT modifier", () => {
|
||||
const sql = "SELECT DISTINCT * FROM users";
|
||||
const cursor = sql.indexOf("*");
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
|
||||
expect(prepareSqlCompletionReplacement(sql, cursor, context, [columnItem]).items[0]).toMatchObject({ replaceSelectWildcard: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("sqlCompletion keyword snippets", () => {
|
||||
it("auto-opens and suggests SELECT when typing sel", () => {
|
||||
const sql = "sel";
|
||||
|
||||
@@ -8,14 +8,24 @@ export function shouldResolveSqlColumnCompletion(options: { suggestColumns: bool
|
||||
return options.suggestColumns && options.hasReferencedTables && (options.prefix.length > 0 || options.typedActivation || options.selectListColumnContext);
|
||||
}
|
||||
|
||||
export function shouldSwallowSelectStar(from: number, to: number, nextCharacter: string, replaceSelectWildcard = false): boolean {
|
||||
return replaceSelectWildcard && from === to && nextCharacter === "*";
|
||||
}
|
||||
|
||||
export function completionReplacementTo(options: { from: number; to: number; nextCharacter: string; replaceClosingQuote?: string; replaceSelectWildcard?: boolean }): number {
|
||||
const { from, to, nextCharacter, replaceClosingQuote, replaceSelectWildcard } = options;
|
||||
return replaceClosingQuote === nextCharacter || shouldSwallowSelectStar(from, to, nextCharacter, replaceSelectWildcard) ? to + 1 : to;
|
||||
}
|
||||
|
||||
/**
|
||||
* The INSERT batch action writes its own closing parenthesis before VALUES.
|
||||
* Consume an existing one (normally inserted by CodeMirror's auto-close
|
||||
* brackets extension) so the resulting statement has exactly one `)`.
|
||||
*/
|
||||
export function batchColumnSelectionReplaceTo(options: { to: number; mode: BatchColumnSelectionMode; nextCharacter: string; replaceClosingQuote?: string }): number {
|
||||
const { to, mode, nextCharacter, replaceClosingQuote } = options;
|
||||
return replaceClosingQuote === nextCharacter || (mode === "insert" && nextCharacter === ")") ? to + 1 : to;
|
||||
export function batchColumnSelectionReplaceTo(options: { from: number; to: number; mode: BatchColumnSelectionMode; nextCharacter: string; replaceClosingQuote?: string; replaceSelectWildcard?: boolean }): number {
|
||||
const { from, to, mode, nextCharacter, replaceClosingQuote, replaceSelectWildcard } = options;
|
||||
if (mode === "insert" && nextCharacter === ")") return to + 1;
|
||||
return completionReplacementTo({ from, to, nextCharacter, replaceClosingQuote, replaceSelectWildcard });
|
||||
}
|
||||
|
||||
export function batchColumnSelectionInsertReplacement(options: { document: string; to: number; columns: string; valuesKeyword: "values" | "VALUES"; valueCount: number }): { replaceTo: number; insert: string } {
|
||||
|
||||
@@ -1352,6 +1352,7 @@ export interface SqlCompletionItem {
|
||||
boost: number;
|
||||
exactMatch?: boolean;
|
||||
dedupeKey?: string;
|
||||
replaceSelectWildcard?: true;
|
||||
/** Enables the query editor's checkbox-based batch insertion for this column. */
|
||||
batchSelectionMode?: "select" | "insert";
|
||||
/** Qualifier to prepend to every batch-selected column after the first one. */
|
||||
@@ -1428,6 +1429,7 @@ export interface SqlCompletionContext {
|
||||
comparisonLeftColumn?: string;
|
||||
onStar: boolean;
|
||||
selectListColumnContext: boolean;
|
||||
selectListWildcardAfterCursor?: boolean;
|
||||
preferredKeywords: string[];
|
||||
updateTarget?: { table: string; schema?: string };
|
||||
deleteTarget?: { table: string; schema?: string };
|
||||
@@ -1448,22 +1450,47 @@ const SQL_COMPLETION_CLOSING_QUOTES: Readonly<Record<string, SqlCompletionClosin
|
||||
"[": "]",
|
||||
};
|
||||
|
||||
export function prepareSqlCompletionReplacement(sql: string, cursor: number, context: Pick<SqlCompletionContext, "prefix" | "qualifier" | "replacementRange">, items: SqlCompletionItem[]): { from: number; items: SqlCompletionItem[] } {
|
||||
const SELECT_WILDCARD_FOLLOWING_CLAUSES = new Set(["except", "fetch", "for", "from", "group", "having", "intersect", "into", "limit", "offset", "order", "qualify", "union", "where", "window"]);
|
||||
const SELECT_PROJECTION_MODIFIERS = new Set(["all", "distinct", "distinctrow"]);
|
||||
|
||||
function isStandaloneSelectWildcard(sql: string, cursor: number, selectListColumnContext: boolean, statementKind: SqlStatementKind, dialectId?: string): boolean {
|
||||
if (!selectListColumnContext || statementKind !== "select" || sql[cursor] !== "*") return false;
|
||||
|
||||
const tokens = tokenizeSqlSemantic(sql, dialectId).filter((token) => token.kind !== "comment");
|
||||
const starIndex = tokens.findIndex((token) => token.span.start === cursor && token.text === "*");
|
||||
if (starIndex < 0) return false;
|
||||
|
||||
const star = tokens[starIndex]!;
|
||||
let previousIndex = starIndex - 1;
|
||||
while (previousIndex >= 0 && tokens[previousIndex]!.depth === star.depth && tokens[previousIndex]!.kind === "word" && SELECT_PROJECTION_MODIFIERS.has(tokens[previousIndex]!.normalized)) previousIndex--;
|
||||
const previous = tokens[previousIndex];
|
||||
const startsProjection = previous?.depth === star.depth && (previous.normalized === "select" || previous.text === ",");
|
||||
if (!startsProjection) return false;
|
||||
|
||||
const next = tokens.slice(starIndex + 1).find((token) => token.depth <= star.depth);
|
||||
if (!next) return true;
|
||||
if (next.depth !== star.depth) return false;
|
||||
return next.text === "," || next.text === ";" || (next.kind === "word" && SELECT_WILDCARD_FOLLOWING_CLAUSES.has(next.normalized));
|
||||
}
|
||||
|
||||
export function prepareSqlCompletionReplacement(sql: string, cursor: number, context: Pick<SqlCompletionContext, "prefix" | "qualifier" | "replacementRange" | "selectListWildcardAfterCursor">, items: SqlCompletionItem[]): { from: number; items: SqlCompletionItem[] } {
|
||||
const range = context.replacementRange;
|
||||
const from = range && range.start >= 0 && range.start <= cursor && range.end === cursor ? range.start : cursor - context.prefix.length;
|
||||
const closingQuote = from < cursor ? SQL_COMPLETION_CLOSING_QUOTES[sql[from] ?? ""] : undefined;
|
||||
if (!closingQuote) return { from, items };
|
||||
const replaceClosingQuote = sql[cursor] === closingQuote ? closingQuote : undefined;
|
||||
const replaceSelectWildcard = from === cursor && context.selectListWildcardAfterCursor === true;
|
||||
if (!closingQuote && !replaceSelectWildcard) return { from, items };
|
||||
return {
|
||||
from,
|
||||
items: items.map((item) => {
|
||||
let prepared = item;
|
||||
const apply = item.apply ?? item.label;
|
||||
if (item.type === "column" && !(apply.startsWith(sql[from] ?? "") && apply.endsWith(closingQuote)) && (context.qualifier || !apply.includes("."))) {
|
||||
if (closingQuote && item.type === "column" && !(apply.startsWith(sql[from] ?? "") && apply.endsWith(closingQuote)) && (context.qualifier || !apply.includes("."))) {
|
||||
const escaped = apply.replaceAll(closingQuote, closingQuote + closingQuote);
|
||||
prepared = { ...prepared, apply: `${sql[from]}${escaped}${closingQuote}` };
|
||||
}
|
||||
return replaceClosingQuote && !prepared.replaceClosingQuote ? { ...prepared, replaceClosingQuote } : prepared;
|
||||
if (replaceClosingQuote && !prepared.replaceClosingQuote) prepared = { ...prepared, replaceClosingQuote };
|
||||
return replaceSelectWildcard && !prepared.replaceSelectWildcard ? { ...prepared, replaceSelectWildcard: true } : prepared;
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -2435,6 +2462,7 @@ export function getSqlCompletionContext(sql: string, cursor: number, options: Sq
|
||||
const suggestRoutines = inCallRoutineContext || oracleTableFunctionContext || inPotentialPackageMemberContext || (!exclusiveTableSuggestions && !exclusiveColumnSuggestions && !insertInfo && !updateInfo?.inSetClause && prefix.length >= 2);
|
||||
|
||||
const statementKind = detectStatementKind(beforeCursor || fullStatement);
|
||||
const selectListWildcardAfterCursor = isStandaloneSelectWildcard(rawStatement, cursor - statementSpan.start, selectListColumnContext, statementKind, resolveSqlDialectId(options));
|
||||
const dataTypeContext = isCreateTableColumnTypeContext(beforeToken, options.databaseType);
|
||||
const preferredValueKeywords = sqlServerDatepartCompletionValues(beforeCursor, options.databaseType);
|
||||
const preferredKeywords = qualifier ? [] : preferredKeywordsForCompletion(beforeCursor, beforeToken, selectListColumnContext, exclusiveTableSuggestions, updateInfo, deleteInfo, options.databaseType);
|
||||
@@ -2480,6 +2508,7 @@ export function getSqlCompletionContext(sql: string, cursor: number, options: Sq
|
||||
comparisonLeftColumn: detectComparisonLeftColumn(beforeCursor),
|
||||
onStar: detectOnStar(beforeCursor),
|
||||
selectListColumnContext,
|
||||
selectListWildcardAfterCursor,
|
||||
preferredKeywords,
|
||||
updateTarget: updateInfo?.target,
|
||||
deleteTarget: deleteInfo?.target,
|
||||
|
||||
Reference in new issue
Block a user