- Fix isDir heuristic: use backend-provided isDirectory instead of filename-based guessing (breaks for Makefile, .github, etc.) - Add scroll-to-line on search result click via editorPendingGoToLine - Add Cmd+Shift+W shortcut for toggling line wrap - Rewrite Quick Open to fetch all project files from backend API instead of flattening the loaded tree (limited to expanded dirs) - Fix fd leak in atomicWrite: close file handle in finally block - Add a11y: role=dialog/alert, aria-modal, aria-label on modals - Add type=button on error state buttons
41 lines
1.2 KiB
TypeScript
41 lines
1.2 KiB
TypeScript
import { randomUUID } from 'crypto';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
/**
|
|
* Async atomic write: write tmp file then rename over target.
|
|
* Uses best-effort fsync and EXDEV fallback for safety.
|
|
*/
|
|
export async function atomicWriteAsync(targetPath: string, data: string): Promise<void> {
|
|
const dir = path.dirname(targetPath);
|
|
const tmpPath = path.join(dir, `.tmp.${randomUUID()}`);
|
|
|
|
try {
|
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
await fs.promises.writeFile(tmpPath, data, 'utf8');
|
|
|
|
let fd: fs.promises.FileHandle | null = null;
|
|
try {
|
|
fd = await fs.promises.open(tmpPath, 'r+');
|
|
await fd.sync();
|
|
} catch {
|
|
// fsync is best-effort.
|
|
} finally {
|
|
await fd?.close();
|
|
}
|
|
|
|
try {
|
|
await fs.promises.rename(tmpPath, targetPath);
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === 'EXDEV') {
|
|
await fs.promises.copyFile(tmpPath, targetPath);
|
|
await fs.promises.unlink(tmpPath).catch(() => undefined);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
await fs.promises.unlink(tmpPath).catch(() => undefined);
|
|
throw error;
|
|
}
|
|
}
|