feat(editor): include column comments in batch column insert

This commit is contained in:
lxk955
2026-10-10 09:51:50 +08:00
committed by GitHub
parent 4f0ce82f4a
commit 68d05ea27c
6 changed files with 128 additions and 11 deletions
@@ -28,7 +28,7 @@ afterEach(() => {
toast.mockReset();
});
function createHarness(doc = "SELECT FROM users", mode: "select" | "insert" = "select") {
function createHarness(doc = "SELECT FROM users", mode: "select" | "insert" = "select", customItems?: SqlCompletionItem[], customFrom?: number) {
const completionComp = new Compartment();
const parent = document.createElement("div");
document.body.append(parent);
@@ -46,8 +46,8 @@ function createHarness(doc = "SELECT FROM users", mode: "select" | "insert" = "
};
const accepted = vi.fn();
const batch = useQueryEditorBatchSelection({ view, runtime, settingsStore: { editorSettings: { sqlFormatter: { keywordCase: "upper" } } } as Options["settingsStore"], markCompletionAccepted: accepted });
const items: SqlCompletionItem[] = ["id", "name", "email"].map((label) => ({ label, apply: label, type: "column", batchSelectionMode: mode }));
const from = mode === "select" ? 7 : doc.length;
const items: SqlCompletionItem[] = customItems ?? ["id", "name", "email"].map((label) => ({ label, apply: label, type: "column", batchSelectionMode: mode }));
const from = customFrom ?? (mode === "select" ? 7 : doc.length);
const session = batch.prepareBatchColumnSelectionSession(items, doc, from, from)!;
batch.attach(currentView, parent);
cleanups.push(() => {
@@ -136,6 +136,42 @@ describe("QueryEditor batch selection ownership", () => {
expect(currentView.state.doc.toString()).toBe("SELECT id, email FROM users");
});
it("inserts fields with column comments formatted with indentation and comments (#11354)", () => {
const doc = "SELECT\n t.\nFROM users";
const from = "SELECT\n t.".length;
const items: SqlCompletionItem[] = [
{ label: "id", apply: "id", type: "column", batchSelectionMode: "select", batchSelectionQualifier: "t", comment: "编号" },
{ label: "name", apply: "name", type: "column", batchSelectionMode: "select", batchSelectionQualifier: "t", comment: "名称" },
];
const { batch, session, currentView } = createHarness(doc, "select", items, from);
batch.toggleAllBatchColumnSelection(currentView, session.key);
expect(batch.applySelectedBatchColumnSelection(currentView)).toBe(true);
expect(currentView.state.doc.toString()).toBe("SELECT\n t.id, -- 编号\n t.name -- 名称\nFROM users");
});
it("pushes trailing SQL to a new line when inserting commented columns on a single line (#11354)", () => {
const doc = "SELECT t. FROM users";
const from = "SELECT t.".length;
const items: SqlCompletionItem[] = [
{ label: "id", apply: "id", type: "column", batchSelectionMode: "select", batchSelectionQualifier: "t", comment: "编号" },
{ label: "name", apply: "name", type: "column", batchSelectionMode: "select", batchSelectionQualifier: "t", comment: "名称" },
];
const { batch, session, currentView } = createHarness(doc, "select", items, from);
batch.toggleAllBatchColumnSelection(currentView, session.key);
expect(batch.applySelectedBatchColumnSelection(currentView)).toBe(true);
expect(currentView.state.doc.toString()).toBe("SELECT t.id, -- 编号\n t.name -- 名称\nFROM users");
});
it("moves existing trailing comma before comment when inserting commented columns (#11354)", () => {
const doc = "SELECT\n t.,\n other\nFROM users";
const from = "SELECT\n t.".length;
const items: SqlCompletionItem[] = [{ label: "id", apply: "id", type: "column", batchSelectionMode: "select", batchSelectionQualifier: "t", comment: "编号" }];
const { batch, session, currentView } = createHarness(doc, "select", items, from);
batch.toggleAllBatchColumnSelection(currentView, session.key);
expect(batch.applySelectedBatchColumnSelection(currentView)).toBe(true);
expect(currentView.state.doc.toString()).toBe("SELECT\n t.id, -- 编号\n other\nFROM users");
});
it("rejects stale documents and pending completion without recording acceptance", () => {
const { batch, session, runtime, currentView, accepted } = createHarness();
batch.toggleAllBatchColumnSelection(currentView, session.key);
@@ -57,6 +57,7 @@ export function useQueryEditorBatchSelection(options: QueryEditorBatchSelectionO
interface BatchColumnSelectionCandidate {
key: string;
apply: string;
comment?: string;
}
interface BatchColumnSelectionSession {
@@ -269,7 +270,7 @@ export function useQueryEditorBatchSelection(options: QueryEditorBatchSelectionO
}
const mode = selectableItems[0]!.batchSelectionMode!;
const candidates = selectableItems.filter((item) => item.batchSelectionMode === mode).map((item) => ({ key: batchColumnSelectionCandidateKey(item), apply: item.batchSelectionApply ?? item.apply! }));
const candidates = selectableItems.filter((item) => item.batchSelectionMode === mode).map((item) => ({ key: batchColumnSelectionCandidateKey(item), apply: item.batchSelectionApply ?? item.apply!, comment: item.comment }));
const key = `${mode}\u0000${from}\u0000${to}\u0000${document}`;
if (!batchColumnSelectionSession || batchColumnSelectionSession.key !== key) {
batchColumnSelectionSession = {
@@ -538,11 +539,10 @@ export function useQueryEditorBatchSelection(options: QueryEditorBatchSelectionO
return;
}
const columns = batchColumnSelectionColumnList(
selected.map((candidate) => candidate.apply),
session.mode,
session.qualifier,
);
const line = view.state.doc.lineAt(from);
const lineIndent = line.text.match(/^\s*/)?.[0] ?? "";
const indent = lineIndent || " ";
let replaceTo = batchColumnSelectionReplaceTo({
from,
to,
@@ -551,7 +551,34 @@ export function useQueryEditorBatchSelection(options: QueryEditorBatchSelectionO
replaceClosingQuote: session.replaceClosingQuote,
replaceSelectWildcard: session.replaceSelectWildcard,
});
const hasComments = session.mode === "select" && selected.some((candidate) => !!candidate.comment?.trim());
let trailingComma = false;
let pushRestToNewLine = false;
if (hasComments) {
const lineAfterTo = view.state.doc.lineAt(replaceTo);
const restOfLine = lineAfterTo.text.slice(replaceTo - lineAfterTo.from);
const commaMatch = restOfLine.match(/^\s*,/);
if (commaMatch) {
trailingComma = true;
replaceTo += commaMatch[0].length;
}
const remainingAfterComma = commaMatch ? restOfLine.slice(commaMatch[0].length) : restOfLine;
if (remainingAfterComma.trim().length > 0) {
pushRestToNewLine = true;
const leadingSpaceMatch = remainingAfterComma.match(/^\s+/);
if (leadingSpaceMatch) {
replaceTo += leadingSpaceMatch[0].length;
}
}
}
const columns = batchColumnSelectionColumnList(selected, session.mode, session.qualifier, indent, { trailingComma });
let insert = columns;
if (pushRestToNewLine) {
insert = `${insert}\n${lineIndent}`;
}
if (session.mode === "insert") {
const replacement = batchColumnSelectionInsertReplacement({
document: view.state.doc.toString(),
@@ -22,6 +22,32 @@ describe("batchColumnSelectionColumnList", () => {
it("does not add a qualifier to INSERT target columns", () => {
expect(batchColumnSelectionColumnList(["id", "name"], "insert", "users")).toBe("id, name");
});
it("formats candidates with column comments in SELECT mode (#11354)", () => {
const candidates = [{ apply: "order_start_time", comment: "医嘱开始时间" }, { apply: "order_end_time", comment: "医嘱结束时间" }, { apply: "patient_id" }];
expect(batchColumnSelectionColumnList(candidates, "select", "t", " ")).toBe("order_start_time, -- 医嘱开始时间\n t.order_end_time, -- 医嘱结束时间\n t.patient_id");
});
it("normalizes newlines in column comments (#11354)", () => {
const candidates = [
{ apply: "status", comment: "状态:\n0-停用\r\n1-启用" },
{ apply: "created_at", comment: "创建时间" },
];
expect(batchColumnSelectionColumnList(candidates, "select", undefined, " ")).toBe("status, -- 状态: 0-停用 1-启用\n created_at -- 创建时间");
});
it("adds trailing comma when trailingComma option is enabled (#11354)", () => {
const candidates = [{ apply: "id", comment: "主键ID" }];
expect(batchColumnSelectionColumnList(candidates, "select", undefined, " ", { trailingComma: true })).toBe("id, -- 主键ID");
});
it("ignores candidate comments in INSERT mode (#11354)", () => {
const candidates = [
{ apply: "id", comment: "主键ID" },
{ apply: "name", comment: "名称" },
];
expect(batchColumnSelectionColumnList(candidates, "insert", "users")).toBe("id, name");
});
});
describe("batchColumnSelectionReplaceTo", () => {
@@ -68,6 +68,7 @@ describe("semantic SQL completion candidates", () => {
const nickname = items.find((item) => item.type === "column" && item.label === "nickname");
expect(nickname?.detail).toContain("-- 用户昵称");
expect(nickname?.comment).toBe("用户昵称");
});
it("ignores line-comment semicolons after a real statement boundary", () => {
@@ -1,7 +1,31 @@
export type BatchColumnSelectionMode = "select" | "insert";
export function batchColumnSelectionColumnList(candidates: string[], mode: BatchColumnSelectionMode, qualifier?: string): string {
return candidates.map((candidate, index) => (mode === "select" && qualifier && index > 0 ? `${qualifier}.${candidate}` : candidate)).join(", ");
export interface BatchColumnCandidate {
apply: string;
comment?: string;
}
export function batchColumnSelectionColumnList(candidates: Array<string | BatchColumnCandidate>, mode: BatchColumnSelectionMode, qualifier?: string, indent = " ", options?: { trailingComma?: boolean }): string {
const normalized = candidates.map((candidate) => (typeof candidate === "string" ? { apply: candidate } : candidate));
const hasComments = mode === "select" && normalized.some((candidate) => !!candidate.comment?.trim());
if (!hasComments) {
return normalized.map((candidate, index) => (mode === "select" && qualifier && index > 0 ? `${qualifier}.${candidate.apply}` : candidate.apply)).join(", ");
}
return normalized
.map((candidate, index) => {
const col = qualifier && index > 0 ? `${qualifier}.${candidate.apply}` : candidate.apply;
const isLast = index === normalized.length - 1;
const comment = candidate.comment?.replace(/[\r\n]+/g, " ").trim();
const linePrefix = index > 0 ? indent : "";
const shouldHaveComma = !isLast || options?.trailingComma;
if (comment) {
return shouldHaveComma ? `${linePrefix}${col}, -- ${comment}` : `${linePrefix}${col} -- ${comment}`;
}
return shouldHaveComma ? `${linePrefix}${col},` : `${linePrefix}${col}`;
})
.join("\n");
}
export function shouldResolveSqlColumnCompletion(options: { suggestColumns: boolean; hasReferencedTables: boolean; prefix: string; typedActivation: boolean; selectListColumnContext: boolean }): boolean {
@@ -1505,6 +1505,8 @@ export interface SqlCompletionItem {
batchSelectionApply?: string;
/** Qualifier to prepend to every batch-selected column after the first one. */
batchSelectionQualifier?: string;
/** Optional column comment for batch insertion documentation. */
comment?: string;
}
export function shouldChainSqlCompletionAfterAccept(item: { type?: string; apply?: string }): boolean {
@@ -5367,6 +5369,7 @@ function buildColumnItems(context: SqlCompletionContext, columnsByTable: Map<str
// document. Subsequent columns must use that same user-typed qualifier,
// not a referenced-table alias which may be different from it.
batchSelectionQualifier: batchSelectionMode === "select" && context.qualifier ? (context.qualifierParts ?? context.qualifier.split(".").filter(Boolean)).map((part) => quoteCompletionApplyIdentifier(part, dialect)).join(".") : undefined,
comment: column.comment?.trim() || undefined,
};
});
}