critique 0.1.129 → 0.1.135

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.
@@ -13,9 +13,13 @@ export declare function countDelimiter(code: string, delimiter: string): number;
13
13
  * Pass 1 (tokenize): for each hunk, extract content lines and count
14
14
  * delimiter occurrences.
15
15
  *
16
- * Pass 2 (fix): if a hunk has an odd count for any delimiter, prepend
17
- * that delimiter to the first content line so tree-sitter sees balanced
18
- * pairs. No header adjustment needed since no new lines are added.
16
+ * Pass 2 (repair): if a hunk has an odd count for any symmetric delimiter,
17
+ * classify the unmatched boundary token as a likely opener or closer and
18
+ * escape that token in place.
19
+ *
20
+ * Pass 3 (hunk isolation): if a hunk leaves an asymmetric delimiter open,
21
+ * append its closing token to the last content line so the next hunk starts
22
+ * from a clean parser state.
19
23
  */
20
24
  export declare function balanceDelimiters(rawDiff: string, filetype?: string): string;
21
25
  //# sourceMappingURL=balance-delimiters.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"balance-delimiters.d.ts","sourceRoot":"","sources":["../src/balance-delimiters.ts"],"names":[],"mappings":"AAqCA;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CActE;AAOD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA4D5E"}
1
+ {"version":3,"file":"balance-delimiters.d.ts","sourceRoot":"","sources":["../src/balance-delimiters.ts"],"names":[],"mappings":"AAmEA;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CActE;AAwbD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA4E5E"}
@@ -1,37 +1,50 @@
1
1
  // Delimiter balancing for syntax highlighting in diff hunks.
2
2
  //
3
- // When a diff hunk starts inside a paired delimiter (template literal,
4
- // triple-quoted string, fenced code block, etc.), tree-sitter sees an
5
- // odd number of that delimiter and misparses everything after the first
6
- // occurrence.
3
+ // When a diff hunk starts or ends inside a paired delimiter (template
4
+ // literal, triple-quoted string, fenced code block, etc.), tree-sitter can
5
+ // misparse everything after the unmatched token.
7
6
  //
8
- // Two-pass fix:
7
+ // Boundary repair strategy:
9
8
  // 1. Tokenizer: count delimiter occurrences in each hunk's content,
10
9
  // skipping escaped characters.
11
- // 2. Fix: if a hunk has an odd count, prepend a balancing delimiter to
12
- // the first content line so tree-sitter sees balanced pairs.
10
+ // 2. Repair symmetric delimiters by escaping the unmatched boundary token.
11
+ // 3. Repair asymmetric delimiters by appending the closing token to the
12
+ // last content line in the hunk so later hunks do not inherit state.
13
13
  //
14
- // Why no string/comment state tracking: the tokenizer processes partial
15
- // code (diff hunks) that may start mid-string. Tracking quote states
16
- // causes false positives when template text contains apostrophes
17
- // (`it's`) or quotes (`class="foo"`). Tracking comment states breaks
18
- // on `://` in URL templates. The \-escape handler already covers
19
- // escaped delimiters, and unescaped delimiters in "wrong" contexts
20
- // (strings, comments) are rare enough that the tradeoff is worth it.
14
+ // Why this is safer than prepending a synthetic opener: prepending fixes hunks
15
+ // that begin inside a string, but it corrupts hunks that end inside an open
16
+ // string/fence/docstring. Escaping the actual unmatched token keeps the repair
17
+ // local and avoids duplicating delimiters like ``` -> ``````.
18
+ const cStyleBlockCommentRule = {
19
+ token: "/*",
20
+ closeToken: "*/",
21
+ };
22
+ const htmlCommentRule = {
23
+ token: "<!--",
24
+ closeToken: "-->",
25
+ };
21
26
  /**
22
27
  * Delimiters to balance per language filetype.
23
28
  *
24
- * Each entry maps a filetype (from detectFiletype) to the list of
25
- * delimiters that come in open/close pairs and can span lines.
29
+ * Each entry maps a filetype (from detectFiletype) to the list of delimiters
30
+ * that come in open/close pairs and can span lines.
26
31
  */
27
32
  const LANGUAGE_DELIMITERS = {
28
- typescript: ["`"],
29
- python: ['"""', "'''"],
30
- markdown: ["```"],
31
- go: ["`"],
32
- scala: ['"""'],
33
- swift: ['"""'],
34
- julia: ['"""'],
33
+ typescript: [{ token: "`" }, cStyleBlockCommentRule],
34
+ python: [{ token: '"""' }, { token: "'''" }],
35
+ markdown: [{ token: "```", classifyFence: classifyMarkdownFence }],
36
+ go: [{ token: "`" }, cStyleBlockCommentRule],
37
+ rust: [cStyleBlockCommentRule],
38
+ cpp: [cStyleBlockCommentRule],
39
+ csharp: [cStyleBlockCommentRule],
40
+ c: [cStyleBlockCommentRule],
41
+ java: [cStyleBlockCommentRule],
42
+ php: [cStyleBlockCommentRule],
43
+ scala: [{ token: '"""' }, cStyleBlockCommentRule],
44
+ html: [htmlCommentRule],
45
+ css: [cStyleBlockCommentRule],
46
+ swift: [{ token: '"""' }, cStyleBlockCommentRule],
47
+ julia: [{ token: '"""' }],
35
48
  };
36
49
  /**
37
50
  * Count unescaped occurrences of a delimiter in a code string.
@@ -49,11 +62,354 @@ export function countDelimiter(code, delimiter) {
49
62
  }
50
63
  else if (code.startsWith(delimiter, i)) {
51
64
  count++;
52
- i += len - 1; // -1 because the loop increments
65
+ i += len - 1;
53
66
  }
54
67
  }
55
68
  return count;
56
69
  }
70
+ function isDiffContentLine(line) {
71
+ return line[0] === " " || line[0] === "+" || line[0] === "-";
72
+ }
73
+ function getContentLines(lines) {
74
+ return lines.flatMap((line, hunkLineIndex) => isDiffContentLine(line)
75
+ ? [{ hunkLineIndex, content: line.slice(1) }]
76
+ : []);
77
+ }
78
+ function findDelimiterOccurrences(contentLines, delimiter) {
79
+ const occurrences = [];
80
+ for (const [contentLineIndex, line] of contentLines.entries()) {
81
+ const content = line.content;
82
+ const len = delimiter.length;
83
+ for (let column = 0; column < content.length; column++) {
84
+ if (content[column] === "\\") {
85
+ column++;
86
+ continue;
87
+ }
88
+ if (!content.startsWith(delimiter, column)) {
89
+ continue;
90
+ }
91
+ occurrences.push({
92
+ contentLineIndex,
93
+ hunkLineIndex: line.hunkLineIndex,
94
+ column,
95
+ });
96
+ column += len - 1;
97
+ }
98
+ }
99
+ return occurrences;
100
+ }
101
+ function findAnyDelimiterOccurrences(contentLines, delimiters) {
102
+ const ordered = [...delimiters].sort((a, b) => b.length - a.length);
103
+ const occurrences = [];
104
+ for (const [contentLineIndex, line] of contentLines.entries()) {
105
+ const content = line.content;
106
+ for (let column = 0; column < content.length; column++) {
107
+ if (content[column] === "\\") {
108
+ column++;
109
+ continue;
110
+ }
111
+ const matched = ordered.find((delimiter) => content.startsWith(delimiter, column));
112
+ if (!matched) {
113
+ continue;
114
+ }
115
+ occurrences.push({
116
+ contentLineIndex,
117
+ hunkLineIndex: line.hunkLineIndex,
118
+ column,
119
+ });
120
+ column += matched.length - 1;
121
+ }
122
+ }
123
+ return occurrences;
124
+ }
125
+ function getPreviousNonWhitespaceChar(content, column) {
126
+ for (let i = column - 1; i >= 0; i--) {
127
+ const char = content[i];
128
+ if (char && !/\s/.test(char)) {
129
+ return char;
130
+ }
131
+ }
132
+ return undefined;
133
+ }
134
+ function getNextNonWhitespaceChar(content, column) {
135
+ for (let i = column; i < content.length; i++) {
136
+ const char = content[i];
137
+ if (char && !/\s/.test(char)) {
138
+ return char;
139
+ }
140
+ }
141
+ return undefined;
142
+ }
143
+ function hasNonEmptyContentBefore(contentLines, contentLineIndex) {
144
+ return contentLines.slice(0, contentLineIndex).some((line) => line.content.trim() !== "");
145
+ }
146
+ function hasNonEmptyContentAfter(contentLines, contentLineIndex) {
147
+ return contentLines.slice(contentLineIndex + 1).some((line) => line.content.trim() !== "");
148
+ }
149
+ function classifyOccurrence(contentLines, occurrence, token) {
150
+ const content = contentLines[occurrence.contentLineIndex]?.content;
151
+ if (content === undefined) {
152
+ return "unknown";
153
+ }
154
+ const before = getPreviousNonWhitespaceChar(content, occurrence.column);
155
+ const after = getNextNonWhitespaceChar(content, occurrence.column + token.length);
156
+ const trimmed = content.trim();
157
+ const hasBeforeLines = hasNonEmptyContentBefore(contentLines, occurrence.contentLineIndex);
158
+ const hasAfterLines = hasNonEmptyContentAfter(contentLines, occurrence.contentLineIndex);
159
+ if (token.length > 1) {
160
+ if (trimmed === token) {
161
+ if (hasBeforeLines)
162
+ return "close";
163
+ if (hasAfterLines)
164
+ return "open";
165
+ return "unknown";
166
+ }
167
+ if (trimmed.startsWith(token)) {
168
+ if (hasBeforeLines && (!after || /[.\])};:,]/.test(after))) {
169
+ return "close";
170
+ }
171
+ if (after) {
172
+ return "open";
173
+ }
174
+ }
175
+ if (trimmed.endsWith(token)) {
176
+ return "close";
177
+ }
178
+ return "unknown";
179
+ }
180
+ if (!before && after) {
181
+ return "open";
182
+ }
183
+ if (before && !after) {
184
+ return "close";
185
+ }
186
+ if (after && /[$A-Za-z0-9_{[(]/.test(after)) {
187
+ return "open";
188
+ }
189
+ if (before && after && /[)\]};:.,]/.test(after)) {
190
+ return "close";
191
+ }
192
+ return "unknown";
193
+ }
194
+ function escapeDelimiterAt(lines, hunkLineIndex, column) {
195
+ return lines.map((line, index) => {
196
+ if (index !== hunkLineIndex || !isDiffContentLine(line)) {
197
+ return line;
198
+ }
199
+ const prefix = line[0] ?? "";
200
+ const content = line.slice(1);
201
+ return prefix + content.slice(0, column) + "\\" + content.slice(column);
202
+ });
203
+ }
204
+ // ---------------------------------------------------------------------------
205
+ // Contextual fence classification and repair (markdown code fences)
206
+ // ---------------------------------------------------------------------------
207
+ /**
208
+ * Classify a ``` occurrence as a markdown fence opener, closer, or not a fence.
209
+ *
210
+ * Returns null if the occurrence is not a valid block-level fence (e.g. inline
211
+ * triple-backtick in prose, or indented more than 3 spaces).
212
+ * Returns "open" if followed by an info string (language tag).
213
+ * Returns "close" if nothing follows the backtick run (only whitespace).
214
+ */
215
+ function classifyMarkdownFence(content, column) {
216
+ // Must be at start of line with at most 3 spaces of indentation
217
+ const beforeFence = content.slice(0, column);
218
+ if (beforeFence.length > 3 || /\S/.test(beforeFence))
219
+ return null;
220
+ // Count consecutive backticks (support 4+ backtick fences per CommonMark)
221
+ let fenceLen = 0;
222
+ for (let i = column; i < content.length && content[i] === "`"; i++)
223
+ fenceLen++;
224
+ if (fenceLen < 3)
225
+ return null;
226
+ // What comes after the backtick run?
227
+ const afterFence = content.slice(column + fenceLen).trim();
228
+ // Closing fence: nothing after backticks (only whitespace)
229
+ if (!afterFence)
230
+ return "close";
231
+ // Opening fence: has info string (language tag)
232
+ // Info string must not contain backticks (CommonMark spec)
233
+ if (!afterFence.includes("`"))
234
+ return "open";
235
+ return null;
236
+ }
237
+ /**
238
+ * Simulate a sequential walk through classified fences to detect boundary
239
+ * artifacts. Markdown code fences don't nest, so depth toggles between 0
240
+ * (outside) and 1 (inside).
241
+ *
242
+ * - "open" (has info string) always pushes depth to 1.
243
+ * - "close" (bare fence) decrements if inside, or acts as a bare opener
244
+ * if already outside (a code block without a language tag).
245
+ *
246
+ * Conflicts are counted when a must-open fires while already inside (depth
247
+ * was already 1) or when other impossible transitions occur.
248
+ */
249
+ function walkFences(fences, startDepth) {
250
+ let depth = startDepth;
251
+ let conflicts = 0;
252
+ for (const fence of fences) {
253
+ if (fence.kind === "open") {
254
+ if (depth > 0)
255
+ conflicts++;
256
+ depth = 1;
257
+ }
258
+ else {
259
+ // "close" (bare fence): close if inside, otherwise bare opener
260
+ if (depth > 0) {
261
+ depth = 0;
262
+ }
263
+ else {
264
+ depth = 1;
265
+ }
266
+ }
267
+ }
268
+ return { startDepth, endDepth: depth, conflicts };
269
+ }
270
+ /**
271
+ * Repair context-dependent fences (like markdown ```) using sequential
272
+ * open/close pairing instead of simple odd/even counting.
273
+ *
274
+ * Tries two starting states (outside vs inside a code block), picks the
275
+ * walk with fewer conflicts, and adds synthetic fence tokens inline:
276
+ * - If the hunk starts inside a block: prepend ``` to the first content
277
+ * line so the boundary closer has something to close.
278
+ * - If the hunk ends inside a block: append ``` to the last content
279
+ * line so the boundary opener is properly closed.
280
+ * Tokens are added inline (no new lines) to preserve patch header counts.
281
+ */
282
+ function repairContextualFences(contentLines, lines, rule) {
283
+ if (!rule.classifyFence)
284
+ return [...lines];
285
+ const occurrences = findDelimiterOccurrences(contentLines, rule.token);
286
+ if (occurrences.length === 0)
287
+ return [...lines];
288
+ // Classify each occurrence as fence open, close, or not-a-fence
289
+ const fences = [];
290
+ for (const occ of occurrences) {
291
+ const content = contentLines[occ.contentLineIndex]?.content;
292
+ if (!content)
293
+ continue;
294
+ const kind = rule.classifyFence(content, occ.column);
295
+ if (kind) {
296
+ fences.push({ occurrence: occ, kind });
297
+ }
298
+ }
299
+ if (fences.length === 0)
300
+ return [...lines];
301
+ // Try both starting states
302
+ const walk0 = walkFences(fences, 0);
303
+ const walk1 = walkFences(fences, 1);
304
+ // Pick better walk: fewer conflicts → fewer repairs → content heuristic
305
+ let chosen;
306
+ if (walk0.conflicts < walk1.conflicts) {
307
+ chosen = walk0;
308
+ }
309
+ else if (walk1.conflicts < walk0.conflicts) {
310
+ chosen = walk1;
311
+ }
312
+ else {
313
+ const repairs0 = (walk0.startDepth > 0 ? 1 : 0) + (walk0.endDepth > 0 ? 1 : 0);
314
+ const repairs1 = (walk1.startDepth > 0 ? 1 : 0) + (walk1.endDepth > 0 ? 1 : 0);
315
+ if (repairs0 < repairs1) {
316
+ chosen = walk0;
317
+ }
318
+ else if (repairs1 < repairs0) {
319
+ chosen = walk1;
320
+ }
321
+ else {
322
+ // Still tied: disambiguate by content position relative to the first fence.
323
+ // If there's non-empty content before the first fence in the hunk, the fence
324
+ // is likely closing a block from before the hunk → prefer depth=1 (starts inside).
325
+ // This produces better tree-sitter pairing: the prepended ``` + original ```
326
+ // form a matched pair, while appended inline ``` doesn't close a fence.
327
+ const firstIdx = fences[0]?.occurrence.contentLineIndex ?? 0;
328
+ const hasContentBefore = contentLines.slice(0, firstIdx).some((line) => line.content.trim() !== "");
329
+ chosen = hasContentBefore ? walk1 : walk0;
330
+ }
331
+ }
332
+ let result = [...lines];
333
+ // Append synthetic closer to end of last content line (inline, no new lines)
334
+ if (chosen.endDepth > 0) {
335
+ result = appendClosingTokensToLastContentLine(result, rule.token, 1);
336
+ }
337
+ // Prepend synthetic opener to start of first content line (inline, no new lines).
338
+ // Pass the first fence's hunk line index so the search only looks at lines
339
+ // before the boundary closer — the opener must precede it to form a pair.
340
+ if (chosen.startDepth > 0) {
341
+ const firstFenceHunkLine = fences[0]?.occurrence.hunkLineIndex;
342
+ result = prependOpeningTokenToFirstContentLine(result, rule.token, firstFenceHunkLine);
343
+ }
344
+ return result;
345
+ }
346
+ function getRuleOpenTokens(rule) {
347
+ return rule.openTokens ?? [rule.token];
348
+ }
349
+ function getRuleCloseToken(rule) {
350
+ return rule.closeToken ?? rule.token;
351
+ }
352
+ function isSymmetricRule(rule) {
353
+ const openTokens = getRuleOpenTokens(rule);
354
+ const closeToken = getRuleCloseToken(rule);
355
+ return openTokens.length === 1 && openTokens[0] === closeToken;
356
+ }
357
+ function getUnclosedTokenCount(lines, rule) {
358
+ const contentLines = getContentLines(lines);
359
+ const openTokens = getRuleOpenTokens(rule);
360
+ const closeToken = getRuleCloseToken(rule);
361
+ if (isSymmetricRule(rule)) {
362
+ return 0;
363
+ }
364
+ const openCount = findAnyDelimiterOccurrences(contentLines, openTokens).length;
365
+ const closeCount = findDelimiterOccurrences(contentLines, closeToken).length;
366
+ return Math.max(0, openCount - closeCount);
367
+ }
368
+ function appendClosingTokensToLastContentLine(lines, closeToken, count) {
369
+ if (count <= 0) {
370
+ return [...lines];
371
+ }
372
+ const lastContentLineIndex = [...lines].findLastIndex(isDiffContentLine);
373
+ if (lastContentLineIndex === -1) {
374
+ return [...lines];
375
+ }
376
+ const closingSuffix = Array.from({ length: count }, () => closeToken).join(" ");
377
+ return lines.map((line, index) => {
378
+ if (index !== lastContentLineIndex || !isDiffContentLine(line)) {
379
+ return line;
380
+ }
381
+ return `${line} ${closingSuffix}`;
382
+ });
383
+ }
384
+ function prependOpeningTokenToFirstContentLine(lines, openToken, beforeHunkLineIndex) {
385
+ // Prefer a blank/whitespace content line to avoid creating a fake info string
386
+ // (e.g. "``` handler() {" makes tree-sitter think "handler()" is a language).
387
+ // Only search among lines BEFORE the first fence so the synthetic opener
388
+ // appears before the boundary closer (they need to pair).
389
+ const firstContentLineIndex = lines.findIndex(isDiffContentLine);
390
+ if (firstContentLineIndex === -1) {
391
+ return [...lines];
392
+ }
393
+ const searchEnd = beforeHunkLineIndex ?? lines.length;
394
+ let targetIndex = firstContentLineIndex;
395
+ for (let i = firstContentLineIndex; i < searchEnd; i++) {
396
+ const line = lines[i];
397
+ if (!isDiffContentLine(line))
398
+ continue;
399
+ if (line.slice(1).trim() === "") {
400
+ targetIndex = i;
401
+ break;
402
+ }
403
+ }
404
+ return lines.map((line, index) => {
405
+ if (index !== targetIndex || !isDiffContentLine(line)) {
406
+ return line;
407
+ }
408
+ const prefix = line[0] ?? "";
409
+ const content = line.slice(1);
410
+ return `${prefix}${openToken} ${content}`;
411
+ });
412
+ }
57
413
  /**
58
414
  * Balance paired delimiters in a unified diff patch for correct syntax
59
415
  * highlighting.
@@ -61,20 +417,23 @@ export function countDelimiter(code, delimiter) {
61
417
  * Pass 1 (tokenize): for each hunk, extract content lines and count
62
418
  * delimiter occurrences.
63
419
  *
64
- * Pass 2 (fix): if a hunk has an odd count for any delimiter, prepend
65
- * that delimiter to the first content line so tree-sitter sees balanced
66
- * pairs. No header adjustment needed since no new lines are added.
420
+ * Pass 2 (repair): if a hunk has an odd count for any symmetric delimiter,
421
+ * classify the unmatched boundary token as a likely opener or closer and
422
+ * escape that token in place.
423
+ *
424
+ * Pass 3 (hunk isolation): if a hunk leaves an asymmetric delimiter open,
425
+ * append its closing token to the last content line so the next hunk starts
426
+ * from a clean parser state.
67
427
  */
68
428
  export function balanceDelimiters(rawDiff, filetype) {
69
429
  if (!filetype)
70
430
  return rawDiff;
71
- const delimiters = LANGUAGE_DELIMITERS[filetype];
72
- if (!delimiters)
431
+ const rules = LANGUAGE_DELIMITERS[filetype];
432
+ if (!rules)
73
433
  return rawDiff;
74
434
  const lines = rawDiff.split("\n");
75
435
  const fileHeader = [];
76
436
  const hunks = [];
77
- // Split into file header + hunks
78
437
  for (const line of lines) {
79
438
  if (line.startsWith("@@")) {
80
439
  hunks.push({ header: line, lines: [] });
@@ -88,39 +447,48 @@ export function balanceDelimiters(rawDiff, filetype) {
88
447
  }
89
448
  if (hunks.length === 0)
90
449
  return rawDiff;
91
- // Pass 2: check each hunk and fix if needed
92
450
  const result = [...fileHeader];
93
451
  for (const hunk of hunks) {
94
- // Extract content text from diff lines (strip the +/-/space prefix)
95
- const content = hunk.lines
96
- .filter(l => l[0] === " " || l[0] === "+" || l[0] === "-")
97
- .map(l => l.slice(1))
98
- .join("\n");
99
- // Find the first delimiter with an odd count
100
- let unbalanced;
101
- for (const delim of delimiters) {
102
- if (countDelimiter(content, delim) % 2 !== 0) {
103
- unbalanced = delim;
452
+ const contentLines = getContentLines(hunk.lines);
453
+ let repairedLines = hunk.lines;
454
+ for (const rule of rules) {
455
+ if (!isSymmetricRule(rule)) {
456
+ continue;
457
+ }
458
+ // Contextual fence pairing (markdown code fences): uses sequential
459
+ // open/close tracking instead of simple odd/even parity.
460
+ if (rule.classifyFence) {
461
+ repairedLines = repairContextualFences(contentLines, repairedLines, rule);
462
+ continue;
463
+ }
464
+ const occurrences = findDelimiterOccurrences(contentLines, rule.token);
465
+ if (occurrences.length % 2 === 0) {
466
+ continue;
467
+ }
468
+ const first = occurrences[0];
469
+ const last = occurrences[occurrences.length - 1];
470
+ if (!first || !last) {
471
+ continue;
472
+ }
473
+ const firstBoundary = classifyOccurrence(contentLines, first, rule.token);
474
+ const lastBoundary = classifyOccurrence(contentLines, last, rule.token);
475
+ if (firstBoundary === "close") {
476
+ repairedLines = escapeDelimiterAt(repairedLines, first.hunkLineIndex, first.column);
104
477
  break;
105
478
  }
106
- }
107
- result.push(hunk.header);
108
- if (unbalanced) {
109
- // Prepend the balancing delimiter to the first content line
110
- let fixed = false;
111
- for (const line of hunk.lines) {
112
- if (!fixed && (line[0] === " " || line[0] === "+" || line[0] === "-")) {
113
- result.push(line[0] + unbalanced + line.slice(1));
114
- fixed = true;
115
- }
116
- else {
117
- result.push(line);
118
- }
479
+ if (lastBoundary === "open") {
480
+ repairedLines = escapeDelimiterAt(repairedLines, last.hunkLineIndex, last.column);
481
+ break;
119
482
  }
120
483
  }
121
- else {
122
- result.push(...hunk.lines);
484
+ for (const rule of rules) {
485
+ const unclosedCount = getUnclosedTokenCount(repairedLines, rule);
486
+ if (unclosedCount > 0) {
487
+ repairedLines = appendClosingTokensToLastContentLine(repairedLines, getRuleCloseToken(rule), unclosedCount);
488
+ }
123
489
  }
490
+ result.push(hunk.header);
491
+ result.push(...repairedLines);
124
492
  }
125
493
  return result.join("\n");
126
494
  }