From 62f2185f44c98edb8bedc3557e3ad080a4bc74c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Thu, 27 Aug 2026 08:38:50 +0800 Subject: [PATCH] feat(sql-formatter): add setting to preserve blank lines --- .../editor/SqlFormatterSettingsPanel.vue | 6 + .../src/components/editor/SqlPreviewPanel.vue | 4 +- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/ko.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + .../lib/__tests__/sql/sqlFormatter.spec.ts | 24 ++++ .../__tests__/sql/sqlFormatterConfig.spec.ts | 9 ++ .../settings/__tests__/settingsSearch.spec.ts | 1 + .../src/lib/settings/settingsSearch.ts | 1 + apps/desktop/src/lib/sql/sqlFormatter.ts | 107 +++++++++++++++++- .../desktop/src/lib/sql/sqlFormatterConfig.ts | 5 + 16 files changed, 158 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue b/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue index 385086bd4a..7997b02230 100644 --- a/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue +++ b/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue @@ -109,6 +109,7 @@ const sqlFormatterOptionLabelKeys: Record {
+
+ + +
+
diff --git a/apps/desktop/src/components/editor/SqlPreviewPanel.vue b/apps/desktop/src/components/editor/SqlPreviewPanel.vue index 1efc0a8701..833f657aec 100644 --- a/apps/desktop/src/components/editor/SqlPreviewPanel.vue +++ b/apps/desktop/src/components/editor/SqlPreviewPanel.vue @@ -9,6 +9,7 @@ import { useToast } from "@/composables/useToast"; import { copyToClipboard } from "@/lib/common/clipboard"; import { formatSqlText, type SqlFormatDialect } from "@/lib/sql/sqlFormatter"; import { createShikiSqlHighlighter, type SqlHighlighter } from "@/lib/sql/sqlHighlighter"; +import { useSettingsStore } from "@/stores/settingsStore"; const props = defineProps<{ sql: string; @@ -27,6 +28,7 @@ const emit = defineEmits<{ const { t } = useI18n(); const { isDark } = useTheme(); const { toast } = useToast(); +const settingsStore = useSettingsStore(); const isFormatted = ref(false); const formattedSql = ref(""); @@ -79,7 +81,7 @@ async function toggleFormat() { formatting.value = true; try { - formattedSql.value = await formatSqlText(props.sql, props.sqlFormatDialect ?? "generic"); + formattedSql.value = await formatSqlText(props.sql, props.sqlFormatDialect ?? "generic", settingsStore.editorSettings.sqlFormatter); isFormatted.value = true; await highlightSql(); } catch { diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index e21e32e99e..eb5378ba76 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -6057,6 +6057,7 @@ export default { sqlFormatterFromSameLine: "FROM with first table", sqlFormatterExpressionWidth: "Expression width", sqlFormatterLinesBetweenQueries: "Lines between queries", + sqlFormatterPreserveEmptyLines: "Preserve empty lines", sqlFormatterDenseOperators: "Dense operators", sqlFormatterNewlineBeforeSemicolon: "Semicolon on new line", sqlFormatterAdvancedOptions: "Advanced options", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 680e1c9145..6660505134 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -5786,6 +5786,7 @@ export default withEnglishFallback({ sqlFormatterFromSameLine: "FROM con la primera tabla", sqlFormatterExpressionWidth: "Ancho de expresión", sqlFormatterLinesBetweenQueries: "Líneas entre consultas", + sqlFormatterPreserveEmptyLines: "Conservar líneas vacías", sqlFormatterDenseOperators: "Operadores compactos", sqlFormatterNewlineBeforeSemicolon: "Punto y coma en nueva línea", sqlFormatterAdvancedOptions: "Opciones avanzadas", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index f2a0951e9d..b9d2fdc131 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -5786,6 +5786,7 @@ export default withEnglishFallback({ sqlFormatterFromSameLine: "FROM con la prima tabella", sqlFormatterExpressionWidth: "Larghezza espressione", sqlFormatterLinesBetweenQueries: "Righe tra query", + sqlFormatterPreserveEmptyLines: "Mantieni le righe vuote", sqlFormatterDenseOperators: "Operatori compatti", sqlFormatterNewlineBeforeSemicolon: "Punto e virgola su nuova riga", sqlFormatterAdvancedOptions: "Opzioni avanzate", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 5fe62eb138..f4b78bd6f8 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -5824,6 +5824,7 @@ export default withEnglishFallback({ sqlFormatterFromSameLine: "FROM と最初のテーブルを同じ行に配置", sqlFormatterExpressionWidth: "式の幅", sqlFormatterLinesBetweenQueries: "クエリ間の行数", + sqlFormatterPreserveEmptyLines: "空行を保持", sqlFormatterDenseOperators: "演算子を詰める", sqlFormatterNewlineBeforeSemicolon: "セミコロンを新しい行に", sqlFormatterAdvancedOptions: "詳細オプション", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 913af444d5..70d9506885 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -5465,6 +5465,7 @@ export default withEnglishFallback({ sqlFormatterFromSameLine: "FROM과 첫 번째 테이블을 같은 줄에 배치", sqlFormatterExpressionWidth: "표현식 너비", sqlFormatterLinesBetweenQueries: "쿼리 사이 줄 수", + sqlFormatterPreserveEmptyLines: "빈 줄 유지", sqlFormatterDenseOperators: "조밀한 연산자", sqlFormatterNewlineBeforeSemicolon: "세미콜론 새 줄에", sqlFormatterAdvancedOptions: "고급 옵션", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index be5c825b5a..26fd6c8c55 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -5788,6 +5788,7 @@ export default withEnglishFallback({ sqlFormatterFromSameLine: "FROM com a primeira tabela", sqlFormatterExpressionWidth: "Largura da expressão", sqlFormatterLinesBetweenQueries: "Linhas entre consultas", + sqlFormatterPreserveEmptyLines: "Preservar linhas em branco", sqlFormatterDenseOperators: "Operadores compactos", sqlFormatterNewlineBeforeSemicolon: "Ponto e vírgula em nova linha", sqlFormatterAdvancedOptions: "Opções avançadas", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 7bb71fcb14..4e001c333c 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -6041,6 +6041,7 @@ export default withEnglishFallback({ sqlFormatterFromSameLine: "FROM 与首个表同行", sqlFormatterExpressionWidth: "表达式宽度", sqlFormatterLinesBetweenQueries: "查询之间空行", + sqlFormatterPreserveEmptyLines: "保留原有空行", sqlFormatterDenseOperators: "紧凑运算符", sqlFormatterNewlineBeforeSemicolon: "分号单独换行", sqlFormatterAdvancedOptions: "高级选项", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 5fd1dec30d..a337f3ac31 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -5110,6 +5110,7 @@ export default withEnglishFallback({ sqlFormatterFromSameLine: "FROM 與第一個表同行", sqlFormatterExpressionWidth: "運算式寬度", sqlFormatterLinesBetweenQueries: "查詢之間空行", + sqlFormatterPreserveEmptyLines: "保留原有空行", sqlFormatterDenseOperators: "緊湊運算子", sqlFormatterNewlineBeforeSemicolon: "分號獨立換行", sqlFormatterAdvancedOptions: "進階選項", diff --git a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts index 9b2cccf574..d992682cf5 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts @@ -16,6 +16,30 @@ describe("sqlFormatter", () => { expect(canFormatSqlForDatabaseType("mysql")).toBe(true); }); + it("preserves source empty lines only when configured", async () => { + const sql = "-- tstt\n\nSELECT * FROM AUX_TABLE AS au LIMIT 100;"; + const consecutiveEmptyLines = "-- section\n\n\nSELECT 1;"; + const queriesWithEmptyLine = "SELECT 1;\n\nSELECT 2;"; + const queriesWithTwoEmptyLines = "SELECT 1;\n\n\nSELECT 2;"; + + const defaultFormatted = await formatSqlForEditing(sql, "generic"); + const preserved = await formatSqlForEditing(sql, "generic", { preserveEmptyLines: true }); + const preservedConsecutive = await formatSqlForEditing(consecutiveEmptyLines, "generic", { preserveEmptyLines: true }); + const preservedQueriesNoSpacing = await formatSqlForEditing(queriesWithEmptyLine, "generic", { preserveEmptyLines: true, linesBetweenQueries: 0 }); + const preservedQueries = await formatSqlForEditing(queriesWithEmptyLine, "generic", { preserveEmptyLines: true, linesBetweenQueries: 1 }); + const preservedQueriesWideSpacing = await formatSqlForEditing(queriesWithEmptyLine, "generic", { preserveEmptyLines: true, linesBetweenQueries: 2 }); + const preservedQueriesWithTwoEmptyLines = await formatSqlForEditing(queriesWithTwoEmptyLines, "generic", { preserveEmptyLines: true, linesBetweenQueries: 1 }); + + expect(defaultFormatted).toContain("-- tstt\nSELECT"); + expect(preserved).toContain("-- tstt\n\nSELECT"); + expect(preservedConsecutive).toContain("-- section\n\n\nSELECT"); + expect(preservedQueriesNoSpacing).toBe("SELECT\n 1;\n\nSELECT\n 2;"); + expect(preservedQueries).toBe("SELECT\n 1;\n\nSELECT\n 2;"); + expect(preservedQueriesWideSpacing).toBe("SELECT\n 1;\n\n\nSELECT\n 2;"); + expect(preservedQueriesWithTwoEmptyLines).toBe("SELECT\n 1;\n\n\nSELECT\n 2;"); + expect(preserved).not.toContain("__DBX_PRESERVE_EMPTY_LINE_"); + }); + it("maps PostgreSQL-compatible database types to the postgres formatter dialect", () => { for (const dbType of ["postgres", "kwdb", "gaussdb", "opengauss", "questdb", "kingbase", "highgo", "vastbase", "redshift"]) { expect(sqlFormatDialectForDbType(dbType)).toBe("postgres"); diff --git a/apps/desktop/src/lib/__tests__/sql/sqlFormatterConfig.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlFormatterConfig.spec.ts index 47e450e3a3..69f3a663e5 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlFormatterConfig.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlFormatterConfig.spec.ts @@ -63,4 +63,13 @@ describe("sqlFormatterConfig shortcut storage", () => { if (result.ok) expect(result.settings.fromClauseLayout).toBe("sameLine"); expect(JSON.parse(serializeSqlFormatterConfig({ fromClauseLayout: "sameLine" })).options.fromClauseLayout).toBe("sameLine"); }); + + it("defaults empty-line preservation off and serializes an explicit opt-in", () => { + const defaultConfig = JSON.parse(serializeSqlFormatterConfig({})); + const optIn = parseSqlFormatterConfig(JSON.stringify({ version: 1, formatter: "sql-formatter", options: { preserveEmptyLines: true } })); + + expect(defaultConfig.options.preserveEmptyLines).toBe(false); + expect(optIn).toEqual(expect.objectContaining({ ok: true })); + if (optIn.ok) expect(optIn.settings.preserveEmptyLines).toBe(true); + }); }); diff --git a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts index aac2033617..dde2436c27 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts @@ -239,6 +239,7 @@ describe("settings search", () => { { titleKey: "settings.sqlFormatterLogicalOperatorNewline", category: "formatter", targetId: "formatter" }, { titleKey: "settings.sqlFormatterExpressionWidth", category: "formatter", targetId: "formatter" }, { titleKey: "settings.sqlFormatterLinesBetweenQueries", category: "formatter", targetId: "formatter" }, + { titleKey: "settings.sqlFormatterPreserveEmptyLines", category: "formatter", targetId: "formatter" }, { titleKey: "settings.sqlFormatterDenseOperators", category: "formatter", targetId: "formatter" }, { titleKey: "settings.sqlFormatterNewlineBeforeSemicolon", category: "formatter", targetId: "formatter" }, { titleKey: "settings.sqlFormatterParamTypes", category: "formatter", targetId: "formatter" }, diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index 62cc6188eb..554ba8d072 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -142,6 +142,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "formatter-from-clause-layout", category: "formatter", titleKey: "settings.sqlFormatterFromClauseLayout", targetId: "formatter" }, { id: "formatter-expression-width", category: "formatter", titleKey: "settings.sqlFormatterExpressionWidth", targetId: "formatter" }, { id: "formatter-lines-between-queries", category: "formatter", titleKey: "settings.sqlFormatterLinesBetweenQueries", targetId: "formatter" }, + { id: "formatter-preserve-empty-lines", category: "formatter", titleKey: "settings.sqlFormatterPreserveEmptyLines", targetId: "formatter" }, { id: "formatter-dense-operators", category: "formatter", titleKey: "settings.sqlFormatterDenseOperators", targetId: "formatter" }, { id: "formatter-newline-before-semicolon", category: "formatter", titleKey: "settings.sqlFormatterNewlineBeforeSemicolon", targetId: "formatter" }, { id: "formatter-param-types", category: "formatter", titleKey: "settings.sqlFormatterParamTypes", targetId: "formatter" }, diff --git a/apps/desktop/src/lib/sql/sqlFormatter.ts b/apps/desktop/src/lib/sql/sqlFormatter.ts index 7876888f08..82b88782bd 100644 --- a/apps/desktop/src/lib/sql/sqlFormatter.ts +++ b/apps/desktop/src/lib/sql/sqlFormatter.ts @@ -118,6 +118,95 @@ function splitTrailingStandaloneDot(sql: string): { body: string; suffix: string }; } +interface EmptyLineProtection { + sql: string; + markers: string[]; +} + +/** + * Replaces blank source lines with unique line comments while the third-party + * formatter runs. sql-formatter intentionally normalizes whitespace, whereas + * the optional DBX setting needs to retain visual paragraph boundaries such as + * the blank line between a heading comment and a query. Line comments are + * valid at every SQL code boundary and are restored only after all DBX layout + * post-processing is complete. + */ +function protectEmptyLines(sql: string): EmptyLineProtection { + let namespace = 0; + while (sql.includes(`__DBX_PRESERVE_EMPTY_LINE_${namespace}_`)) namespace += 1; + + const markers: string[] = []; + const lineBreakPattern = /\r\n|\r|\n/g; + let output = ""; + let lineStart = 0; + let match: RegExpExecArray | null; + + while ((match = lineBreakPattern.exec(sql))) { + const line = sql.slice(lineStart, match.index); + if (line.trim().length === 0) { + const marker = `-- __DBX_PRESERVE_EMPTY_LINE_${namespace}_${markers.length}__`; + markers.push(marker); + output += marker; + } else { + output += line; + } + output += match[0]; + lineStart = lineBreakPattern.lastIndex; + } + + // A final non-terminated blank line is still a user-authored empty line. + // Do not manufacture one after a trailing line break, though: that would + // turn the normal EOF sentinel into an additional preserved blank line. + const finalLine = sql.slice(lineStart); + if (finalLine.length > 0 && finalLine.trim().length === 0) { + const marker = `-- __DBX_PRESERVE_EMPTY_LINE_${namespace}_${markers.length}__`; + markers.push(marker); + output += marker; + } else { + output += finalLine; + } + + return { sql: markers.length > 0 ? output : sql, markers }; +} + +function restoreProtectedEmptyLines(sql: string, markers: readonly string[]): string { + if (markers.length === 0) return sql; + + const markerSet = new Set(markers); + const lines = sql.split(/\r\n|\r|\n/); + for (let index = 0; index < lines.length; ) { + if (!markerSet.has(lines[index].trim())) { + index += 1; + continue; + } + + let runEnd = index; + while (runEnd < lines.length && markerSet.has(lines[runEnd].trim())) runEnd += 1; + const markerCount = runEnd - index; + + // sql-formatter adds `linesBetweenQueries` blank lines before a marker + // group placed between statements. Those lines did not exist in the + // source—the marker group itself represents the source blank lines—so + // remove at most one generated line per original blank line. This keeps + // the larger of the user-authored spacing and the formatter's configured + // query spacing instead of adding the two counts together. + let removed = 0; + while (removed < markerCount && index > 0 && lines[index - 1].trim().length === 0) { + lines.splice(index - 1, 1); + index -= 1; + runEnd -= 1; + removed += 1; + } + + for (let markerIndex = index; markerIndex < runEnd; markerIndex += 1) { + lines[markerIndex] = ""; + } + index = runEnd; + } + + return lines.join("\n"); +} + const DUCKDB_ALIAS_IDENTIFIER_START_RE = /[\p{L}_]/u; const DUCKDB_ALIAS_IDENTIFIER_CHAR_RE = /[\p{L}\p{N}_]/u; @@ -253,7 +342,9 @@ export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "ge const normalizedSettings = normalizeSqlFormatterSettings(settings); const options = sqlFormatterOptions(normalizedSettings); const language = formatterLanguage(dialect); - const protectedInput = dialect === "duckdb" ? protectDuckDbPrefixAliasSeparators(sql) : { sql, marker: null }; + const emptyLineProtection = normalizedSettings.preserveEmptyLines ? protectEmptyLines(sql) : null; + const sqlWithProtectedEmptyLines = emptyLineProtection?.sql ?? sql; + const protectedInput = dialect === "duckdb" ? protectDuckDbPrefixAliasSeparators(sqlWithProtectedEmptyLines) : { sql: sqlWithProtectedEmptyLines, marker: null }; const formatterOptions = language === "postgresql" ? { @@ -290,16 +381,20 @@ export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "ge } }; + const finalizeFormattedSql = (formatted: string): string => { + const restoredDuckDbAliases = restoreDuckDbPrefixAliasSeparators(formatted, protectedInput.marker); + const laidOut = applySqlFormatterLayout(restoredDuckDbAliases, normalizedSettings, dialect); + return emptyLineProtection ? restoreProtectedEmptyLines(laidOut, emptyLineProtection.markers) : laidOut; + }; + try { - const formatted = restoreDuckDbPrefixAliasSeparators(formatWithFallback(protectedInput.sql), protectedInput.marker); - return applySqlFormatterLayout(formatted, normalizedSettings, dialect); + return finalizeFormattedSql(formatWithFallback(protectedInput.sql)); } catch (err) { - const trailingDot = dialect === "dameng" ? splitTrailingStandaloneDot(sql) : null; + const trailingDot = dialect === "dameng" ? splitTrailingStandaloneDot(protectedInput.sql) : null; if (!trailingDot) throw err; try { - const formatted = applySqlFormatterLayout(formatWithFallback(trailingDot.body), normalizedSettings, dialect); - return `${formatted}${trailingDot.suffix}`; + return finalizeFormattedSql(`${formatWithFallback(trailingDot.body)}${trailingDot.suffix}`); } catch { throw err; } diff --git a/apps/desktop/src/lib/sql/sqlFormatterConfig.ts b/apps/desktop/src/lib/sql/sqlFormatterConfig.ts index d0ef2ef0c9..b5204f71d6 100644 --- a/apps/desktop/src/lib/sql/sqlFormatterConfig.ts +++ b/apps/desktop/src/lib/sql/sqlFormatterConfig.ts @@ -49,6 +49,7 @@ export interface SqlFormatterOptionSettings { fromClauseLayout: SqlFormatterFromClauseLayout; expressionWidth: SqlFormatterExpressionWidth; linesBetweenQueries: SqlFormatterLinesBetweenQueries; + preserveEmptyLines: boolean; denseOperators: boolean; newlineBeforeSemicolon: boolean; paramTypes: SqlFormatterParamTypes | null; @@ -76,6 +77,7 @@ export const DEFAULT_SQL_FORMATTER_SETTINGS: SqlFormatterSettings = { fromClauseLayout: "newLine", expressionWidth: 50, linesBetweenQueries: 1, + preserveEmptyLines: false, denseOperators: false, newlineBeforeSemicolon: false, paramTypes: null, @@ -93,6 +95,7 @@ const SQL_FORMATTER_OPTION_KEYS = new Set([ "fromClauseLayout", "expressionWidth", "linesBetweenQueries", + "preserveEmptyLines", "denseOperators", "newlineBeforeSemicolon", "paramTypes", @@ -110,6 +113,7 @@ const SQL_FORMATTER_OPTION_VALIDATORS: Record isStringChoice(value, FROM_CLAUSE_LAYOUT_VALUES), expressionWidth: (value) => isNumberChoice(value, EXPRESSION_WIDTH_VALUES), linesBetweenQueries: (value) => isNumberChoice(value, LINES_BETWEEN_QUERIES_VALUES), + preserveEmptyLines: (value) => typeof value === "boolean", denseOperators: (value) => typeof value === "boolean", newlineBeforeSemicolon: (value) => typeof value === "boolean", paramTypes: isSqlFormatterParamTypes, @@ -191,6 +195,7 @@ export function sqlFormatterOptionSettings(settings: unknown): SqlFormatterOptio fromClauseLayout: normalizeChoice(input.fromClauseLayout, FROM_CLAUSE_LAYOUT_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.fromClauseLayout), expressionWidth: normalizeNumberChoice(input.expressionWidth, EXPRESSION_WIDTH_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.expressionWidth), linesBetweenQueries: normalizeNumberChoice(input.linesBetweenQueries, LINES_BETWEEN_QUERIES_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.linesBetweenQueries), + preserveEmptyLines: normalizeBoolean(input.preserveEmptyLines, DEFAULT_SQL_FORMATTER_SETTINGS.preserveEmptyLines), denseOperators: normalizeBoolean(input.denseOperators, DEFAULT_SQL_FORMATTER_SETTINGS.denseOperators), newlineBeforeSemicolon: normalizeBoolean(input.newlineBeforeSemicolon, DEFAULT_SQL_FORMATTER_SETTINGS.newlineBeforeSemicolon), paramTypes: normalizeParamTypes(input.paramTypes, DEFAULT_SQL_FORMATTER_SETTINGS.paramTypes),