Skip to content

Commit 3e2ffb1

Browse files
committed
fix: preserve TOML arrays when persisting config
1 parent 6c01c13 commit 3e2ffb1

2 files changed

Lines changed: 73 additions & 4 deletions

File tree

src/core/config.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,13 @@ export function resolveConfiguredCliInput(
182182
};
183183
}
184184

185-
/** Serialize one scalar TOML value. */
186-
function serializeTomlValue(value: unknown) {
185+
/** Return whether an array contains only TOML table objects. */
186+
function isRecordArray(value: unknown): value is Array<Record<string, unknown>> {
187+
return Array.isArray(value) && value.every(isRecord);
188+
}
189+
190+
/** Serialize one inline TOML value, including scalar arrays. */
191+
function serializeTomlValue(value: unknown): string | undefined {
187192
if (typeof value === "string") {
188193
return JSON.stringify(value);
189194
}
@@ -192,14 +197,24 @@ function serializeTomlValue(value: unknown) {
192197
return String(value);
193198
}
194199

200+
if (Array.isArray(value) && !isRecordArray(value)) {
201+
const serializedItems = value.map((item) => serializeTomlValue(item));
202+
if (serializedItems.some((item) => item === undefined)) {
203+
return undefined;
204+
}
205+
206+
return `[${serializedItems.join(", ")}]`;
207+
}
208+
195209
return undefined;
196210
}
197211

198212
/** Render one TOML object recursively while keeping scalar keys above child tables. */
199-
function serializeTomlObject(source: Record<string, unknown>, sectionName?: string): string[] {
213+
function serializeTomlObject(source: Record<string, unknown>, sectionName?: string, arrayTable = false): string[] {
200214
const lines: string[] = [];
201215
const scalarEntries: Array<[string, string]> = [];
202216
const tableEntries: Array<[string, Record<string, unknown>]> = [];
217+
const arrayTableEntries: Array<[string, Array<Record<string, unknown>>]> = [];
203218

204219
for (const [key, value] of Object.entries(source)) {
205220
if (value === undefined) {
@@ -211,14 +226,19 @@ function serializeTomlObject(source: Record<string, unknown>, sectionName?: stri
211226
continue;
212227
}
213228

229+
if (isRecordArray(value)) {
230+
arrayTableEntries.push([key, value]);
231+
continue;
232+
}
233+
214234
const serialized = serializeTomlValue(value);
215235
if (serialized !== undefined) {
216236
scalarEntries.push([key, serialized]);
217237
}
218238
}
219239

220240
if (sectionName) {
221-
lines.push(`[${sectionName}]`);
241+
lines.push(`${arrayTable ? "[[" : "["}${sectionName}${arrayTable ? "]]" : "]"}`);
222242
}
223243

224244
for (const [key, value] of scalarEntries) {
@@ -233,6 +253,16 @@ function serializeTomlObject(source: Record<string, unknown>, sectionName?: stri
233253
lines.push(...serializeTomlObject(value, sectionName ? `${sectionName}.${key}` : key));
234254
}
235255

256+
for (const [key, values] of arrayTableEntries) {
257+
for (const value of values) {
258+
if (lines.length > 0) {
259+
lines.push("");
260+
}
261+
262+
lines.push(...serializeTomlObject(value, sectionName ? `${sectionName}.${key}` : key, true));
263+
}
264+
}
265+
236266
return lines;
237267
}
238268

test/config.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ describe("config resolution", () => {
114114
writeFileSync(
115115
configPath,
116116
[
117+
'recent_themes = ["paper", "midnight"]',
118+
'',
117119
'[pager]',
118120
'mode = "stack"',
119121
'',
@@ -138,10 +140,47 @@ describe("config resolution", () => {
138140
expect(parsed.wrap_lines).toBe(true);
139141
expect(parsed.hunk_headers).toBe(false);
140142
expect(parsed.agent_notes).toBe(true);
143+
expect(parsed.recent_themes).toEqual(["paper", "midnight"]);
141144
expect((parsed.pager as Record<string, unknown>).mode).toBe("stack");
142145
expect((parsed.git as Record<string, unknown>).wrap_lines).toBe(true);
143146
});
144147

148+
test("preserves TOML array-of-table sections when persisting view preferences", () => {
149+
const repo = createTempDir("hunk-config-repo-");
150+
const configPath = join(repo, ".hunk", "config.toml");
151+
152+
mkdirSync(join(repo, ".hunk"), { recursive: true });
153+
writeFileSync(
154+
configPath,
155+
[
156+
'theme = "paper"',
157+
'',
158+
'[[bookmarks]]',
159+
'path = "src/a.ts"',
160+
'hunk = 0',
161+
'',
162+
'[[bookmarks]]',
163+
'path = "src/b.ts"',
164+
'hunk = 2',
165+
].join('\n'),
166+
);
167+
168+
persistViewPreferences(configPath, {
169+
mode: "split",
170+
theme: "paper",
171+
showLineNumbers: true,
172+
wrapLines: false,
173+
showHunkHeaders: true,
174+
showAgentNotes: false,
175+
});
176+
177+
const parsed = Bun.TOML.parse(readFileSync(configPath, "utf8")) as Record<string, unknown>;
178+
expect(parsed.bookmarks).toEqual([
179+
{ path: "src/a.ts", hunk: 0 },
180+
{ path: "src/b.ts", hunk: 2 },
181+
]);
182+
});
183+
145184
test("loadAppBootstrap exposes resolved initial preferences to the UI", async () => {
146185
const home = createTempDir("hunk-config-home-");
147186
const repo = createTempDir("hunk-config-repo-");

0 commit comments

Comments
 (0)