@bubblebrain-ai/bubble 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/agent/tool-intent.js +0 -1
  2. package/dist/agent.d.ts +1 -0
  3. package/dist/agent.js +54 -21
  4. package/dist/context/prune.d.ts +1 -0
  5. package/dist/context/prune.js +32 -0
  6. package/dist/feishu/agent-host/run-driver.js +2 -2
  7. package/dist/feishu/card/run-state.js +1 -0
  8. package/dist/main.js +11 -9
  9. package/dist/model-pricing.js +2 -1
  10. package/dist/model-selection.d.ts +7 -0
  11. package/dist/model-selection.js +9 -0
  12. package/dist/network/chatgpt-transport.d.ts +1 -0
  13. package/dist/network/chatgpt-transport.js +123 -16
  14. package/dist/orchestrator/default-hooks.js +1 -1
  15. package/dist/prompt/environment.js +1 -3
  16. package/dist/prompt/runtime.js +1 -1
  17. package/dist/provider-anthropic.d.ts +15 -3
  18. package/dist/provider-anthropic.js +55 -2
  19. package/dist/provider-openai-codex.js +3 -1
  20. package/dist/provider.js +1 -1
  21. package/dist/session-title.js +3 -6
  22. package/dist/slash-commands/commands.js +4 -0
  23. package/dist/stats/usage.d.ts +1 -0
  24. package/dist/stats/usage.js +28 -3
  25. package/dist/tools/edit.js +75 -1
  26. package/dist/tools/glob.js +77 -12
  27. package/dist/tools/index.d.ts +1 -1
  28. package/dist/tools/index.js +1 -3
  29. package/dist/tools/prompt-metadata.d.ts +3 -0
  30. package/dist/tools/prompt-metadata.js +17 -0
  31. package/dist/tools/write.js +14 -0
  32. package/dist/tui/paste-placeholder.d.ts +10 -0
  33. package/dist/tui/paste-placeholder.js +45 -0
  34. package/dist/tui/run.js +23 -0
  35. package/dist/tui-ink/app.js +2 -0
  36. package/dist/tui-ink/input-box.d.ts +1 -8
  37. package/dist/tui-ink/input-box.js +8 -38
  38. package/dist/tui-opentui/app.js +2 -0
  39. package/dist/tui-opentui/input-box.d.ts +1 -3
  40. package/dist/tui-opentui/input-box.js +17 -26
  41. package/dist/types.d.ts +9 -0
  42. package/package.json +7 -3
  43. package/dist/tools/apply-patch.d.ts +0 -9
  44. package/dist/tools/apply-patch.js +0 -330
  45. package/dist/tools/patch-apply.d.ts +0 -41
  46. package/dist/tools/patch-apply.js +0 -312
@@ -1,330 +0,0 @@
1
- import { constants } from "node:fs";
2
- import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
4
- import { createTwoFilesPatch } from "diff";
5
- import { gateToolAction } from "../approval/tool-helper.js";
6
- import { countUnifiedDiffChanges } from "../diff-stats.js";
7
- import { formatDiagnosticBlocks } from "../lsp/index.js";
8
- import { isWithinWorkspace } from "./file-state.js";
9
- import { withFileMutationQueues } from "./file-mutation-queue.js";
10
- import { resolveToolPath } from "./path-utils.js";
11
- import { applyPatchChunks, buildAddedFileContent, parseApplyPatch, PatchApplyError, } from "./patch-apply.js";
12
- export function createApplyPatchTool(cwd, approval, lsp, fileState) {
13
- return {
14
- name: "apply_patch",
15
- effect: "write_patch",
16
- requiresApproval: true,
17
- description: "Apply a structured patch for multi-file or larger changes. Use edit for small targeted replacements, write for full-file generation, and apply_patch for related adds/updates/deletes/moves.",
18
- parameters: {
19
- type: "object",
20
- properties: {
21
- patch: {
22
- type: "string",
23
- description: "Patch text using *** Begin Patch / *** Add File / *** Update File / *** Delete File / *** End Patch markers.",
24
- },
25
- patchText: {
26
- type: "string",
27
- description: "Alias for patch; accepted for compatibility with other coding agents.",
28
- },
29
- },
30
- },
31
- async execute(args) {
32
- const patchText = typeof args.patch === "string"
33
- ? args.patch
34
- : typeof args.patchText === "string"
35
- ? args.patchText
36
- : "";
37
- if (!patchText.trim()) {
38
- return {
39
- content: "Error: apply_patch requires a non-empty patch string.",
40
- isError: true,
41
- status: "blocked",
42
- };
43
- }
44
- let operations;
45
- try {
46
- operations = parseApplyPatch(patchText).operations;
47
- }
48
- catch (err) {
49
- if (err instanceof PatchApplyError) {
50
- return { content: err.message, isError: true, status: err.status };
51
- }
52
- throw err;
53
- }
54
- const lockPaths = collectOperationPaths(cwd, operations);
55
- return withFileMutationQueues(lockPaths, async () => {
56
- let plan;
57
- try {
58
- plan = await buildPatchPlan(cwd, operations);
59
- }
60
- catch (err) {
61
- if (err instanceof PatchApplyError) {
62
- return { content: err.message, isError: true, status: err.status };
63
- }
64
- throw err;
65
- }
66
- const gate = await gateToolAction(approval, {
67
- type: "patch",
68
- path: summarizePaths(plan.paths),
69
- paths: plan.paths,
70
- files: plan.changes.map((change) => ({ path: change.path, kind: change.kind })),
71
- diff: plan.diff,
72
- });
73
- if (!gate.approved)
74
- return gate.result;
75
- const stale = await checkPlanFresh(plan);
76
- if (stale)
77
- return stale;
78
- try {
79
- await writePatchPlan(plan);
80
- }
81
- catch (err) {
82
- await rollbackPatchPlan(plan).catch(() => undefined);
83
- return {
84
- content: `Error: apply_patch failed while writing files: ${err instanceof Error ? err.message : String(err)}`,
85
- isError: true,
86
- status: "partial",
87
- metadata: {
88
- kind: "patch",
89
- paths: plan.paths,
90
- diff: plan.diff,
91
- },
92
- };
93
- }
94
- await observePatchPlan(fileState, plan);
95
- let output = `Applied patch to ${plan.changes.length} file${plan.changes.length === 1 ? "" : "s"}.`;
96
- if (plan.fallbackCount > 0) {
97
- output += ` ${plan.fallbackCount} hunk${plan.fallbackCount === 1 ? "" : "s"} used normalized matching.`;
98
- }
99
- if (lsp) {
100
- for (const change of plan.changes) {
101
- if (change.newContent === undefined)
102
- continue;
103
- try {
104
- await lsp.touchFile(change.path, "document");
105
- output += formatDiagnosticBlocks(cwd, change.path, lsp.diagnostics());
106
- }
107
- catch {
108
- // LSP diagnostics should not turn a successful patch into a failed tool call.
109
- }
110
- }
111
- }
112
- const totals = plan.changes.reduce((acc, change) => ({
113
- added: acc.added + change.addedLines,
114
- removed: acc.removed + change.removedLines,
115
- }), { added: 0, removed: 0 });
116
- return {
117
- content: output,
118
- status: "success",
119
- metadata: {
120
- kind: "patch",
121
- paths: plan.paths,
122
- diff: plan.diff,
123
- addedLines: totals.added,
124
- removedLines: totals.removed,
125
- },
126
- };
127
- });
128
- },
129
- };
130
- }
131
- async function buildPatchPlan(cwd, operations) {
132
- const states = new Map();
133
- let fallbackCount = 0;
134
- const stateFor = async (path) => {
135
- const absolutePath = resolveToolPath(cwd, path);
136
- assertWorkspacePath(cwd, absolutePath);
137
- const existing = states.get(absolutePath);
138
- if (existing)
139
- return existing;
140
- const originalContent = await readExistingFile(absolutePath);
141
- const state = {
142
- path: absolutePath,
143
- originalContent,
144
- currentContent: originalContent,
145
- };
146
- states.set(absolutePath, state);
147
- return state;
148
- };
149
- for (const operation of operations) {
150
- if (operation.type === "add") {
151
- const state = await stateFor(operation.path);
152
- if (state.currentContent !== undefined) {
153
- throw new PatchApplyError(`Error: Cannot add ${operation.path}; file already exists.`, "blocked");
154
- }
155
- state.currentContent = buildAddedFileContent(operation.lines);
156
- continue;
157
- }
158
- if (operation.type === "delete") {
159
- const state = await stateFor(operation.path);
160
- if (state.currentContent === undefined) {
161
- throw new PatchApplyError(`Error: Cannot delete ${operation.path}; file does not exist.`, "blocked");
162
- }
163
- state.currentContent = undefined;
164
- continue;
165
- }
166
- const source = await stateFor(operation.path);
167
- if (source.currentContent === undefined) {
168
- throw new PatchApplyError(`Error: Cannot update ${operation.path}; file does not exist.`, "blocked");
169
- }
170
- let nextContent = source.currentContent;
171
- if (operation.chunks.length > 0) {
172
- const patched = applyPatchChunks(source.currentContent, operation.chunks, operation.path);
173
- nextContent = patched.content;
174
- if (patched.usedFallback)
175
- fallbackCount++;
176
- }
177
- if (operation.movePath) {
178
- const target = await stateFor(operation.movePath);
179
- if (target.path === source.path) {
180
- throw new PatchApplyError(`Error: Cannot move ${operation.path} to itself.`, "blocked");
181
- }
182
- if (target.currentContent !== undefined) {
183
- throw new PatchApplyError(`Error: Cannot move ${operation.path} to ${operation.movePath}; target already exists.`, "blocked");
184
- }
185
- source.currentContent = undefined;
186
- target.currentContent = nextContent;
187
- }
188
- else {
189
- source.currentContent = nextContent;
190
- }
191
- }
192
- const changes = [...states.values()]
193
- .filter((state) => state.originalContent !== state.currentContent)
194
- .map((state) => fileStateToChange(state));
195
- if (changes.length === 0) {
196
- throw new PatchApplyError("Error: Patch produced no file changes.", "blocked");
197
- }
198
- const diff = changes.map((change) => change.diff).join("\n");
199
- return {
200
- changes,
201
- diff,
202
- paths: changes.map((change) => change.path),
203
- fallbackCount,
204
- };
205
- }
206
- function fileStateToChange(state) {
207
- const oldContent = state.originalContent;
208
- const newContent = state.currentContent;
209
- const kind = oldContent === undefined
210
- ? "add"
211
- : newContent === undefined
212
- ? "delete"
213
- : "update";
214
- const diff = createTwoFilesPatch(state.path, state.path, oldContent ?? "", newContent ?? "", "original", "modified", { context: 3 });
215
- const stats = countUnifiedDiffChanges(diff);
216
- return {
217
- path: state.path,
218
- kind,
219
- oldContent,
220
- newContent,
221
- diff,
222
- addedLines: stats.added,
223
- removedLines: stats.removed,
224
- };
225
- }
226
- async function readExistingFile(path) {
227
- try {
228
- const info = await stat(path);
229
- if (info.isDirectory()) {
230
- throw new PatchApplyError(`Error: Cannot patch directory: ${path}`, "blocked");
231
- }
232
- await access(path, constants.R_OK | constants.W_OK);
233
- return await readFile(path, "utf-8");
234
- }
235
- catch (err) {
236
- if (err instanceof PatchApplyError)
237
- throw err;
238
- if (isMissingPathError(err))
239
- return undefined;
240
- throw new PatchApplyError(`Error: Cannot read/write file for patch: ${path}`, "blocked");
241
- }
242
- }
243
- async function checkPlanFresh(plan) {
244
- for (const change of plan.changes) {
245
- let current;
246
- try {
247
- current = await readExistingFile(change.path);
248
- }
249
- catch (err) {
250
- if (err instanceof PatchApplyError) {
251
- return { content: err.message, isError: true, status: err.status };
252
- }
253
- throw err;
254
- }
255
- if (current !== change.oldContent) {
256
- return {
257
- content: `Error: Cannot safely apply patch because ${change.path} changed after the patch was prepared.\n\n`
258
- + "Re-read the affected file and regenerate the patch against the latest content.",
259
- isError: true,
260
- status: "blocked",
261
- metadata: {
262
- kind: "patch",
263
- path: change.path,
264
- paths: plan.paths,
265
- reason: "changed",
266
- },
267
- };
268
- }
269
- }
270
- return undefined;
271
- }
272
- async function writePatchPlan(plan) {
273
- for (const change of plan.changes) {
274
- if (change.newContent === undefined)
275
- continue;
276
- await mkdir(dirname(change.path), { recursive: true });
277
- await writeFile(change.path, change.newContent, "utf-8");
278
- }
279
- for (const change of plan.changes) {
280
- if (change.newContent !== undefined)
281
- continue;
282
- await rm(change.path, { force: true });
283
- }
284
- }
285
- async function rollbackPatchPlan(plan) {
286
- for (const change of [...plan.changes].reverse()) {
287
- if (change.oldContent === undefined) {
288
- await rm(change.path, { force: true });
289
- }
290
- else {
291
- await mkdir(dirname(change.path), { recursive: true });
292
- await writeFile(change.path, change.oldContent, "utf-8");
293
- }
294
- }
295
- }
296
- async function observePatchPlan(fileState, plan) {
297
- if (!fileState)
298
- return;
299
- await Promise.all(plan.changes.map(async (change) => {
300
- if (change.newContent === undefined)
301
- return;
302
- await fileState.observe(change.path, "edit", change.newContent).catch(() => undefined);
303
- }));
304
- }
305
- function collectOperationPaths(cwd, operations) {
306
- const paths = [];
307
- for (const operation of operations) {
308
- paths.push(resolveToolPath(cwd, operation.path));
309
- if (operation.type === "update" && operation.movePath) {
310
- paths.push(resolveToolPath(cwd, operation.movePath));
311
- }
312
- }
313
- return paths;
314
- }
315
- function assertWorkspacePath(cwd, filePath) {
316
- if (!isWithinWorkspace(cwd, filePath)) {
317
- throw new PatchApplyError(`Error: Patch path is outside the workspace: ${filePath}`, "blocked");
318
- }
319
- }
320
- function summarizePaths(paths) {
321
- if (paths.length === 1)
322
- return paths[0];
323
- return `${paths[0]} (+${paths.length - 1} more)`;
324
- }
325
- function isMissingPathError(error) {
326
- return (typeof error === "object"
327
- && error !== null
328
- && "code" in error
329
- && (error.code === "ENOENT" || error.code === "ENOTDIR"));
330
- }
@@ -1,41 +0,0 @@
1
- export type PatchFileOperation = {
2
- type: "add";
3
- path: string;
4
- lines: string[];
5
- } | {
6
- type: "delete";
7
- path: string;
8
- } | {
9
- type: "update";
10
- path: string;
11
- movePath?: string;
12
- chunks: PatchChunk[];
13
- };
14
- export interface PatchChunk {
15
- header: string;
16
- lines: PatchLine[];
17
- }
18
- export type PatchLine = {
19
- kind: "context";
20
- text: string;
21
- } | {
22
- kind: "remove";
23
- text: string;
24
- } | {
25
- kind: "add";
26
- text: string;
27
- };
28
- export interface ParsedApplyPatch {
29
- operations: PatchFileOperation[];
30
- }
31
- export interface PatchedContentResult {
32
- content: string;
33
- usedFallback: boolean;
34
- }
35
- export declare class PatchApplyError extends Error {
36
- readonly status: "no_match" | "blocked";
37
- constructor(message: string, status?: "no_match" | "blocked");
38
- }
39
- export declare function parseApplyPatch(patchText: string): ParsedApplyPatch;
40
- export declare function buildAddedFileContent(lines: string[]): string;
41
- export declare function applyPatchChunks(rawContent: string, chunks: PatchChunk[], path: string): PatchedContentResult;
@@ -1,312 +0,0 @@
1
- export class PatchApplyError extends Error {
2
- status;
3
- constructor(message, status = "no_match") {
4
- super(message);
5
- this.status = status;
6
- this.name = "PatchApplyError";
7
- }
8
- }
9
- const CHANGE_MARKERS = [
10
- "*** Add File: ",
11
- "*** Delete File: ",
12
- "*** Update File: ",
13
- "*** End Patch",
14
- ];
15
- export function parseApplyPatch(patchText) {
16
- const lines = normalizeToLF(patchText).split("\n");
17
- if (lines[lines.length - 1] === "")
18
- lines.pop();
19
- if (lines[0] !== "*** Begin Patch") {
20
- throw new PatchApplyError("Error: apply_patch must start with *** Begin Patch", "blocked");
21
- }
22
- const operations = [];
23
- let index = 1;
24
- while (index < lines.length) {
25
- const line = lines[index];
26
- if (line === "*** End Patch") {
27
- if (index !== lines.length - 1) {
28
- throw new PatchApplyError("Error: Unexpected content after *** End Patch", "blocked");
29
- }
30
- if (operations.length === 0) {
31
- throw new PatchApplyError("Error: apply_patch rejected an empty patch", "blocked");
32
- }
33
- return { operations };
34
- }
35
- if (line.startsWith("*** Add File: ")) {
36
- const path = parseMarkerPath(line, "*** Add File: ");
37
- index++;
38
- const addLines = [];
39
- while (index < lines.length && !isFileMarker(lines[index])) {
40
- const current = lines[index];
41
- if (!current.startsWith("+")) {
42
- throw new PatchApplyError(`Error: Add File ${path} contains a non-added line: ${current}`, "blocked");
43
- }
44
- addLines.push(current.slice(1));
45
- index++;
46
- }
47
- operations.push({ type: "add", path, lines: addLines });
48
- continue;
49
- }
50
- if (line.startsWith("*** Delete File: ")) {
51
- const path = parseMarkerPath(line, "*** Delete File: ");
52
- operations.push({ type: "delete", path });
53
- index++;
54
- continue;
55
- }
56
- if (line.startsWith("*** Update File: ")) {
57
- const path = parseMarkerPath(line, "*** Update File: ");
58
- index++;
59
- let movePath;
60
- const chunks = [];
61
- while (index < lines.length && !isFileMarker(lines[index])) {
62
- const current = lines[index];
63
- if (current.startsWith("*** Move to: ")) {
64
- if (movePath || chunks.length > 0) {
65
- throw new PatchApplyError(`Error: Move marker for ${path} must appear before update chunks`, "blocked");
66
- }
67
- movePath = parseMarkerPath(current, "*** Move to: ");
68
- index++;
69
- continue;
70
- }
71
- if (!current.startsWith("@@")) {
72
- throw new PatchApplyError(`Error: Update File ${path} expected @@ hunk header, got: ${current}`, "blocked");
73
- }
74
- const header = current;
75
- index++;
76
- const chunkLines = [];
77
- while (index < lines.length && !isFileMarker(lines[index]) && !lines[index].startsWith("@@")) {
78
- const patchLine = lines[index];
79
- if (patchLine.startsWith("\")) {
80
- index++;
81
- continue;
82
- }
83
- const prefix = patchLine[0];
84
- const text = patchLine.slice(1);
85
- if (prefix === " ") {
86
- chunkLines.push({ kind: "context", text });
87
- }
88
- else if (prefix === "-") {
89
- chunkLines.push({ kind: "remove", text });
90
- }
91
- else if (prefix === "+") {
92
- chunkLines.push({ kind: "add", text });
93
- }
94
- else {
95
- throw new PatchApplyError(`Error: Hunk for ${path} contains invalid line: ${patchLine}`, "blocked");
96
- }
97
- index++;
98
- }
99
- if (chunkLines.length === 0) {
100
- throw new PatchApplyError(`Error: Empty hunk in ${path}`, "blocked");
101
- }
102
- chunks.push({ header, lines: chunkLines });
103
- }
104
- if (!movePath && chunks.length === 0) {
105
- throw new PatchApplyError(`Error: Update File ${path} has no hunks`, "blocked");
106
- }
107
- operations.push({ type: "update", path, ...(movePath ? { movePath } : {}), chunks });
108
- continue;
109
- }
110
- throw new PatchApplyError(`Error: Unexpected patch marker: ${line}`, "blocked");
111
- }
112
- throw new PatchApplyError("Error: apply_patch must end with *** End Patch", "blocked");
113
- }
114
- export function buildAddedFileContent(lines) {
115
- if (lines.length === 0)
116
- return "";
117
- return `${lines.join("\n")}\n`;
118
- }
119
- export function applyPatchChunks(rawContent, chunks, path) {
120
- const { bom, text } = stripBom(rawContent);
121
- const lineEnding = detectLineEnding(text);
122
- let normalized = normalizeToLF(text);
123
- let usedFallback = false;
124
- for (let index = 0; index < chunks.length; index++) {
125
- const result = applyChunk(normalized, chunks[index], path, index);
126
- normalized = result.content;
127
- usedFallback ||= result.usedFallback;
128
- }
129
- return {
130
- content: bom + restoreLineEndings(normalized, lineEnding),
131
- usedFallback,
132
- };
133
- }
134
- function applyChunk(content, chunk, path, chunkIndex) {
135
- const oldLines = chunk.lines
136
- .filter((line) => line.kind === "context" || line.kind === "remove")
137
- .map((line) => line.text);
138
- const newLines = chunk.lines
139
- .filter((line) => line.kind === "context" || line.kind === "add")
140
- .map((line) => line.text);
141
- if (oldLines.length === 0) {
142
- throw new PatchApplyError(`Error: Hunk ${chunkIndex + 1} in ${path} has no context to locate an insertion.`, "blocked");
143
- }
144
- const exactMatches = findExactLineBlockMatches(content, oldLines);
145
- if (exactMatches.length === 1) {
146
- return {
147
- content: replaceSpan(content, exactMatches[0], newLines),
148
- usedFallback: false,
149
- };
150
- }
151
- if (exactMatches.length > 1) {
152
- throw new PatchApplyError(`Error: Hunk ${chunkIndex + 1} in ${path} matched ${exactMatches.length} exact locations. Add more context.`, "blocked");
153
- }
154
- const fallbackMatches = findNormalizedLineBlockMatches(content, oldLines, path);
155
- if (fallbackMatches.length === 1) {
156
- return {
157
- content: replaceSpan(content, fallbackMatches[0], newLines),
158
- usedFallback: true,
159
- };
160
- }
161
- if (fallbackMatches.length > 1) {
162
- throw new PatchApplyError(`Error: Hunk ${chunkIndex + 1} in ${path} matched ${fallbackMatches.length} normalized locations. Add more context.`, "blocked");
163
- }
164
- throw new PatchApplyError(`Error: Hunk ${chunkIndex + 1} in ${path} did not match the file. Re-read the file and regenerate the patch.`);
165
- }
166
- function parseMarkerPath(line, marker) {
167
- const path = line.slice(marker.length).trim();
168
- if (!path)
169
- throw new PatchApplyError(`Error: Patch marker is missing a path: ${line}`, "blocked");
170
- return path;
171
- }
172
- function isFileMarker(line) {
173
- return CHANGE_MARKERS.some((marker) => line === marker || line.startsWith(marker));
174
- }
175
- function detectLineEnding(content) {
176
- const crlf = content.indexOf("\r\n");
177
- const lf = content.indexOf("\n");
178
- return crlf !== -1 && crlf === lf - 1 ? "\r\n" : "\n";
179
- }
180
- function stripBom(content) {
181
- return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
182
- }
183
- function normalizeToLF(text) {
184
- return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
185
- }
186
- function restoreLineEndings(text, lineEnding) {
187
- return lineEnding === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
188
- }
189
- function splitLines(content) {
190
- const lines = [];
191
- let start = 0;
192
- for (let index = 0; index < content.length; index++) {
193
- if (content[index] === "\n") {
194
- lines.push({ text: content.slice(start, index), start, endNoNewline: index });
195
- start = index + 1;
196
- }
197
- }
198
- lines.push({ text: content.slice(start), start, endNoNewline: content.length });
199
- return lines;
200
- }
201
- function findExactLineBlockMatches(content, oldLines) {
202
- const lines = splitLines(content);
203
- const matches = [];
204
- for (let index = 0; index <= lines.length - oldLines.length; index++) {
205
- let matched = true;
206
- for (let offset = 0; offset < oldLines.length; offset++) {
207
- if (lines[index + offset].text !== oldLines[offset]) {
208
- matched = false;
209
- break;
210
- }
211
- }
212
- if (matched) {
213
- matches.push({
214
- start: lines[index].start,
215
- end: lines[index + oldLines.length - 1].endNoNewline,
216
- });
217
- }
218
- }
219
- return matches;
220
- }
221
- function findNormalizedLineBlockMatches(content, oldLines, path) {
222
- const expected = oldLines
223
- .map((line) => normalizeLineForMatch(line))
224
- .filter((line) => line.trim().length > 0);
225
- if (expected.length === 0)
226
- return [];
227
- const contentLines = splitLines(content)
228
- .map((line) => ({ line, normalized: normalizeLineForMatch(line.text) }))
229
- .filter((item) => item.normalized.trim().length > 0);
230
- const matches = [];
231
- for (let index = 0; index <= contentLines.length - expected.length; index++) {
232
- let matched = true;
233
- for (let offset = 0; offset < expected.length; offset++) {
234
- if (!lineEquivalent(contentLines[index + offset].line.text, expected[offset], path, expected.length)) {
235
- matched = false;
236
- break;
237
- }
238
- }
239
- if (matched) {
240
- matches.push({
241
- start: contentLines[index].line.start,
242
- end: contentLines[index + expected.length - 1].line.endNoNewline,
243
- });
244
- }
245
- }
246
- return matches;
247
- }
248
- function normalizeLineForMatch(line) {
249
- return line
250
- .normalize("NFKC")
251
- .trimEnd()
252
- .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
253
- .replace(/[\u201C\u201D\u201E\u201F]/g, '"')
254
- .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
255
- .replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
256
- }
257
- function lineEquivalent(actual, expectedNormalized, path, expectedLineCount) {
258
- const actualNormalized = normalizeLineForMatch(actual);
259
- if (actualNormalized === expectedNormalized)
260
- return true;
261
- const actualCells = splitMarkdownTableCells(actualNormalized);
262
- const expectedCells = splitMarkdownTableCells(expectedNormalized);
263
- if (actualCells && expectedCells && sameCells(actualCells, expectedCells))
264
- return true;
265
- if (expectedLineCount === 1 && isDocumentLikePath(path)) {
266
- return collapseInlineWhitespace(actualNormalized) === collapseInlineWhitespace(expectedNormalized);
267
- }
268
- return false;
269
- }
270
- function splitMarkdownTableCells(line) {
271
- const normalized = line.trim();
272
- if (!normalized.startsWith("|") || !normalized.endsWith("|"))
273
- return undefined;
274
- const parts = [];
275
- let current = "";
276
- let escaped = false;
277
- for (const char of normalized) {
278
- if (escaped) {
279
- current += char;
280
- escaped = false;
281
- continue;
282
- }
283
- if (char === "\\") {
284
- current += char;
285
- escaped = true;
286
- continue;
287
- }
288
- if (char === "|") {
289
- parts.push(current);
290
- current = "";
291
- continue;
292
- }
293
- current += char;
294
- }
295
- parts.push(current);
296
- if (parts.length < 4 || parts[0] !== "" || parts[parts.length - 1] !== "")
297
- return undefined;
298
- const cells = parts.slice(1, -1).map((cell) => cell.trim());
299
- return cells.length >= 2 ? cells : undefined;
300
- }
301
- function sameCells(a, b) {
302
- return a.length === b.length && a.every((cell, index) => cell === b[index]);
303
- }
304
- function collapseInlineWhitespace(text) {
305
- return text.trim().replace(/[ \t]+/g, " ");
306
- }
307
- function isDocumentLikePath(path) {
308
- return /\.(?:md|mdx|markdown|txt|rst|adoc)$/i.test(path);
309
- }
310
- function replaceSpan(content, span, newLines) {
311
- return content.slice(0, span.start) + newLines.join("\n") + content.slice(span.end);
312
- }