From 7a24227e56cd173fd668d25b0881972762c48491 Mon Sep 17 00:00:00 2001 From: OOMEcho <228389787@qq.com> Date: Tue, 29 Sep 2026 17:31:11 +0800 Subject: [PATCH] fix(sqlserver): complete routine parameters from the typed header --- .../sql/sqlCompletion.context.spec.ts | 30 +++++++++++ apps/desktop/src/lib/sql/sqlCompletion.ts | 53 +++++++++++++++++-- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts index e3daa75a0f..484ea5bd5a 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts @@ -143,6 +143,36 @@ BEGIN expect(completionItems(sql)).toEqual(expect.arrayContaining([expect.objectContaining({ label: "@customer_id", type: "variable" }), expect.objectContaining({ label: "@customer_name", type: "variable" })])); }); + it("suggests procedure parameters in the current routine body", () => { + const sql = `CREATE OR ALTER PROCEDURE dbo.find_customer + @customer_id AS INT, + @customer_name NVARCHAR(100) = NULL OUTPUT +AS +BEGIN + SELECT @customer_`; + + expect(completionItems(sql)).toEqual(expect.arrayContaining([expect.objectContaining({ label: "@customer_id", type: "variable" }), expect.objectContaining({ label: "@customer_name", type: "variable" })])); + }); + + it("suggests parenthesized function parameters without collecting body references", () => { + const sql = `ALTER FUNCTION dbo.customer_label( + @customer_id INT, + @separator NVARCHAR(10) = N'-' +) +RETURNS NVARCHAR(200) +AS +BEGIN + DECLARE @label NVARCHAR(200); + SET @phantom = 1; + SELECT @`; + const variables = completionItems(sql) + .filter((item) => item.type === "variable") + .map((item) => item.label); + + expect(variables).toEqual(expect.arrayContaining(["@customer_id", "@separator", "@label"])); + expect(variables).not.toContain("@phantom"); + }); + it("ignores later declarations and declarations inside comments or strings", () => { const sql = `DECLARE @visible INT; -- DECLARE @commented INT; diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index 8b46f62326..881be5e638 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -2651,6 +2651,15 @@ function collectSqlServerLocalVariables(sql: string, cursor: number, options: Sq const tokens = tokenizeSqlSemantic(batchSql, "sqlserver").filter((token) => token.kind !== "comment" && token.kind !== "string"); const variables: string[] = []; const seen = new Set(); + const addVariable = (variable: string) => { + if (!/^@[A-Za-z_@$#][\w@$#]*$/u.test(variable) || variable.startsWith("@@")) return; + const key = variable.toLowerCase(); + if (seen.has(key)) return; + seen.add(key); + variables.push(variable); + }; + + collectSqlServerRoutineParameters(tokens, addVariable); for (let index = 0; index < tokens.length; index += 1) { const declare = tokens[index]; @@ -2668,11 +2677,7 @@ function collectSqlServerLocalVariables(sql: string, cursor: number, options: Sq if (token.kind === "word" && /^@[A-Za-z_@$#][\w@$#]*$/u.test(token.text) && !token.text.startsWith("@@")) { const next = tokens[declarationIndex + 1]; if (next && next.text !== "," && next.text !== ";") { - const key = token.text.toLowerCase(); - if (!seen.has(key)) { - seen.add(key); - variables.push(token.text); - } + addVariable(token.text); } expectVariable = false; } @@ -2685,6 +2690,44 @@ function collectSqlServerLocalVariables(sql: string, cursor: number, options: Sq return variables; } +function collectSqlServerRoutineParameters(tokens: ReturnType, addVariable: (variable: string) => void) { + for (let index = 0; index < tokens.length; index += 1) { + const start = tokens[index]; + if (start?.kind !== "word" || (start.normalized !== "create" && start.normalized !== "alter")) continue; + + let routineIndex = index + 1; + if (start.normalized === "create" && tokens[routineIndex]?.kind === "word" && tokens[routineIndex]?.normalized === "or" && tokens[routineIndex + 1]?.kind === "word" && tokens[routineIndex + 1]?.normalized === "alter") { + routineIndex += 2; + } + const routine = tokens[routineIndex]; + if (routine?.kind !== "word" || !["proc", "procedure", "function"].includes(routine.normalized)) continue; + + if (routine.normalized === "function") { + const openingIndex = tokens.findIndex((token, tokenIndex) => tokenIndex > routineIndex && token.kind === "punctuation" && token.text === "(" && token.depth === routine.depth); + if (openingIndex < 0) continue; + const openingDepth = tokens[openingIndex]!.depth; + for (let parameterIndex = openingIndex + 1; parameterIndex < tokens.length; parameterIndex += 1) { + const token = tokens[parameterIndex]!; + if (token.kind === "punctuation" && token.text === ")" && token.depth === openingDepth) break; + if (token.kind === "word" && token.depth === openingDepth + 1) addVariable(token.text); + } + continue; + } + + for (let parameterIndex = routineIndex + 1; parameterIndex < tokens.length; parameterIndex += 1) { + const token = tokens[parameterIndex]!; + if (token.depth < routine.depth) break; + if (token.depth !== routine.depth || token.kind !== "word") continue; + if (token.normalized === "as") { + const previous = tokens[parameterIndex - 1]; + if (previous?.kind === "word" && previous.text.startsWith("@") && !previous.text.startsWith("@@")) continue; + break; + } + addVariable(token.text); + } + } +} + function sqlTokenStartsLine(sql: string, position: number): boolean { const lineStart = sql.lastIndexOf("\n", Math.max(0, position - 1)) + 1; return sql.slice(lineStart, position).trim().length === 0;