fix(edit): skipped notebook bom sniffing
Skipped binary BOM detection for notebook-backed hashline reads so virtual cell text is serialized without a leading U+FEFF marker.
This commit is contained in:
@@ -28,6 +28,7 @@ import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
|
||||
import { isInternalUrlPath } from "../../tools/path-utils";
|
||||
import { enforcePlanModeWrite, resolvePlanPath, targetsLocalSandbox } from "../../tools/plan-mode-guard";
|
||||
import { canonicalSnapshotKey } from "../file-snapshot-store";
|
||||
import { isNotebookPath } from "../notebook";
|
||||
import { readEditFileText, serializeEditFileText } from "../read-file";
|
||||
import type { LspBatchRequest } from "../renderer";
|
||||
|
||||
@@ -123,8 +124,9 @@ export class HashlineFilesystem extends Filesystem {
|
||||
return content;
|
||||
}
|
||||
|
||||
async readBinary(relativePath: string): Promise<Uint8Array> {
|
||||
async readBinary(relativePath: string): Promise<Uint8Array | undefined> {
|
||||
const absolutePath = this.resolveAbsolute(relativePath);
|
||||
if (isNotebookPath(absolutePath)) return undefined;
|
||||
try {
|
||||
return await fs.readFile(absolutePath);
|
||||
} catch (error) {
|
||||
|
||||
@@ -128,6 +128,39 @@ describe("hashline executor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("edits BOM-prefixed notebooks through the virtual cell text", async () => {
|
||||
await withTempDir(async tempDir => {
|
||||
const filePath = path.join(tempDir, "notebook.ipynb");
|
||||
const notebook = {
|
||||
cells: [
|
||||
{
|
||||
cell_type: "markdown",
|
||||
metadata: { keep: true },
|
||||
source: ["# Title\n"],
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
nbformat: 4,
|
||||
nbformat_minor: 5,
|
||||
};
|
||||
await Bun.write(
|
||||
filePath,
|
||||
new Uint8Array([0xef, 0xbb, 0xbf, ...new TextEncoder().encode(JSON.stringify(notebook))]),
|
||||
);
|
||||
const session = makeHashlineSession(tempDir);
|
||||
const editableText = "# %% [markdown] cell:0\n# Title\n";
|
||||
const sourceTag = recordFullSnapshot(getFileReadCache(session), filePath, editableText);
|
||||
const input = `${header("notebook.ipynb", sourceTag)}\n${sameLineRange(tag(2, "# Title"))}\n${repl("# Updated")}\n`;
|
||||
|
||||
await executeHashlineSingle(hashlineExecuteOptions(tempDir, input, undefined, session));
|
||||
|
||||
const updated = await Bun.file(filePath).json();
|
||||
expect(updated.cells).toHaveLength(1);
|
||||
expect(updated.cells[0].source).toEqual(["# Updated\n"]);
|
||||
expect(updated.cells[0].metadata).toEqual({ keep: true });
|
||||
});
|
||||
});
|
||||
|
||||
it("emits an actionable no-op diagnostic when the payload matches the file byte-for-byte", async () => {
|
||||
await withTempDir(async tempDir => {
|
||||
const filePath = path.join(tempDir, "a.ts");
|
||||
|
||||
@@ -65,8 +65,8 @@ export abstract class Filesystem {
|
||||
/** Read the file's full text content. Throw on missing file. */
|
||||
abstract readText(path: string): Promise<string>;
|
||||
|
||||
/** Read the file's raw bytes when text decoding may hide leading bytes such as a UTF-8 BOM. */
|
||||
readBinary?(path: string): Promise<Uint8Array>;
|
||||
/** Read raw bytes for backends whose text is a direct decode of persisted bytes. */
|
||||
readBinary?(path: string): Promise<Uint8Array | undefined>;
|
||||
|
||||
/** Validate that `path` is writable before a prepared batch starts committing. */
|
||||
async preflightWrite(_path: string, _options?: PreflightWriteOptions): Promise<void> {}
|
||||
|
||||
Reference in New Issue
Block a user