@pierre/diffs 1.2.6 → 1.3.0-beta.1

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 (165) hide show
  1. package/dist/components/CodeView.d.ts +0 -1
  2. package/dist/components/CodeView.d.ts.map +1 -1
  3. package/dist/components/CodeView.js +0 -23
  4. package/dist/components/CodeView.js.map +1 -1
  5. package/dist/components/File.d.ts +7 -2
  6. package/dist/components/File.d.ts.map +1 -1
  7. package/dist/components/File.js +36 -2
  8. package/dist/components/File.js.map +1 -1
  9. package/dist/components/FileDiff.d.ts +8 -2
  10. package/dist/components/FileDiff.d.ts.map +1 -1
  11. package/dist/components/FileDiff.js +73 -1
  12. package/dist/components/FileDiff.js.map +1 -1
  13. package/dist/components/UnresolvedFile.d.ts.map +1 -1
  14. package/dist/components/UnresolvedFile.js +2 -2
  15. package/dist/components/VirtualizedFile.d.ts +2 -1
  16. package/dist/components/VirtualizedFile.d.ts.map +1 -1
  17. package/dist/components/VirtualizedFile.js +48 -48
  18. package/dist/components/VirtualizedFile.js.map +1 -1
  19. package/dist/components/VirtualizedFileDiff.js +42 -22
  20. package/dist/components/VirtualizedFileDiff.js.map +1 -1
  21. package/dist/components/Virtualizer.d.ts +1 -1
  22. package/dist/components/Virtualizer.d.ts.map +1 -1
  23. package/dist/components/Virtualizer.js +3 -5
  24. package/dist/components/Virtualizer.js.map +1 -1
  25. package/dist/constants.d.ts.map +1 -1
  26. package/dist/editor/command.d.ts +6 -0
  27. package/dist/editor/command.d.ts.map +1 -0
  28. package/dist/editor/command.js +31 -0
  29. package/dist/editor/command.js.map +1 -0
  30. package/dist/editor/css.d.ts +6 -0
  31. package/dist/editor/css.d.ts.map +1 -0
  32. package/dist/editor/css.js +218 -0
  33. package/dist/editor/css.js.map +1 -0
  34. package/dist/editor/editStack.d.ts +66 -0
  35. package/dist/editor/editStack.d.ts.map +1 -0
  36. package/dist/editor/editStack.js +218 -0
  37. package/dist/editor/editStack.js.map +1 -0
  38. package/dist/editor/editor.d.ts +22 -0
  39. package/dist/editor/editor.d.ts.map +1 -0
  40. package/dist/editor/editor.js +1323 -0
  41. package/dist/editor/editor.js.map +1 -0
  42. package/dist/editor/index.d.ts +3 -0
  43. package/dist/editor/index.js +4 -0
  44. package/dist/editor/lineAnnotations.d.ts +8 -0
  45. package/dist/editor/lineAnnotations.d.ts.map +1 -0
  46. package/dist/editor/lineAnnotations.js +32 -0
  47. package/dist/editor/lineAnnotations.js.map +1 -0
  48. package/dist/editor/pieceTable.d.ts +33 -0
  49. package/dist/editor/pieceTable.d.ts.map +1 -0
  50. package/dist/editor/pieceTable.js +590 -0
  51. package/dist/editor/pieceTable.js.map +1 -0
  52. package/dist/editor/platform.d.ts +12 -0
  53. package/dist/editor/platform.d.ts.map +1 -0
  54. package/dist/editor/platform.js +44 -0
  55. package/dist/editor/platform.js.map +1 -0
  56. package/dist/editor/quickEdit.d.ts +29 -0
  57. package/dist/editor/quickEdit.d.ts.map +1 -0
  58. package/dist/editor/quickEdit.js +81 -0
  59. package/dist/editor/quickEdit.js.map +1 -0
  60. package/dist/editor/searchPanel.d.ts +30 -0
  61. package/dist/editor/searchPanel.d.ts.map +1 -0
  62. package/dist/editor/searchPanel.js +219 -0
  63. package/dist/editor/searchPanel.js.map +1 -0
  64. package/dist/editor/selection.d.ts +126 -0
  65. package/dist/editor/selection.d.ts.map +1 -0
  66. package/dist/editor/selection.js +900 -0
  67. package/dist/editor/selection.js.map +1 -0
  68. package/dist/editor/textDocument.d.ts +139 -0
  69. package/dist/editor/textDocument.d.ts.map +1 -0
  70. package/dist/editor/textDocument.js +202 -0
  71. package/dist/editor/textDocument.js.map +1 -0
  72. package/dist/editor/textMeasure.d.ts +32 -0
  73. package/dist/editor/textMeasure.d.ts.map +1 -0
  74. package/dist/editor/textMeasure.js +108 -0
  75. package/dist/editor/textMeasure.js.map +1 -0
  76. package/dist/editor/tokenzier.d.ts +37 -0
  77. package/dist/editor/tokenzier.d.ts.map +1 -0
  78. package/dist/editor/tokenzier.js +348 -0
  79. package/dist/editor/tokenzier.js.map +1 -0
  80. package/dist/editor/utils.d.ts +16 -0
  81. package/dist/editor/utils.d.ts.map +1 -0
  82. package/dist/editor/utils.js +37 -0
  83. package/dist/editor/utils.js.map +1 -0
  84. package/dist/index.d.ts +2 -2
  85. package/dist/index.js +2 -2
  86. package/dist/react/EditorContext.d.ts +16 -0
  87. package/dist/react/EditorContext.d.ts.map +1 -0
  88. package/dist/react/EditorContext.js +26 -0
  89. package/dist/react/EditorContext.js.map +1 -0
  90. package/dist/react/File.d.ts +2 -1
  91. package/dist/react/File.d.ts.map +1 -1
  92. package/dist/react/File.js +3 -2
  93. package/dist/react/File.js.map +1 -1
  94. package/dist/react/FileDiff.d.ts +3 -1
  95. package/dist/react/FileDiff.d.ts.map +1 -1
  96. package/dist/react/FileDiff.js +3 -2
  97. package/dist/react/FileDiff.js.map +1 -1
  98. package/dist/react/MultiFileDiff.d.ts +3 -1
  99. package/dist/react/MultiFileDiff.d.ts.map +1 -1
  100. package/dist/react/MultiFileDiff.js +3 -2
  101. package/dist/react/MultiFileDiff.js.map +1 -1
  102. package/dist/react/PatchDiff.d.ts +3 -1
  103. package/dist/react/PatchDiff.d.ts.map +1 -1
  104. package/dist/react/PatchDiff.js +3 -2
  105. package/dist/react/PatchDiff.js.map +1 -1
  106. package/dist/react/index.d.ts +3 -2
  107. package/dist/react/index.js +2 -1
  108. package/dist/react/jsx.d.ts +0 -1
  109. package/dist/react/jsx.d.ts.map +1 -1
  110. package/dist/react/types.d.ts +1 -0
  111. package/dist/react/types.d.ts.map +1 -1
  112. package/dist/react/types.js +0 -1
  113. package/dist/react/utils/useFileDiffInstance.d.ts +3 -1
  114. package/dist/react/utils/useFileDiffInstance.d.ts.map +1 -1
  115. package/dist/react/utils/useFileDiffInstance.js +31 -5
  116. package/dist/react/utils/useFileDiffInstance.js.map +1 -1
  117. package/dist/react/utils/useFileInstance.d.ts +4 -1
  118. package/dist/react/utils/useFileInstance.d.ts.map +1 -1
  119. package/dist/react/utils/useFileInstance.js +30 -5
  120. package/dist/react/utils/useFileInstance.js.map +1 -1
  121. package/dist/renderers/DiffHunksRenderer.d.ts +2 -2
  122. package/dist/renderers/DiffHunksRenderer.d.ts.map +1 -1
  123. package/dist/renderers/DiffHunksRenderer.js +9 -5
  124. package/dist/renderers/DiffHunksRenderer.js.map +1 -1
  125. package/dist/renderers/FileRenderer.d.ts +5 -1
  126. package/dist/renderers/FileRenderer.d.ts.map +1 -1
  127. package/dist/renderers/FileRenderer.js +108 -41
  128. package/dist/renderers/FileRenderer.js.map +1 -1
  129. package/dist/ssr/index.d.ts +2 -2
  130. package/dist/style.js +1 -1
  131. package/dist/style.js.map +1 -1
  132. package/dist/types.d.ts +45 -1
  133. package/dist/types.d.ts.map +1 -1
  134. package/dist/utils/cleanLastNewline.js +6 -1
  135. package/dist/utils/cleanLastNewline.js.map +1 -1
  136. package/dist/utils/computeEstimatedDiffHeights.js +20 -9
  137. package/dist/utils/computeEstimatedDiffHeights.js.map +1 -1
  138. package/dist/utils/computeFileOffsets.d.ts +13 -0
  139. package/dist/utils/computeFileOffsets.d.ts.map +1 -0
  140. package/dist/utils/computeFileOffsets.js +33 -0
  141. package/dist/utils/computeFileOffsets.js.map +1 -0
  142. package/dist/utils/createTransformerWithState.js +9 -0
  143. package/dist/utils/createTransformerWithState.js.map +1 -1
  144. package/dist/utils/iterateOverDiff.js +182 -147
  145. package/dist/utils/iterateOverDiff.js.map +1 -1
  146. package/dist/utils/renderDiffWithHighlighter.js +1 -1
  147. package/dist/utils/renderFileWithHighlighter.js +5 -14
  148. package/dist/utils/renderFileWithHighlighter.js.map +1 -1
  149. package/dist/utils/virtualDiffLayout.d.ts +2 -23
  150. package/dist/utils/virtualDiffLayout.d.ts.map +1 -1
  151. package/dist/utils/virtualDiffLayout.js +1 -41
  152. package/dist/utils/virtualDiffLayout.js.map +1 -1
  153. package/dist/worker/WorkerPoolManager.js +1 -1
  154. package/dist/worker/{wasm-BaDzIkIn.js → wasm-D4DU5jgR.js} +2 -2
  155. package/dist/worker/wasm-D4DU5jgR.js.map +1 -0
  156. package/dist/worker/worker-portable.js +349 -363
  157. package/dist/worker/worker-portable.js.map +1 -1
  158. package/dist/worker/worker.js +222 -243
  159. package/dist/worker/worker.js.map +1 -1
  160. package/package.json +9 -1
  161. package/dist/utils/iterateOverFile.d.ts +0 -50
  162. package/dist/utils/iterateOverFile.d.ts.map +0 -1
  163. package/dist/utils/iterateOverFile.js +0 -49
  164. package/dist/utils/iterateOverFile.js.map +0 -1
  165. package/dist/worker/wasm-BaDzIkIn.js.map +0 -1
@@ -0,0 +1,13 @@
1
+ //#region src/utils/computeFileOffsets.d.ts
2
+ /**
3
+ * Computes line start offsets for a string.
4
+ */
5
+ declare function computeLineOffsets(contents: string): number[];
6
+ /**
7
+ * Splits file contents into lines aligned with {@link computeLineOffsets}.
8
+ * Unlike splitFileContents, a trailing newline produces a final empty line.
9
+ */
10
+ declare function linesFromFileContents(contents: string): string[];
11
+ //#endregion
12
+ export { computeLineOffsets, linesFromFileContents };
13
+ //# sourceMappingURL=computeFileOffsets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computeFileOffsets.d.ts","names":["computeLineOffsets","linesFromFileContents"],"sources":["../../src/utils/computeFileOffsets.d.ts"],"sourcesContent":["/**\n * Computes line start offsets for a string.\n */\nexport declare function computeLineOffsets(contents: string): number[];\n/**\n * Splits file contents into lines aligned with {@link computeLineOffsets}.\n * Unlike splitFileContents, a trailing newline produces a final empty line.\n */\nexport declare function linesFromFileContents(contents: string): string[];\n//# sourceMappingURL=computeFileOffsets.d.ts.map"],"mappings":";;AAGA;AAKA;iBALwBA,kBAAAA;;;;;iBAKAC,qBAAAA"}
@@ -0,0 +1,33 @@
1
+ //#region src/utils/computeFileOffsets.ts
2
+ const LINE_FEED = 10;
3
+ const CARRIAGE_RETURN = 13;
4
+ /**
5
+ * Computes line start offsets for a string.
6
+ */
7
+ function computeLineOffsets(contents) {
8
+ const offsets = [0];
9
+ for (let i = 0; i < contents.length; i++) {
10
+ const char = contents.charCodeAt(i);
11
+ if (char === LINE_FEED || char === CARRIAGE_RETURN) {
12
+ if (char === CARRIAGE_RETURN && i + 1 < contents.length && contents.charCodeAt(i + 1) === LINE_FEED) i++;
13
+ offsets.push(i + 1);
14
+ }
15
+ }
16
+ return offsets;
17
+ }
18
+ /**
19
+ * Splits file contents into lines aligned with {@link computeLineOffsets}.
20
+ * Unlike splitFileContents, a trailing newline produces a final empty line.
21
+ */
22
+ function linesFromFileContents(contents) {
23
+ const offsets = computeLineOffsets(contents);
24
+ return Array.from({ length: offsets.length }, (_, i) => {
25
+ const start = offsets[i];
26
+ const end = offsets[i + 1] ?? contents.length;
27
+ return contents.slice(start, end);
28
+ });
29
+ }
30
+
31
+ //#endregion
32
+ export { computeLineOffsets, linesFromFileContents };
33
+ //# sourceMappingURL=computeFileOffsets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computeFileOffsets.js","names":["offsets: number[]"],"sources":["../../src/utils/computeFileOffsets.ts"],"sourcesContent":["const LINE_FEED = 10; // \\n\nconst CARRIAGE_RETURN = 13; // \\r\n\n/**\n * Computes line start offsets for a string.\n */\nexport function computeLineOffsets(contents: string): number[] {\n const offsets: number[] = [0];\n for (let i = 0; i < contents.length; i++) {\n const char = contents.charCodeAt(i);\n if (char === LINE_FEED || char === CARRIAGE_RETURN) {\n if (\n char === CARRIAGE_RETURN &&\n i + 1 < contents.length &&\n contents.charCodeAt(i + 1) === LINE_FEED\n ) {\n i++;\n }\n offsets.push(i + 1);\n }\n }\n return offsets;\n}\n\n/**\n * Splits file contents into lines aligned with {@link computeLineOffsets}.\n * Unlike splitFileContents, a trailing newline produces a final empty line.\n */\nexport function linesFromFileContents(contents: string): string[] {\n const offsets = computeLineOffsets(contents);\n const lines = Array.from({ length: offsets.length }, (_, i) => {\n const start = offsets[i];\n const end = offsets[i + 1] ?? contents.length;\n return contents.slice(start, end);\n });\n return lines;\n}\n"],"mappings":";AAAA,MAAM,YAAY;AAClB,MAAM,kBAAkB;;;;AAKxB,SAAgB,mBAAmB,UAA4B;CAC7D,MAAMA,UAAoB,CAAC,EAAE;AAC7B,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;EACxC,MAAM,OAAO,SAAS,WAAW,EAAE;AACnC,MAAI,SAAS,aAAa,SAAS,iBAAiB;AAClD,OACE,SAAS,mBACT,IAAI,IAAI,SAAS,UACjB,SAAS,WAAW,IAAI,EAAE,KAAK,UAE/B;AAEF,WAAQ,KAAK,IAAI,EAAE;;;AAGvB,QAAO;;;;;;AAOT,SAAgB,sBAAsB,UAA4B;CAChE,MAAM,UAAU,mBAAmB,SAAS;AAM5C,QALc,MAAM,KAAK,EAAE,QAAQ,QAAQ,QAAQ,GAAG,GAAG,MAAM;EAC7D,MAAM,QAAQ,QAAQ;EACtB,MAAM,MAAM,QAAQ,IAAI,MAAM,SAAS;AACvC,SAAO,SAAS,MAAM,OAAO,IAAI;GACjC"}
@@ -51,6 +51,15 @@ function createTransformerWithState(useTokenTransformer = false, useCSSClasses =
51
51
  } : null
52
52
  }];
53
53
  if (useCSSClasses) transformers.push(tokenStyleNormalizer, toClass);
54
+ if (useTokenTransformer) transformers.push({ line: (node) => {
55
+ if (node.type === "element" && node.children.length === 0) node.children.push({
56
+ type: "element",
57
+ tagName: "br",
58
+ properties: {},
59
+ children: []
60
+ });
61
+ return node;
62
+ } });
54
63
  return {
55
64
  state,
56
65
  transformers,
@@ -1 +1 @@
1
- {"version":3,"file":"createTransformerWithState.js","names":["state: SharedRenderState","transformers: ShikiTransformer[]","children: ElementContent[]","tokenStyleNormalizer: ShikiTransformer","style: Record<string, string>"],"sources":["../../src/utils/createTransformerWithState.ts"],"sourcesContent":["import {\n type ShikiTransformerStyleToClass,\n transformerStyleToClass,\n} from '@shikijs/transformers';\nimport type { ElementContent } from 'hast';\nimport type { ThemedToken } from 'shiki';\n\nimport type { SharedRenderState, ShikiTransformer } from '../types';\nimport { findCodeElement } from './hast_utils';\nimport { processLine } from './processLine';\nimport { wrapTokenFragments } from './wrapTokenFragments';\n\ninterface CreateTransformerWithStateReturn {\n state: SharedRenderState;\n transformers: ShikiTransformer[];\n toClass: ShikiTransformerStyleToClass;\n}\n\ntype TokenWithLineChar = ThemedToken & {\n __lineChar?: number;\n};\n\nexport function createTransformerWithState(\n useTokenTransformer = false,\n useCSSClasses = false\n): CreateTransformerWithStateReturn {\n const state: SharedRenderState = { lineInfo: [] };\n const transformers: ShikiTransformer[] = [\n {\n line(node) {\n // Remove the default class\n delete node.properties.class;\n return node;\n },\n pre(pre) {\n const code = findCodeElement(pre);\n const children: ElementContent[] = [];\n if (code != null) {\n let index = 1;\n for (const node of code.children) {\n if (node.type !== 'element') continue;\n if (useTokenTransformer) {\n wrapTokenFragments(node);\n }\n children.push(processLine(node, index, state));\n index++;\n }\n code.children = children;\n }\n return pre;\n },\n ...(useTokenTransformer\n ? {\n tokens(lines) {\n for (const line of lines) {\n let col = 0;\n for (const token of line) {\n const tokenWithOriginalRange = token as TokenWithLineChar;\n tokenWithOriginalRange.__lineChar ??= col;\n col += token.content.length;\n }\n }\n },\n preprocess(_code, options) {\n options.mergeWhitespaces = 'never';\n },\n span(hast, _line, _char, _lineElement, token) {\n if (token?.offset != null && token.content != null) {\n const tokenWithOriginalRange = token as TokenWithLineChar;\n const tokenChar = tokenWithOriginalRange.__lineChar;\n if (tokenChar != null) {\n hast.properties['data-char'] = tokenChar;\n }\n return hast;\n }\n return hast;\n },\n }\n : null),\n },\n ];\n if (useCSSClasses) {\n transformers.push(tokenStyleNormalizer, toClass);\n }\n return { state, transformers, toClass };\n}\n\nconst toClass = transformerStyleToClass({ classPrefix: 'hl-' });\n\n// Create a transformer that converts token color/fontStyle to htmlStyle\n// This needs to run BEFORE transformerStyleToClass\nconst tokenStyleNormalizer: ShikiTransformer = {\n name: 'token-style-normalizer',\n tokens(lines) {\n for (const line of lines) {\n for (const token of line) {\n // Skip if htmlStyle is already set\n if (token.htmlStyle != null) continue;\n\n const style: Record<string, string> = {};\n\n if (token.color != null) {\n style.color = token.color;\n }\n if (token.bgColor != null) {\n style['background-color'] = token.bgColor;\n }\n if (token.fontStyle != null && token.fontStyle !== 0) {\n // FontStyle is a bitmask: 1 = italic, 2 = bold, 4 = underline\n if ((token.fontStyle & 1) !== 0) {\n style['font-style'] = 'italic';\n }\n if ((token.fontStyle & 2) !== 0) {\n style['font-weight'] = 'bold';\n }\n if ((token.fontStyle & 4) !== 0) {\n style['text-decoration'] = 'underline';\n }\n }\n\n // Only set htmlStyle if we have any styles\n if (Object.keys(style).length > 0) {\n token.htmlStyle = style;\n }\n }\n }\n },\n};\n"],"mappings":";;;;;;AAsBA,SAAgB,2BACd,sBAAsB,OACtB,gBAAgB,OACkB;CAClC,MAAMA,QAA2B,EAAE,UAAU,EAAE,EAAE;CACjD,MAAMC,eAAmC,CACvC;EACE,KAAK,MAAM;AAET,UAAO,KAAK,WAAW;AACvB,UAAO;;EAET,IAAI,KAAK;GACP,MAAM,OAAO,gBAAgB,IAAI;GACjC,MAAMC,WAA6B,EAAE;AACrC,OAAI,QAAQ,MAAM;IAChB,IAAI,QAAQ;AACZ,SAAK,MAAM,QAAQ,KAAK,UAAU;AAChC,SAAI,KAAK,SAAS,UAAW;AAC7B,SAAI,oBACF,oBAAmB,KAAK;AAE1B,cAAS,KAAK,YAAY,MAAM,OAAO,MAAM,CAAC;AAC9C;;AAEF,SAAK,WAAW;;AAElB,UAAO;;EAET,GAAI,sBACA;GACE,OAAO,OAAO;AACZ,SAAK,MAAM,QAAQ,OAAO;KACxB,IAAI,MAAM;AACV,UAAK,MAAM,SAAS,MAAM;MACxB,MAAM,yBAAyB;AAC/B,6BAAuB,eAAe;AACtC,aAAO,MAAM,QAAQ;;;;GAI3B,WAAW,OAAO,SAAS;AACzB,YAAQ,mBAAmB;;GAE7B,KAAK,MAAM,OAAO,OAAO,cAAc,OAAO;AAC5C,QAAI,OAAO,UAAU,QAAQ,MAAM,WAAW,MAAM;KAElD,MAAM,YADyB,MACU;AACzC,SAAI,aAAa,KACf,MAAK,WAAW,eAAe;AAEjC,YAAO;;AAET,WAAO;;GAEV,GACD;EACL,CACF;AACD,KAAI,cACF,cAAa,KAAK,sBAAsB,QAAQ;AAElD,QAAO;EAAE;EAAO;EAAc;EAAS;;AAGzC,MAAM,UAAU,wBAAwB,EAAE,aAAa,OAAO,CAAC;AAI/D,MAAMC,uBAAyC;CAC7C,MAAM;CACN,OAAO,OAAO;AACZ,OAAK,MAAM,QAAQ,MACjB,MAAK,MAAM,SAAS,MAAM;AAExB,OAAI,MAAM,aAAa,KAAM;GAE7B,MAAMC,QAAgC,EAAE;AAExC,OAAI,MAAM,SAAS,KACjB,OAAM,QAAQ,MAAM;AAEtB,OAAI,MAAM,WAAW,KACnB,OAAM,sBAAsB,MAAM;AAEpC,OAAI,MAAM,aAAa,QAAQ,MAAM,cAAc,GAAG;AAEpD,SAAK,MAAM,YAAY,OAAO,EAC5B,OAAM,gBAAgB;AAExB,SAAK,MAAM,YAAY,OAAO,EAC5B,OAAM,iBAAiB;AAEzB,SAAK,MAAM,YAAY,OAAO,EAC5B,OAAM,qBAAqB;;AAK/B,OAAI,OAAO,KAAK,MAAM,CAAC,SAAS,EAC9B,OAAM,YAAY;;;CAK3B"}
1
+ {"version":3,"file":"createTransformerWithState.js","names":["state: SharedRenderState","transformers: ShikiTransformer[]","children: ElementContent[]","tokenStyleNormalizer: ShikiTransformer","style: Record<string, string>"],"sources":["../../src/utils/createTransformerWithState.ts"],"sourcesContent":["import {\n type ShikiTransformerStyleToClass,\n transformerStyleToClass,\n} from '@shikijs/transformers';\nimport type { ElementContent } from 'hast';\nimport type { ThemedToken } from 'shiki';\n\nimport type { SharedRenderState, ShikiTransformer } from '../types';\nimport { findCodeElement } from './hast_utils';\nimport { processLine } from './processLine';\nimport { wrapTokenFragments } from './wrapTokenFragments';\n\ninterface CreateTransformerWithStateReturn {\n state: SharedRenderState;\n transformers: ShikiTransformer[];\n toClass: ShikiTransformerStyleToClass;\n}\n\ntype TokenWithLineChar = ThemedToken & {\n __lineChar?: number;\n};\n\nexport function createTransformerWithState(\n useTokenTransformer = false,\n useCSSClasses = false\n): CreateTransformerWithStateReturn {\n const state: SharedRenderState = { lineInfo: [] };\n const transformers: ShikiTransformer[] = [\n {\n line(node) {\n // Remove the default class\n delete node.properties.class;\n return node;\n },\n pre(pre) {\n const code = findCodeElement(pre);\n const children: ElementContent[] = [];\n if (code != null) {\n let index = 1;\n for (const node of code.children) {\n if (node.type !== 'element') continue;\n if (useTokenTransformer) {\n wrapTokenFragments(node);\n }\n children.push(processLine(node, index, state));\n index++;\n }\n code.children = children;\n }\n return pre;\n },\n ...(useTokenTransformer\n ? {\n tokens(lines) {\n for (const line of lines) {\n let col = 0;\n for (const token of line) {\n const tokenWithOriginalRange = token as TokenWithLineChar;\n tokenWithOriginalRange.__lineChar ??= col;\n col += token.content.length;\n }\n }\n },\n preprocess(_code, options) {\n options.mergeWhitespaces = 'never';\n },\n span(hast, _line, _char, _lineElement, token) {\n if (token?.offset != null && token.content != null) {\n const tokenWithOriginalRange = token as TokenWithLineChar;\n const tokenChar = tokenWithOriginalRange.__lineChar;\n if (tokenChar != null) {\n hast.properties['data-char'] = tokenChar;\n }\n return hast;\n }\n return hast;\n },\n }\n : null),\n },\n ];\n if (useCSSClasses) {\n transformers.push(tokenStyleNormalizer, toClass);\n }\n if (useTokenTransformer) {\n // shiki renders empty lines as \" \" that breaks the editor selection.\n // We replace them with <br> tags.\n transformers.push({\n line: (node) => {\n if (node.type === 'element' && node.children.length === 0) {\n node.children.push({\n type: 'element',\n tagName: 'br',\n properties: {},\n children: [],\n });\n }\n return node;\n },\n });\n }\n return { state, transformers, toClass };\n}\n\nconst toClass = transformerStyleToClass({ classPrefix: 'hl-' });\n\n// Create a transformer that converts token color/fontStyle to htmlStyle\n// This needs to run BEFORE transformerStyleToClass\nconst tokenStyleNormalizer: ShikiTransformer = {\n name: 'token-style-normalizer',\n tokens(lines) {\n for (const line of lines) {\n for (const token of line) {\n // Skip if htmlStyle is already set\n if (token.htmlStyle != null) continue;\n\n const style: Record<string, string> = {};\n\n if (token.color != null) {\n style.color = token.color;\n }\n if (token.bgColor != null) {\n style['background-color'] = token.bgColor;\n }\n if (token.fontStyle != null && token.fontStyle !== 0) {\n // FontStyle is a bitmask: 1 = italic, 2 = bold, 4 = underline\n if ((token.fontStyle & 1) !== 0) {\n style['font-style'] = 'italic';\n }\n if ((token.fontStyle & 2) !== 0) {\n style['font-weight'] = 'bold';\n }\n if ((token.fontStyle & 4) !== 0) {\n style['text-decoration'] = 'underline';\n }\n }\n\n // Only set htmlStyle if we have any styles\n if (Object.keys(style).length > 0) {\n token.htmlStyle = style;\n }\n }\n }\n },\n};\n"],"mappings":";;;;;;AAsBA,SAAgB,2BACd,sBAAsB,OACtB,gBAAgB,OACkB;CAClC,MAAMA,QAA2B,EAAE,UAAU,EAAE,EAAE;CACjD,MAAMC,eAAmC,CACvC;EACE,KAAK,MAAM;AAET,UAAO,KAAK,WAAW;AACvB,UAAO;;EAET,IAAI,KAAK;GACP,MAAM,OAAO,gBAAgB,IAAI;GACjC,MAAMC,WAA6B,EAAE;AACrC,OAAI,QAAQ,MAAM;IAChB,IAAI,QAAQ;AACZ,SAAK,MAAM,QAAQ,KAAK,UAAU;AAChC,SAAI,KAAK,SAAS,UAAW;AAC7B,SAAI,oBACF,oBAAmB,KAAK;AAE1B,cAAS,KAAK,YAAY,MAAM,OAAO,MAAM,CAAC;AAC9C;;AAEF,SAAK,WAAW;;AAElB,UAAO;;EAET,GAAI,sBACA;GACE,OAAO,OAAO;AACZ,SAAK,MAAM,QAAQ,OAAO;KACxB,IAAI,MAAM;AACV,UAAK,MAAM,SAAS,MAAM;MACxB,MAAM,yBAAyB;AAC/B,6BAAuB,eAAe;AACtC,aAAO,MAAM,QAAQ;;;;GAI3B,WAAW,OAAO,SAAS;AACzB,YAAQ,mBAAmB;;GAE7B,KAAK,MAAM,OAAO,OAAO,cAAc,OAAO;AAC5C,QAAI,OAAO,UAAU,QAAQ,MAAM,WAAW,MAAM;KAElD,MAAM,YADyB,MACU;AACzC,SAAI,aAAa,KACf,MAAK,WAAW,eAAe;AAEjC,YAAO;;AAET,WAAO;;GAEV,GACD;EACL,CACF;AACD,KAAI,cACF,cAAa,KAAK,sBAAsB,QAAQ;AAElD,KAAI,oBAGF,cAAa,KAAK,EAChB,OAAO,SAAS;AACd,MAAI,KAAK,SAAS,aAAa,KAAK,SAAS,WAAW,EACtD,MAAK,SAAS,KAAK;GACjB,MAAM;GACN,SAAS;GACT,YAAY,EAAE;GACd,UAAU,EAAE;GACb,CAAC;AAEJ,SAAO;IAEV,CAAC;AAEJ,QAAO;EAAE;EAAO;EAAc;EAAS;;AAGzC,MAAM,UAAU,wBAAwB,EAAE,aAAa,OAAO,CAAC;AAI/D,MAAMC,uBAAyC;CAC7C,MAAM;CACN,OAAO,OAAO;AACZ,OAAK,MAAM,QAAQ,MACjB,MAAK,MAAM,SAAS,MAAM;AAExB,OAAI,MAAM,aAAa,KAAM;GAE7B,MAAMC,QAAgC,EAAE;AAExC,OAAI,MAAM,SAAS,KACjB,OAAM,QAAQ,MAAM;AAEtB,OAAI,MAAM,WAAW,KACnB,OAAM,sBAAsB,MAAM;AAEpC,OAAI,MAAM,aAAa,QAAQ,MAAM,cAAc,GAAG;AAEpD,SAAK,MAAM,YAAY,OAAO,EAC5B,OAAM,gBAAgB;AAExB,SAAK,MAAM,YAAY,OAAO,EAC5B,OAAM,iBAAiB;AAEzB,SAAK,MAAM,YAAY,OAAO,EAC5B,OAAM,qBAAqB;;AAK/B,OAAI,OAAO,KAAK,MAAM,CAAC,SAAS,EAC9B,OAAM,YAAY;;;CAK3B"}
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_COLLAPSED_CONTEXT_THRESHOLD } from "../constants.js";
2
- import { getExpandedRegion, getTrailingExpandedRegion } from "./virtualDiffLayout.js";
2
+ import { getExpandedRegion } from "./virtualDiffLayout.js";
3
3
 
4
4
  //#region src/utils/iterateOverDiff.ts
5
5
  function iterateOverDiff({ diff, diffStyle, startingLine = 0, totalLines = Infinity, expandedHunks, collapsedContextThreshold = DEFAULT_COLLAPSED_CONTEXT_THRESHOLD, callback }) {
@@ -11,12 +11,12 @@ function iterateOverDiff({ diff, diffStyle, startingLine = 0, totalLines = Infin
11
11
  collapsedContextThreshold
12
12
  });
13
13
  const state = {
14
+ finalHunk: diff.hunks.at(-1),
14
15
  viewportStart: startingLine,
15
16
  viewportEnd: startingLine + totalLines,
16
17
  isWindowedHighlight: startingLine > 0 || totalLines < Infinity,
17
18
  splitCount: iterationStart.splitCount,
18
19
  unifiedCount: iterationStart.unifiedCount,
19
- finalHunkIndex: diff.hunks.length - 1,
20
20
  shouldBreak() {
21
21
  if (!state.isWindowedHighlight) return false;
22
22
  const breakUnified = state.unifiedCount >= startingLine + totalLines;
@@ -69,24 +69,31 @@ function iterateOverDiff({ diff, diffStyle, startingLine = 0, totalLines = Infin
69
69
  hunkIndex,
70
70
  collapsedContextThreshold
71
71
  });
72
- const trailingRegion = hunkIndex === state.finalHunkIndex ? getTrailingExpandedRegion({
73
- fileDiff: diff,
74
- hunkIndex,
75
- expandedHunks,
76
- collapsedContextThreshold,
77
- errorPrefix: "iterateOverDiff"
78
- }) : void 0;
72
+ const trailingRegion = (() => {
73
+ if (hunk !== state.finalHunk || !hasFinalCollapsedHunk(diff)) return;
74
+ const additionRemaining = diff.additionLines.length - (hunk.additionLineIndex + hunk.additionCount);
75
+ const deletionRemaining = diff.deletionLines.length - (hunk.deletionLineIndex + hunk.deletionCount);
76
+ if (additionRemaining !== deletionRemaining) throw new Error(`iterateOverDiff: trailing context mismatch (additions=${additionRemaining}, deletions=${deletionRemaining}) for ${diff.name}`);
77
+ const trailingRangeSize = Math.min(additionRemaining, deletionRemaining);
78
+ return getExpandedRegion({
79
+ isPartial: diff.isPartial,
80
+ rangeSize: trailingRangeSize,
81
+ expandedHunks,
82
+ hunkIndex: diff.hunks.length,
83
+ collapsedContextThreshold
84
+ });
85
+ })();
79
86
  const expandedLineCount = leadingRegion.fromStart + leadingRegion.fromEnd;
80
87
  function getTrailingCollapsedAfter(unifiedLineIndex$1, splitLineIndex$1) {
81
88
  if (trailingRegion == null || trailingRegion.collapsedLines <= 0 || trailingRegion.fromStart + trailingRegion.fromEnd > 0) return 0;
82
89
  if (diffStyle === "unified") return unifiedLineIndex$1 === hunk.unifiedLineStart + hunk.unifiedLineCount - 1 ? trailingRegion.collapsedLines : 0;
83
90
  return splitLineIndex$1 === hunk.splitLineStart + hunk.splitLineCount - 1 ? trailingRegion.collapsedLines : 0;
84
91
  }
85
- let consumedCollapsed = leadingRegion.collapsedLines === 0;
86
- function consumePendingCollapsed() {
87
- if (consumedCollapsed) return 0;
88
- consumedCollapsed = true;
89
- return leadingRegion.collapsedLines;
92
+ function getPendingCollapsed() {
93
+ if (leadingRegion.collapsedLines === 0) return 0;
94
+ const value = leadingRegion.collapsedLines;
95
+ leadingRegion.collapsedLines = 0;
96
+ return value;
90
97
  }
91
98
  if (!state.shouldSkip(expandedLineCount, expandedLineCount)) {
92
99
  let unifiedLineIndex$1 = hunk.unifiedLineStart - leadingRegion.rangeSize;
@@ -95,63 +102,81 @@ function iterateOverDiff({ diff, diffStyle, startingLine = 0, totalLines = Infin
95
102
  let additionLineIndex$1 = hunk.additionLineIndex - leadingRegion.rangeSize;
96
103
  let deletionLineNumber$1 = hunk.deletionStart - leadingRegion.rangeSize;
97
104
  let additionLineNumber$1 = hunk.additionStart - leadingRegion.rangeSize;
98
- if (walkContextLines(state, leadingRegion.fromStart, diffStyle, (index) => {
99
- return state.emit({
100
- hunkIndex,
101
- hunk,
102
- collapsedBefore: 0,
103
- collapsedAfter: 0,
104
- type: "context-expanded",
105
- deletionLine: {
106
- lineNumber: deletionLineNumber$1 + index,
107
- lineIndex: deletionLineIndex$1 + index,
108
- noEOFCR: false,
109
- unifiedLineIndex: unifiedLineIndex$1 + index,
110
- splitLineIndex: splitLineIndex$1 + index
111
- },
112
- additionLine: {
113
- unifiedLineIndex: unifiedLineIndex$1 + index,
114
- splitLineIndex: splitLineIndex$1 + index,
115
- lineIndex: additionLineIndex$1 + index,
116
- lineNumber: additionLineNumber$1 + index,
117
- noEOFCR: false
118
- }
119
- });
120
- })) break hunkIterator;
105
+ const [startIndex, endIndex] = getEqualLineIterationRange(state, leadingRegion.fromStart, diffStyle);
106
+ if (startIndex > 0) state.incrementCounts(startIndex, startIndex);
107
+ let index = startIndex;
108
+ while (index < leadingRegion.fromStart) {
109
+ if (index >= endIndex) {
110
+ state.incrementCounts(leadingRegion.fromStart - index, leadingRegion.fromStart - index);
111
+ break;
112
+ }
113
+ if (state.isInWindow(0, 0)) {
114
+ if (state.emit({
115
+ hunkIndex,
116
+ hunk,
117
+ collapsedBefore: 0,
118
+ collapsedAfter: 0,
119
+ type: "context-expanded",
120
+ deletionLine: {
121
+ lineNumber: deletionLineNumber$1 + index,
122
+ lineIndex: deletionLineIndex$1 + index,
123
+ noEOFCR: false,
124
+ unifiedLineIndex: unifiedLineIndex$1 + index,
125
+ splitLineIndex: splitLineIndex$1 + index
126
+ },
127
+ additionLine: {
128
+ unifiedLineIndex: unifiedLineIndex$1 + index,
129
+ splitLineIndex: splitLineIndex$1 + index,
130
+ lineIndex: additionLineIndex$1 + index,
131
+ lineNumber: additionLineNumber$1 + index,
132
+ noEOFCR: false
133
+ }
134
+ })) break hunkIterator;
135
+ } else state.incrementCounts(1, 1);
136
+ index++;
137
+ }
121
138
  unifiedLineIndex$1 = hunk.unifiedLineStart - leadingRegion.fromEnd;
122
139
  splitLineIndex$1 = hunk.splitLineStart - leadingRegion.fromEnd;
123
140
  deletionLineIndex$1 = hunk.deletionLineIndex - leadingRegion.fromEnd;
124
141
  additionLineIndex$1 = hunk.additionLineIndex - leadingRegion.fromEnd;
125
142
  deletionLineNumber$1 = hunk.deletionStart - leadingRegion.fromEnd;
126
143
  additionLineNumber$1 = hunk.additionStart - leadingRegion.fromEnd;
127
- if (walkContextLines(state, leadingRegion.fromEnd, diffStyle, (index) => {
128
- return state.emit({
129
- hunkIndex,
130
- hunk,
131
- collapsedBefore: consumePendingCollapsed(),
132
- collapsedAfter: 0,
133
- type: "context-expanded",
134
- deletionLine: {
135
- lineNumber: deletionLineNumber$1 + index,
136
- lineIndex: deletionLineIndex$1 + index,
137
- noEOFCR: false,
138
- unifiedLineIndex: unifiedLineIndex$1 + index,
139
- splitLineIndex: splitLineIndex$1 + index
140
- },
141
- additionLine: {
142
- unifiedLineIndex: unifiedLineIndex$1 + index,
143
- splitLineIndex: splitLineIndex$1 + index,
144
- lineIndex: additionLineIndex$1 + index,
145
- lineNumber: additionLineNumber$1 + index,
146
- noEOFCR: false
147
- }
148
- });
149
- }, () => {
150
- consumePendingCollapsed();
151
- })) break hunkIterator;
144
+ const [fromEndStartIndex, fromEndEndIndex] = getEqualLineIterationRange(state, leadingRegion.fromEnd, diffStyle);
145
+ if (fromEndStartIndex > 0) state.incrementCounts(fromEndStartIndex, fromEndStartIndex);
146
+ index = fromEndStartIndex;
147
+ while (index < leadingRegion.fromEnd) {
148
+ if (index >= fromEndEndIndex) {
149
+ state.incrementCounts(leadingRegion.fromEnd - index, leadingRegion.fromEnd - index);
150
+ break;
151
+ }
152
+ if (state.isInWindow(0, 0)) {
153
+ if (state.emit({
154
+ hunkIndex,
155
+ hunk,
156
+ collapsedBefore: getPendingCollapsed(),
157
+ collapsedAfter: 0,
158
+ type: "context-expanded",
159
+ deletionLine: {
160
+ lineNumber: deletionLineNumber$1 + index,
161
+ lineIndex: deletionLineIndex$1 + index,
162
+ noEOFCR: false,
163
+ unifiedLineIndex: unifiedLineIndex$1 + index,
164
+ splitLineIndex: splitLineIndex$1 + index
165
+ },
166
+ additionLine: {
167
+ unifiedLineIndex: unifiedLineIndex$1 + index,
168
+ splitLineIndex: splitLineIndex$1 + index,
169
+ lineIndex: additionLineIndex$1 + index,
170
+ lineNumber: additionLineNumber$1 + index,
171
+ noEOFCR: false
172
+ }
173
+ })) break hunkIterator;
174
+ } else state.incrementCounts(1, 1);
175
+ index++;
176
+ }
152
177
  } else {
153
178
  state.incrementCounts(expandedLineCount, expandedLineCount);
154
- consumePendingCollapsed();
179
+ getPendingCollapsed();
155
180
  }
156
181
  let unifiedLineIndex = hunk.unifiedLineStart;
157
182
  let splitLineIndex = hunk.splitLineStart;
@@ -165,37 +190,45 @@ function iterateOverDiff({ diff, diffStyle, startingLine = 0, totalLines = Infin
165
190
  const isLastContent = content === lastContent;
166
191
  if (content.type === "context") {
167
192
  if (!state.shouldSkip(content.lines, content.lines)) {
168
- if (walkContextLines(state, content.lines, diffStyle, (index) => {
169
- const isLastLine = isLastContent && index === content.lines - 1;
170
- const unifiedRowIndex = unifiedLineIndex + index;
171
- const splitRowIndex = splitLineIndex + index;
172
- return state.emit({
173
- hunkIndex,
174
- hunk,
175
- collapsedBefore: consumePendingCollapsed(),
176
- collapsedAfter: getTrailingCollapsedAfter(unifiedRowIndex, splitRowIndex),
177
- type: "context",
178
- deletionLine: {
179
- lineNumber: deletionLineNumber + index,
180
- lineIndex: deletionLineIndex + index,
181
- noEOFCR: isLastLine && hunk.noEOFCRDeletions,
182
- unifiedLineIndex: unifiedRowIndex,
183
- splitLineIndex: splitRowIndex
184
- },
185
- additionLine: {
186
- unifiedLineIndex: unifiedRowIndex,
187
- splitLineIndex: splitRowIndex,
188
- lineIndex: additionLineIndex + index,
189
- lineNumber: additionLineNumber + index,
190
- noEOFCR: isLastLine && hunk.noEOFCRAdditions
191
- }
192
- });
193
- }, () => {
194
- consumePendingCollapsed();
195
- })) break hunkIterator;
193
+ const [startIndex, endIndex] = getEqualLineIterationRange(state, content.lines, diffStyle);
194
+ if (startIndex > 0) state.incrementCounts(startIndex, startIndex);
195
+ let index = startIndex;
196
+ while (index < content.lines) {
197
+ if (index >= endIndex) {
198
+ state.incrementCounts(content.lines - index, content.lines - index);
199
+ break;
200
+ }
201
+ if (state.isInWindow(0, 0)) {
202
+ const isLastLine = isLastContent && index === content.lines - 1;
203
+ const unifiedRowIndex = unifiedLineIndex + index;
204
+ const splitRowIndex = splitLineIndex + index;
205
+ if (state.emit({
206
+ hunkIndex,
207
+ hunk,
208
+ collapsedBefore: getPendingCollapsed(),
209
+ collapsedAfter: getTrailingCollapsedAfter(unifiedRowIndex, splitRowIndex),
210
+ type: "context",
211
+ deletionLine: {
212
+ lineNumber: deletionLineNumber + index,
213
+ lineIndex: deletionLineIndex + index,
214
+ noEOFCR: isLastLine && hunk.noEOFCRDeletions,
215
+ unifiedLineIndex: unifiedRowIndex,
216
+ splitLineIndex: splitRowIndex
217
+ },
218
+ additionLine: {
219
+ unifiedLineIndex: unifiedRowIndex,
220
+ splitLineIndex: splitRowIndex,
221
+ lineIndex: additionLineIndex + index,
222
+ lineNumber: additionLineNumber + index,
223
+ noEOFCR: isLastLine && hunk.noEOFCRAdditions
224
+ }
225
+ })) break hunkIterator;
226
+ } else state.incrementCounts(1, 1);
227
+ index++;
228
+ }
196
229
  } else {
197
230
  state.incrementCounts(content.lines, content.lines);
198
- consumePendingCollapsed();
231
+ getPendingCollapsed();
199
232
  }
200
233
  unifiedLineIndex += content.lines;
201
234
  splitLineIndex += content.lines;
@@ -208,13 +241,12 @@ function iterateOverDiff({ diff, diffStyle, startingLine = 0, totalLines = Infin
208
241
  const unifiedCount = content.deletions + content.additions;
209
242
  if (!state.shouldSkip(unifiedCount, splitCount)) {
210
243
  const iterationRanges = getChangeIterationRanges(state, content, diffStyle);
211
- if ((iterationRanges[0]?.[0] ?? 0) > 0) consumePendingCollapsed();
212
244
  for (const [rangeStart, rangeEnd] of iterationRanges) for (let index = rangeStart; index < rangeEnd; index++) {
213
245
  const collapsedAfter = getTrailingCollapsedAfter(unifiedLineIndex + index, diffStyle === "unified" ? splitLineIndex + (index < content.deletions ? index : index - content.deletions) : splitLineIndex + index);
214
246
  if (state.emit(getChangeLineData({
215
247
  hunkIndex,
216
248
  hunk,
217
- collapsedBefore: consumePendingCollapsed(),
249
+ collapsedBefore: getPendingCollapsed(),
218
250
  collapsedAfter,
219
251
  diffStyle,
220
252
  index,
@@ -231,7 +263,7 @@ function iterateOverDiff({ diff, diffStyle, startingLine = 0, totalLines = Infin
231
263
  }), true)) break hunkIterator;
232
264
  }
233
265
  }
234
- consumePendingCollapsed();
266
+ getPendingCollapsed();
235
267
  state.incrementCounts(unifiedCount, splitCount);
236
268
  unifiedLineIndex += unifiedCount;
237
269
  splitLineIndex += splitCount;
@@ -244,30 +276,41 @@ function iterateOverDiff({ diff, diffStyle, startingLine = 0, totalLines = Infin
244
276
  if (trailingRegion != null) {
245
277
  const { collapsedLines, fromStart, fromEnd } = trailingRegion;
246
278
  const len = fromStart + fromEnd;
247
- if (walkContextLines(state, len, diffStyle, (index) => {
248
- const isLastLine = index === len - 1;
249
- return state.emit({
250
- hunkIndex: diff.hunks.length,
251
- hunk: void 0,
252
- collapsedBefore: 0,
253
- collapsedAfter: isLastLine ? collapsedLines : 0,
254
- type: "context-expanded",
255
- deletionLine: {
256
- lineNumber: deletionLineNumber + index,
257
- lineIndex: deletionLineIndex + index,
258
- noEOFCR: false,
259
- unifiedLineIndex: unifiedLineIndex + index,
260
- splitLineIndex: splitLineIndex + index
261
- },
262
- additionLine: {
263
- unifiedLineIndex: unifiedLineIndex + index,
264
- splitLineIndex: splitLineIndex + index,
265
- lineIndex: additionLineIndex + index,
266
- lineNumber: additionLineNumber + index,
267
- noEOFCR: false
268
- }
269
- });
270
- }, void 0, () => state.shouldBreak())) break hunkIterator;
279
+ const [startIndex, endIndex] = getEqualLineIterationRange(state, len, diffStyle);
280
+ if (startIndex > 0) state.incrementCounts(startIndex, startIndex);
281
+ let index = startIndex;
282
+ while (index < len) {
283
+ if (state.shouldBreak()) break hunkIterator;
284
+ if (index >= endIndex) {
285
+ state.incrementCounts(len - index, len - index);
286
+ break;
287
+ }
288
+ if (state.isInWindow(0, 0)) {
289
+ const isLastLine = index === len - 1;
290
+ if (state.emit({
291
+ hunkIndex: diff.hunks.length,
292
+ hunk: void 0,
293
+ collapsedBefore: 0,
294
+ collapsedAfter: isLastLine ? collapsedLines : 0,
295
+ type: "context-expanded",
296
+ deletionLine: {
297
+ lineNumber: deletionLineNumber + index,
298
+ lineIndex: deletionLineIndex + index,
299
+ noEOFCR: false,
300
+ unifiedLineIndex: unifiedLineIndex + index,
301
+ splitLineIndex: splitLineIndex + index
302
+ },
303
+ additionLine: {
304
+ unifiedLineIndex: unifiedLineIndex + index,
305
+ splitLineIndex: splitLineIndex + index,
306
+ lineIndex: additionLineIndex + index,
307
+ lineNumber: additionLineNumber + index,
308
+ noEOFCR: false
309
+ }
310
+ })) break hunkIterator;
311
+ } else state.incrementCounts(1, 1);
312
+ index++;
313
+ }
271
314
  }
272
315
  }
273
316
  }
@@ -332,14 +375,15 @@ function getHunkPrefixCounts({ diff, expandedHunks, collapsedContextThreshold })
332
375
  const leadingCount = leadingRegion.fromStart + leadingRegion.fromEnd;
333
376
  splitCount += leadingCount + hunk.splitLineCount;
334
377
  unifiedCount += leadingCount + hunk.unifiedLineCount;
335
- const trailingRegion = index === finalHunkIndex ? getTrailingExpandedRegion({
336
- fileDiff: diff,
337
- hunkIndex: index,
338
- expandedHunks,
339
- collapsedContextThreshold,
340
- errorPrefix: "iterateOverDiff"
341
- }) : void 0;
342
- if (trailingRegion != null) {
378
+ if (index === finalHunkIndex && hasFinalCollapsedHunk(diff)) {
379
+ const trailingRangeSize = getTrailingRangeSize(diff, hunk);
380
+ const trailingRegion = getExpandedRegion({
381
+ isPartial: diff.isPartial,
382
+ rangeSize: trailingRangeSize,
383
+ expandedHunks,
384
+ hunkIndex: diff.hunks.length,
385
+ collapsedContextThreshold
386
+ });
343
387
  const trailingCount = trailingRegion.fromStart + trailingRegion.fromEnd;
344
388
  splitCount += trailingCount;
345
389
  unifiedCount += trailingCount;
@@ -351,7 +395,7 @@ function getHunkPrefixCounts({ diff, expandedHunks, collapsedContextThreshold })
351
395
  }
352
396
  return prefixCounts;
353
397
  }
354
- function getContextLineIterationBounds(state, count, diffStyle) {
398
+ function getEqualLineIterationRange(state, count, diffStyle) {
355
399
  if (!state.isWindowedHighlight || count <= 0) return [0, count];
356
400
  const ranges = [];
357
401
  function pushRange(currentCount) {
@@ -371,25 +415,16 @@ function getContextLineIterationBounds(state, count, diffStyle) {
371
415
  }
372
416
  return [start, end];
373
417
  }
374
- function walkContextLines(state, count, diffStyle, callback, onSkippedStart, shouldBreak) {
375
- const [startIndex, endIndex] = getContextLineIterationBounds(state, count, diffStyle);
376
- if (startIndex > 0) {
377
- state.incrementCounts(startIndex, startIndex);
378
- onSkippedStart?.();
379
- }
380
- let index = startIndex;
381
- while (index < count) {
382
- if (shouldBreak?.() === true) return true;
383
- if (index >= endIndex) {
384
- state.incrementCounts(count - index, count - index);
385
- break;
386
- }
387
- if (state.isInWindow(0, 0)) {
388
- if (callback(index) === true) return true;
389
- } else state.incrementCounts(1, 1);
390
- index++;
391
- }
392
- return false;
418
+ function getTrailingRangeSize(diff, hunk) {
419
+ const additionRemaining = diff.additionLines.length - (hunk.additionLineIndex + hunk.additionCount);
420
+ const deletionRemaining = diff.deletionLines.length - (hunk.deletionLineIndex + hunk.deletionCount);
421
+ if (additionRemaining !== deletionRemaining) throw new Error(`iterateOverDiff: trailing context mismatch (additions=${additionRemaining}, deletions=${deletionRemaining}) for ${diff.name}`);
422
+ return Math.min(additionRemaining, deletionRemaining);
423
+ }
424
+ function hasFinalCollapsedHunk(diff) {
425
+ const lastHunk = diff.hunks.at(-1);
426
+ if (lastHunk == null || diff.isPartial || diff.additionLines.length === 0 || diff.deletionLines.length === 0) return false;
427
+ return lastHunk.additionLineIndex + lastHunk.additionCount < diff.additionLines.length || lastHunk.deletionLineIndex + lastHunk.deletionCount < diff.deletionLines.length;
393
428
  }
394
429
  function getChangeIterationRanges(state, content, diffStyle) {
395
430
  if (!state.isWindowedHighlight) return [[0, diffStyle === "unified" ? content.deletions + content.additions : Math.max(content.deletions, content.additions)]];