feat: added block replace syntax with block edits in hashline parser

- Added `replace block N:` syntax in grammar/parser/tokenizer/types, emitting `kind: "block"` edits with empty-body checks.
- Added block-resolution in hashline apply/recovery, wiring `BlockResolver` to resolve edits with throw/drop unresolved behavior.
- Added native `blockRangeAt` support and exported block range/options types through pi-ast, pi-natives, and JS bindings.
- Added coding-agent resolver wiring plus internal visibility/exports updates and new parser, patcher, and setup-wizard tests.
This commit is contained in:
can1357
2026-05-31 00:32:25 +02:00
parent e7b7c3cc31
commit cb00a07203
31 changed files with 1106 additions and 35 deletions
@@ -0,0 +1,14 @@
/**
* Tree-sitter-backed {@link BlockResolver} for the hashline `replace block N:`
* operator. Bridges the pure hashline seam to the native `blockRangeAt`
* primitive in `@oh-my-pi/pi-natives`, which infers the language from the file
* path and returns the 1-indexed line span of the syntactic block beginning on
* the requested line (or `null` when none can be resolved).
*/
import type { BlockResolver } from "@oh-my-pi/hashline";
import { blockRangeAt } from "@oh-my-pi/pi-natives";
export const nativeBlockResolver: BlockResolver = ({ path, text, line }) => {
const range = blockRangeAt({ code: text, path, line });
return range ? { start: range.startLine, end: range.endLine } : null;
};
@@ -22,6 +22,7 @@ import {
import { resolveToCwd } from "../../tools/path-utils";
import { generateDiffString } from "../diff";
import { readEditFileText } from "../read-file";
import { nativeBlockResolver } from "./block-resolver";
export interface HashlineDiffOptions {
/**
@@ -74,7 +75,9 @@ export async function computeHashlineSectionDiff(
const normalized = normalizeToLF(content);
const hashError = validateSectionHash(section, absolutePath, normalized, snapshots);
if (hashError) return { error: hashError };
const result = options.streaming ? section.applyPartialTo(normalized) : section.applyTo(normalized);
const result = options.streaming
? section.applyPartialTo(normalized, nativeBlockResolver)
: section.applyTo(normalized, nativeBlockResolver);
if (normalized === result.text) return { error: `No changes would be made to ${section.path}.` };
return generateDiffString(normalized, result.text);
} catch (err) {
@@ -25,6 +25,7 @@ import { outputMeta } from "../../tools/output-meta";
import { generateDiffString } from "../diff";
import { getFileSnapshotStore } from "../file-snapshot-store";
import type { EditToolDetails, EditToolPerFileResult, LspBatchRequest } from "../renderer";
import { nativeBlockResolver } from "./block-resolver";
import { HashlineFilesystem } from "./filesystem";
import { type HashlineParams, hashlineEditParamsSchema } from "./params";
@@ -133,7 +134,7 @@ export async function executeHashlineSingle(
batchRequest: options.batchRequest,
});
const snapshots = getFileSnapshotStore(options.session);
const patcher = new Patcher({ fs, snapshots });
const patcher = new Patcher({ fs, snapshots, blockResolver: nativeBlockResolver });
// Single-section fast path: prepare, commit, render.
if (patch.sections.length === 1) {
@@ -1,3 +1,4 @@
export * from "./block-resolver";
export * from "./diff";
export * from "./execute";
export * from "./filesystem";