# textkit.ansi：默认 ANSI 文件编辑

`textkit.ansi` **操作的是文件，不是内存文本**。适用于已确认使用系统默认 ANSI 的 `.bat`、`.cmd`、VB6 `.bas` / `.cls` / `.frm` 等遗留文本文件，主要供 aardio Autos 按需调用。

- 磁盘：固定按 `codepage=0` 读写。
- 内存：调用参数、读取结果、补丁及诊断都是 UTF-8。
- 已经在内存中的文本：直接使用 `textkit`、`textkit.patch`、`textkit.editor`，无需本库。
- 不检测编码、不选择 GBK/BIG5、不修复乱码、不做不可表示字符审计。转换遵循默认 ANSI 规则。
- 不修改或扩展 Autos 的 UTF-8 工具；遇到 ANSI 文件时，通过 `execute_code` 调用本库并 `return` 结果表。

## 四个文件入口

所有入口只接受一个参数表，且必须包含 `path`；成功和失败都返回结果表，首先检查 `ok`。

| 入口 | 用途 | 参数 |
|---|---|---|
| `read` | 有界读取，给 AI 查看、定位 | `path, startLine, lineCount, maxBytes, lineNumbers` |
| `patch` | 精确 SEARCH/REPLACE 补丁 | `path, patches, backup, dryRun, previewMaxBytes` |
| `edit` | 行、锚点、区间及明确批量替换 | `path` 加现有 `edit_text_file` 的编辑字段；不接受 `compile` |
| `save` | 新建或明确全文覆盖 | `path, content, backup, dryRun` |

未知参数会被拒绝，不接受 `codepage`、`encoding`、`compile`。不自动运行 `.bat` 或编译 VB6 源码。

## 读取文件

```aardio
import textkit.ansi;
return textkit.ansi.read({
    path = "/legacy/build.bat";
    startLine = 1;
    lineCount = 80
});
```

默认 `startLine=1`、`lineCount=120`、`maxBytes=20000`、`lineNumbers=true`。

- `data` 是带行号的 UTF-8 展示文本；复制 SEARCH/旧片段时去掉行号。
- `lineNumbers=false` 去掉行号。**仍是展示视图，不是原文件的无损全文读取**：展示行之间用 LF 连接，且可能截断，不能直接拿来 `save` 覆盖文件。
- `startLine/endLine` 是实际展示的行区间；没有展示行时 `endLine=startLine-1`。
- `truncated` 表示后面还有未展示内容，或展示行被裁剪；`clipped` 专指字节预算裁剪。
- `maxBytes=0` 可仅获取元数据。字节预算包括行号和展示换行，截断不会切开 UTF-8 字符。
- 行号与 `editor.replaceLines` 一致：CRLF、LF、CR 都是换行；空文件有第 1 行，末尾换行后还有一个空逻辑行。
- 读取超范围起始行会返回 `line_out_of_range`。本库不是流式大文件编辑器：分页只限制输出，内部仍读取完整文件。

## 精确补丁

```aardio
import textkit.ansi;
var patches = "<<<<<<< SEARCH
echo 编译开始
=======
echo 开始构建
>>>>>>> REPLACE";

return textkit.ansi.patch({
    path = "/legacy/build.bat";
    patches = patches;
    dryRun = true
});
```

确认预览后，移除 `dryRun` 或设为 `false` 再提交。

与 `patch_text_file` 相同：

- 只传独占行的 `<<<<<<< SEARCH / ======= / >>>>>>> REPLACE` 块，不传路径行。
- 所有块匹配**同一份修改前原文**，后块不能搜索前块输出。
- SEARCH 非空、恰好匹配一处；区间不能重叠；任一块失败整单不写入。
- 字面、区分大小写和空白，仅 CRLF/LF/CR 差异等价；不 trim/dedent 旧片段、不做模糊回退。
- 插入时保留锚点并增加内容；删除时 REPLACE 留空。
- 内容含补丁标记行时改用 `edit`，不发明转义规则。

## 结构化编辑

```aardio
import textkit.ansi;
return textkit.ansi.edit({
    path = "/legacy/Module1.bas";
    operation = "replace";
    oldText = "旧提示";
    newText = "新提示";
    expectedCount = 2
});
```

与 `edit_text_file` 对齐的操作字段：

| operation | 定位字段 | 写入字段 |
|---|---|---|
| `replace` | `oldText` | `newText` |
| `insertBefore` / `insertAfter` | `anchor` | `newText` |
| `replaceBetween` | `beginAnchor, endAnchor` | `newText` |
| `deleteBetween` | `beginAnchor, endAnchor` | **不传** `newText` |
| `replaceLines` | `startLine`，可选 `endLine, expectedOldText` | `newText` |

- 默认唯一；`expectedCount=N` 是准确次数断言，不是替换上限或第 N 个。`replaceAll=true` 替换全部非重叠命中，仍可用 `expectedCount` 加断言。零命中始终失败。
- `newText` 必须显式指定；空字符串表示删除。插入不会自动添加换行。
- 区间默认保留锚点，`includeAnchors=true` 才一起替换。`anchor` 可代替 `beginAnchor`，不要传冲突的两者。嵌套或缺少结束锚点会拒绝。
- `replaceLines` 使用从 1 开始的闭区间整行，空字符串删除整行。新文本缺末尾 EOL 时保留原区间末尾 EOL；`expectedOldText` 可以带或不带该 EOL。
- `preserveEol` 默认 `true`：仅换行形式等价，新内容适配局部 EOL；设为 `false` 禁用等价与适配，但不取消整行替换的 EOL 衔接规则。
- 只传当前操作需要的字段，不填充无关参数。

## 新建或全文覆盖

```aardio
import textkit.ansi;
return textkit.ansi.save({
    path = "/legacy/build.bat";
    content = '@echo off\r\necho 开始编译\r\nexit /b 0\r\n'
});
```

`save` 不追加、不自动建父目录、不添加 BOM、不改变换行。创建批处理文件时建议显式用 `\r\n`（aardio 中控制字符写在单引号内）。空字符串表示创建空文件或清空已有文件。

已有源码的局部修改优先用 `patch/edit`，不要用全文覆盖代替精确编辑。

## 返回结果与安全边界

写接口常见字段：

- `ok`：操作是否成功；失败查看 `code/error/hint`。
- `written`：是否已经提交文件；dryRun 或无变化时为 `false`。
- `changed`：拟写入的 ANSI 字节与原文件是否不同；新建空文件也为 `true`。
- `dryRun`：是否仅验证和预览。
- `originalBytes/resultBytes`：实际磁盘 ANSI 字节数，不是 UTF-8 内存大小。
- `backupPath`：实际保留的原始字节备份路径。
- `count/changedCount/locations/preview`：`patch/edit` 引擎返回的匹配数、文本改动数、位置和预览。预览是转码落盘之前的 UTF-8 文本；不保证无法用默认 ANSI 表示的字符落盘后原样保留。

`patch/edit` 的 `previewMaxBytes` 默认 1024，范围 0～4096，0 禁止文本回显。预算按 UTF-8 计，只限制预览及诊断，不限制实际编辑数量。诊断候选仅供定位，不能用于模糊写入。

文件提交规则：

1. `backup` 默认 `true`；备份名称为 `<path>.ansi.bak.<时间戳>.<随机后缀>`，不会沿用 Autos 的 `.aa.bak.*` 名称。
2. `dryRun` 不产生备份或暂存文件；无字节变化不写入。`patch/edit` 无文本变化时直接保留原始字节，不重复转码回写。
3. 同目录独占创建暂存文件，写入、刷新并校验后再提交；不先截断原文件。
4. 提交前复查原文件，变化则取消。**这不是跨进程锁**，复查与替换之间仍有很短的竞争窗口；不承诺多文件事务或断电持久性。
5. 已有文件用 `ReplaceFileW` 替换，新文件用不覆盖现有目标的移动提交。`backup=false` 在提交期间仍保留恢复原件，成功后才删除；删不掉的备份仍通过 `backupPath` 返回。
6. 特殊提交故障可能返回 `recoveryRequired`、`backupPath`、`stagedPath`。此时停止继续编辑，先检查和恢复，不要盲目重试。

备份是原始字节。可用 `io.copy(backupPath, targetPath)` 原样恢复，**不要让 UTF-8 文本编辑/回滚工具读取 ANSI 备份**。恢复前自行确认目标是否已有新修改。

本库拒绝含 NUL 的文件和写入内容，不处理 VB6 `.frx` 等二进制资源。对于有效默认 ANSI 文本，编辑保留未修改部分的文本与换行；对于异常字节或特殊编码映射，不承诺整份转码后的逐字节可逆性。
