@singapore-editor/diff 0.2.0

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 (60) hide show
  1. package/README.md +158 -0
  2. package/dist/diffGutter.d.ts +37 -0
  3. package/dist/diffGutter.d.ts.map +1 -0
  4. package/dist/diffGutter.js +189 -0
  5. package/dist/diffGutter.js.map +1 -0
  6. package/dist/diffRows.d.ts +5 -0
  7. package/dist/diffRows.d.ts.map +1 -0
  8. package/dist/diffRows.js +40 -0
  9. package/dist/diffRows.js.map +1 -0
  10. package/dist/diffSyntax.d.ts +124 -0
  11. package/dist/diffSyntax.d.ts.map +1 -0
  12. package/dist/diffSyntax.js +491 -0
  13. package/dist/diffSyntax.js.map +1 -0
  14. package/dist/editorDiffPlugin.d.ts +74 -0
  15. package/dist/editorDiffPlugin.d.ts.map +1 -0
  16. package/dist/editorDiffPlugin.js +546 -0
  17. package/dist/editorDiffPlugin.js.map +1 -0
  18. package/dist/editorOptions.d.ts +15 -0
  19. package/dist/editorOptions.d.ts.map +1 -0
  20. package/dist/editorOptions.js +33 -0
  21. package/dist/editorOptions.js.map +1 -0
  22. package/dist/gutters.d.ts +53 -0
  23. package/dist/gutters.d.ts.map +1 -0
  24. package/dist/gutters.js +122 -0
  25. package/dist/gutters.js.map +1 -0
  26. package/dist/index.d.ts +18 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +11 -0
  29. package/dist/inline.d.ts +3 -0
  30. package/dist/inline.d.ts.map +1 -0
  31. package/dist/inline.js +72 -0
  32. package/dist/inline.js.map +1 -0
  33. package/dist/lines.d.ts +8 -0
  34. package/dist/lines.d.ts.map +1 -0
  35. package/dist/lines.js +49 -0
  36. package/dist/lines.js.map +1 -0
  37. package/dist/liveProjection.d.ts +11 -0
  38. package/dist/liveProjection.d.ts.map +1 -0
  39. package/dist/liveProjection.js +152 -0
  40. package/dist/liveProjection.js.map +1 -0
  41. package/dist/model.d.ts +4 -0
  42. package/dist/model.d.ts.map +1 -0
  43. package/dist/model.js +247 -0
  44. package/dist/model.js.map +1 -0
  45. package/dist/projection.d.ts +18 -0
  46. package/dist/projection.d.ts.map +1 -0
  47. package/dist/projection.js +281 -0
  48. package/dist/projection.js.map +1 -0
  49. package/dist/regions.d.ts +28 -0
  50. package/dist/regions.d.ts.map +1 -0
  51. package/dist/regions.js +0 -0
  52. package/dist/regions.js.map +1 -0
  53. package/dist/style.css +145 -0
  54. package/dist/theme.d.ts +2 -0
  55. package/dist/theme.d.ts.map +1 -0
  56. package/dist/theme.js +43 -0
  57. package/dist/theme.js.map +1 -0
  58. package/dist/types.d.ts +78 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/package.json +44 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,qBAAqB,EACrB,QAAQ,EAKR,oBAAoB,EACrB,MAAM,SAAS,CAAA;AAgChB,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,QAAQ,CA2BvE;AAED,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,oBAAyB,GACjC,SAAS,QAAQ,EAAE,CASrB"}
package/dist/model.js ADDED
@@ -0,0 +1,247 @@
1
+ import { languageIdForPath, normalizeContextLines, splitTextLines, stripDiffPathPrefix } from "./lines.js";
2
+ import { annotateInlineChanges } from "./inline.js";
3
+ import { parsePatch, structuredPatch } from "diff";
4
+ //#region src/model.ts
5
+ function createTextDiff(options) {
6
+ const oldFile = options.oldFile ?? null;
7
+ const newFile = options.newFile ?? null;
8
+ const oldText = oldFile?.text ?? "";
9
+ const newText = newFile?.text ?? "";
10
+ const oldPath = oldFile?.path ?? newFile?.path ?? "";
11
+ const newPath = newFile?.path ?? oldFile?.path ?? "";
12
+ const patch = structuredPatch(oldPath, newPath, oldText, newText, void 0, void 0, {
13
+ context: normalizeContextLines(options.contextLines),
14
+ ignoreWhitespace: options.ignoreWhitespace
15
+ });
16
+ return {
17
+ path: newPath || oldPath,
18
+ oldPath,
19
+ newPath,
20
+ changeType: changeTypeForTextFiles(oldFile, newFile, oldPath, newPath),
21
+ oldObjectId: oldFile?.objectId,
22
+ newObjectId: newFile?.objectId,
23
+ oldMode: oldFile?.mode,
24
+ newMode: newFile?.mode,
25
+ oldLines: splitTextLines(oldText),
26
+ newLines: splitTextLines(newText),
27
+ hunks: convertPatchHunks(patch.hunks ?? []),
28
+ isPartial: false,
29
+ languageId: newFile?.languageId ?? oldFile?.languageId ?? languageIdForPath(newPath || oldPath)
30
+ };
31
+ }
32
+ function parseGitPatch(patchText, options = {}) {
33
+ const parsed = parsePatchTolerant(patchText);
34
+ const metadata = gitMetadataByPath(patchText);
35
+ return parsed.filter(isParsedPatchFileUsable).map((file, index) => convertParsedPatchFile(file, metadata, cacheKeyForPatch(options.cacheKey, index)));
36
+ }
37
+ function parsePatchTolerant(patchText) {
38
+ try {
39
+ return parsePatch(patchText);
40
+ } catch {
41
+ return parsePatch(removeRepeatedHunkHeaders(patchText));
42
+ }
43
+ }
44
+ function removeRepeatedHunkHeaders(patchText) {
45
+ const lines = patchText.split("\n");
46
+ const cleaned = [];
47
+ for (const line of lines) {
48
+ const previous = cleaned.at(-1);
49
+ if (previous && isRawHunkHeader(previous) && isRawHunkHeader(line)) continue;
50
+ cleaned.push(line);
51
+ }
52
+ return cleaned.join("\n");
53
+ }
54
+ function isParsedPatchFileUsable(file) {
55
+ return normalizedFilePath(stripDiffPathPrefix(file.oldFileName), stripDiffPathPrefix(file.newFileName)).length > 0;
56
+ }
57
+ function convertParsedPatchFile(file, metadata, cacheKey) {
58
+ const oldPath = stripDiffPathPrefix(file.oldFileName);
59
+ const newPath = stripDiffPathPrefix(file.newFileName);
60
+ const path = normalizedFilePath(oldPath, newPath);
61
+ const git = metadata.get(path) ?? metadata.get(oldPath) ?? metadata.get(newPath);
62
+ const hunks = convertPatchHunks(file.hunks ?? []);
63
+ return {
64
+ path,
65
+ oldPath: normalizedOptionalPath(git?.oldPath ?? oldPath),
66
+ newPath: normalizedOptionalPath(git?.newPath ?? newPath) ?? path,
67
+ changeType: git?.changeType ?? changeTypeForPatchPaths(oldPath, newPath),
68
+ oldObjectId: git?.oldObjectId,
69
+ newObjectId: git?.newObjectId,
70
+ oldMode: git?.oldMode,
71
+ newMode: git?.newMode,
72
+ oldLines: collectPatchLines(hunks, "old"),
73
+ newLines: collectPatchLines(hunks, "new"),
74
+ hunks,
75
+ isPartial: true,
76
+ languageId: languageIdForPath(path),
77
+ cacheKey
78
+ };
79
+ }
80
+ function convertPatchHunks(hunks) {
81
+ return hunks.map((hunk) => convertPatchHunk(hunk));
82
+ }
83
+ function convertPatchHunk(hunk) {
84
+ return {
85
+ oldStart: hunk.oldStart,
86
+ oldLines: hunk.oldLines,
87
+ newStart: hunk.newStart,
88
+ newLines: hunk.newLines,
89
+ header: hunkHeader(hunk),
90
+ lines: annotateInlineChanges(convertPatchLines(hunk))
91
+ };
92
+ }
93
+ function convertPatchLines(hunk) {
94
+ const lines = [];
95
+ let oldLineNumber = hunk.oldStart;
96
+ let newLineNumber = hunk.newStart;
97
+ for (const rawLine of hunk.lines) {
98
+ const result = convertPatchLine(rawLine, oldLineNumber, newLineNumber);
99
+ if (!result) continue;
100
+ lines.push(result.line);
101
+ oldLineNumber += result.oldDelta;
102
+ newLineNumber += result.newDelta;
103
+ }
104
+ return lines;
105
+ }
106
+ function convertPatchLine(rawLine, oldLineNumber, newLineNumber) {
107
+ if (rawLine.startsWith("\\")) return null;
108
+ if (isRawHunkHeader(rawLine)) return null;
109
+ const marker = rawLine[0] ?? " ";
110
+ const text = rawLine.slice(1);
111
+ if (isRawHunkHeader(text)) return null;
112
+ if (marker === "-") return {
113
+ line: {
114
+ type: "deletion",
115
+ text,
116
+ oldLineNumber
117
+ },
118
+ oldDelta: 1,
119
+ newDelta: 0
120
+ };
121
+ if (marker === "+") return {
122
+ line: {
123
+ type: "addition",
124
+ text,
125
+ newLineNumber
126
+ },
127
+ oldDelta: 0,
128
+ newDelta: 1
129
+ };
130
+ return {
131
+ line: {
132
+ type: "context",
133
+ text,
134
+ oldLineNumber,
135
+ newLineNumber
136
+ },
137
+ oldDelta: 1,
138
+ newDelta: 1
139
+ };
140
+ }
141
+ function isRawHunkHeader(text) {
142
+ return /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/.test(text.trim());
143
+ }
144
+ function hunkHeader(hunk) {
145
+ return `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
146
+ }
147
+ function collectPatchLines(hunks, side) {
148
+ const lines = [];
149
+ for (const hunk of hunks) appendPatchLines(lines, hunk, side);
150
+ return lines;
151
+ }
152
+ function appendPatchLines(lines, hunk, side) {
153
+ for (const line of hunk.lines) {
154
+ if (side === "old" && line.type === "addition") continue;
155
+ if (side === "new" && line.type === "deletion") continue;
156
+ lines.push(line.text);
157
+ }
158
+ }
159
+ function changeTypeForTextFiles(oldFile, newFile, oldPath, newPath) {
160
+ if (!oldFile && newFile) return "add";
161
+ if (oldFile && !newFile) return "delete";
162
+ if (oldPath !== newPath) return "rename-change";
163
+ return "change";
164
+ }
165
+ function changeTypeForPatchPaths(oldPath, newPath) {
166
+ if (oldPath === "/dev/null") return "add";
167
+ if (newPath === "/dev/null") return "delete";
168
+ if (oldPath && newPath && oldPath !== newPath) return "rename-change";
169
+ return "change";
170
+ }
171
+ function normalizedFilePath(oldPath, newPath) {
172
+ if (newPath && newPath !== "/dev/null") return newPath;
173
+ if (oldPath && oldPath !== "/dev/null") return oldPath;
174
+ return newPath || oldPath;
175
+ }
176
+ function normalizedOptionalPath(path) {
177
+ if (!path || path === "/dev/null") return void 0;
178
+ return path;
179
+ }
180
+ function cacheKeyForPatch(prefix, index) {
181
+ if (!prefix) return void 0;
182
+ return `${prefix}-${index}`;
183
+ }
184
+ function gitMetadataByPath(patchText) {
185
+ const metadata = /* @__PURE__ */ new Map();
186
+ for (const section of gitFileSections(patchText)) addGitMetadata(metadata, section);
187
+ return metadata;
188
+ }
189
+ function gitFileSections(patchText) {
190
+ return patchText.split(/(?=^diff --git )/m).map((section) => section.trim()).filter((section) => section.startsWith("diff --git "));
191
+ }
192
+ function addGitMetadata(metadata, section) {
193
+ const parsed = parseGitSection(section);
194
+ const path = normalizedFilePath(parsed.oldPath ?? "", parsed.newPath ?? "");
195
+ if (!path) return;
196
+ metadata.set(path, parsed);
197
+ if (parsed.oldPath) metadata.set(parsed.oldPath, parsed);
198
+ if (parsed.newPath) metadata.set(parsed.newPath, parsed);
199
+ }
200
+ function parseGitSection(section) {
201
+ const lines = section.split("\n");
202
+ const paths = parseDiffGitPaths(lines[0] ?? "");
203
+ const partial = {
204
+ oldPath: paths.oldPath,
205
+ newPath: paths.newPath
206
+ };
207
+ for (const line of lines) applyGitMetadataLine(partial, line);
208
+ partial.changeType = gitChangeType(partial, section);
209
+ return partial;
210
+ }
211
+ function parseDiffGitPaths(line) {
212
+ const match = line.match(/^diff --git\s+a\/(.+?)\s+b\/(.+)$/);
213
+ if (!match) return {};
214
+ return {
215
+ oldPath: stripDiffPathPrefix(`a/${match[1]}`),
216
+ newPath: stripDiffPathPrefix(`b/${match[2]}`)
217
+ };
218
+ }
219
+ function applyGitMetadataLine(partial, line) {
220
+ if (line.startsWith("index ")) applyIndexMetadata(partial, line);
221
+ if (line.startsWith("old mode ")) partial.oldMode = line.slice(9).trim();
222
+ if (line.startsWith("new mode ")) partial.newMode = line.slice(9).trim();
223
+ if (line.startsWith("new file mode ")) partial.newMode = line.slice(14).trim();
224
+ if (line.startsWith("deleted file mode ")) partial.oldMode = line.slice(18).trim();
225
+ if (line.startsWith("rename from ")) partial.oldPath = line.slice(12).trim();
226
+ if (line.startsWith("rename to ")) partial.newPath = line.slice(10).trim();
227
+ }
228
+ function applyIndexMetadata(partial, line) {
229
+ const match = line.match(/^index\s+([0-9a-f]+)\.\.([0-9a-f]+)(?:\s+(\d+))?/i);
230
+ if (!match) return;
231
+ partial.oldObjectId = match[1];
232
+ partial.newObjectId = match[2];
233
+ if (match[3]) {
234
+ partial.oldMode = partial.oldMode ?? match[3];
235
+ partial.newMode = partial.newMode ?? match[3];
236
+ }
237
+ }
238
+ function gitChangeType(partial, section) {
239
+ if (section.includes("\nnew file mode ")) return "add";
240
+ if (section.includes("\ndeleted file mode ")) return "delete";
241
+ if (partial.oldPath && partial.newPath && partial.oldPath !== partial.newPath) return section.includes("\n@@ ") ? "rename-change" : "rename";
242
+ return "change";
243
+ }
244
+ //#endregion
245
+ export { createTextDiff, parseGitPatch };
246
+
247
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","names":[],"sources":["../src/model.ts"],"sourcesContent":["import { parsePatch, structuredPatch } from 'diff'\nimport { annotateInlineChanges } from './inline'\nimport {\n languageIdForPath,\n normalizeContextLines,\n splitTextLines,\n stripDiffPathPrefix,\n} from './lines'\nimport type {\n CreateTextDiffOptions,\n DiffFile,\n DiffFileChangeType,\n DiffHunk,\n DiffHunkLine,\n DiffTextFile,\n ParseGitPatchOptions,\n} from './types'\n\ntype ParsedPatchFile = {\n readonly oldFileName?: string\n readonly newFileName?: string\n readonly oldHeader?: string\n readonly newHeader?: string\n readonly hunks?: readonly ParsedPatchHunk[]\n}\n\ntype ParsedPatchHunk = {\n readonly oldStart: number\n readonly oldLines: number\n readonly newStart: number\n readonly newLines: number\n readonly lines: readonly string[]\n}\n\ntype GitFileMetadata = {\n readonly oldPath?: string\n readonly newPath?: string\n readonly oldObjectId?: string\n readonly newObjectId?: string\n readonly oldMode?: string\n readonly newMode?: string\n readonly changeType?: DiffFileChangeType\n}\n\ntype MutableGitFileMetadata = {\n -readonly [Key in keyof GitFileMetadata]?: GitFileMetadata[Key]\n}\n\nexport function createTextDiff(options: CreateTextDiffOptions): DiffFile {\n const oldFile = options.oldFile ?? null\n const newFile = options.newFile ?? null\n const oldText = oldFile?.text ?? ''\n const newText = newFile?.text ?? ''\n const oldPath = oldFile?.path ?? newFile?.path ?? ''\n const newPath = newFile?.path ?? oldFile?.path ?? ''\n const patch = structuredPatch(oldPath, newPath, oldText, newText, undefined, undefined, {\n context: normalizeContextLines(options.contextLines),\n ignoreWhitespace: options.ignoreWhitespace,\n }) as ParsedPatchFile\n\n return {\n path: newPath || oldPath,\n oldPath,\n newPath,\n changeType: changeTypeForTextFiles(oldFile, newFile, oldPath, newPath),\n oldObjectId: oldFile?.objectId,\n newObjectId: newFile?.objectId,\n oldMode: oldFile?.mode,\n newMode: newFile?.mode,\n oldLines: splitTextLines(oldText),\n newLines: splitTextLines(newText),\n hunks: convertPatchHunks(patch.hunks ?? []),\n isPartial: false,\n languageId: newFile?.languageId ?? oldFile?.languageId ?? languageIdForPath(newPath || oldPath),\n }\n}\n\nexport function parseGitPatch(\n patchText: string,\n options: ParseGitPatchOptions = {},\n): readonly DiffFile[] {\n const parsed = parsePatchTolerant(patchText)\n const metadata = gitMetadataByPath(patchText)\n\n return parsed\n .filter(isParsedPatchFileUsable)\n .map((file, index) =>\n convertParsedPatchFile(file, metadata, cacheKeyForPatch(options.cacheKey, index)),\n )\n}\n\nfunction parsePatchTolerant(patchText: string): readonly ParsedPatchFile[] {\n try {\n return parsePatch(patchText) as readonly ParsedPatchFile[]\n } catch {\n return parsePatch(removeRepeatedHunkHeaders(patchText)) as readonly ParsedPatchFile[]\n }\n}\n\nfunction removeRepeatedHunkHeaders(patchText: string): string {\n const lines = patchText.split('\\n')\n const cleaned: string[] = []\n\n for (const line of lines) {\n const previous = cleaned.at(-1)\n if (previous && isRawHunkHeader(previous) && isRawHunkHeader(line)) continue\n cleaned.push(line)\n }\n\n return cleaned.join('\\n')\n}\n\nfunction isParsedPatchFileUsable(file: ParsedPatchFile): boolean {\n const oldPath = stripDiffPathPrefix(file.oldFileName)\n const newPath = stripDiffPathPrefix(file.newFileName)\n return normalizedFilePath(oldPath, newPath).length > 0\n}\n\nfunction convertParsedPatchFile(\n file: ParsedPatchFile,\n metadata: ReadonlyMap<string, GitFileMetadata>,\n cacheKey: string | undefined,\n): DiffFile {\n const oldPath = stripDiffPathPrefix(file.oldFileName)\n const newPath = stripDiffPathPrefix(file.newFileName)\n const path = normalizedFilePath(oldPath, newPath)\n const git = metadata.get(path) ?? metadata.get(oldPath) ?? metadata.get(newPath)\n const hunks = convertPatchHunks(file.hunks ?? [])\n\n return {\n path,\n oldPath: normalizedOptionalPath(git?.oldPath ?? oldPath),\n newPath: normalizedOptionalPath(git?.newPath ?? newPath) ?? path,\n changeType: git?.changeType ?? changeTypeForPatchPaths(oldPath, newPath),\n oldObjectId: git?.oldObjectId,\n newObjectId: git?.newObjectId,\n oldMode: git?.oldMode,\n newMode: git?.newMode,\n oldLines: collectPatchLines(hunks, 'old'),\n newLines: collectPatchLines(hunks, 'new'),\n hunks,\n isPartial: true,\n languageId: languageIdForPath(path),\n cacheKey,\n }\n}\n\nfunction convertPatchHunks(hunks: readonly ParsedPatchHunk[]): readonly DiffHunk[] {\n return hunks.map((hunk) => convertPatchHunk(hunk))\n}\n\nfunction convertPatchHunk(hunk: ParsedPatchHunk): DiffHunk {\n return {\n oldStart: hunk.oldStart,\n oldLines: hunk.oldLines,\n newStart: hunk.newStart,\n newLines: hunk.newLines,\n header: hunkHeader(hunk),\n lines: annotateInlineChanges(convertPatchLines(hunk)),\n }\n}\n\nfunction convertPatchLines(hunk: ParsedPatchHunk): readonly DiffHunkLine[] {\n const lines: DiffHunkLine[] = []\n let oldLineNumber = hunk.oldStart\n let newLineNumber = hunk.newStart\n\n for (const rawLine of hunk.lines) {\n const result = convertPatchLine(rawLine, oldLineNumber, newLineNumber)\n if (!result) continue\n\n lines.push(result.line)\n oldLineNumber += result.oldDelta\n newLineNumber += result.newDelta\n }\n\n return lines\n}\n\nfunction convertPatchLine(\n rawLine: string,\n oldLineNumber: number,\n newLineNumber: number,\n): { readonly line: DiffHunkLine; readonly oldDelta: number; readonly newDelta: number } | null {\n if (rawLine.startsWith('\\\\')) return null\n if (isRawHunkHeader(rawLine)) return null\n\n const marker = rawLine[0] ?? ' '\n const text = rawLine.slice(1)\n if (isRawHunkHeader(text)) return null\n\n if (marker === '-') {\n return {\n line: { type: 'deletion', text, oldLineNumber },\n oldDelta: 1,\n newDelta: 0,\n }\n }\n if (marker === '+') {\n return {\n line: { type: 'addition', text, newLineNumber },\n oldDelta: 0,\n newDelta: 1,\n }\n }\n\n return {\n line: { type: 'context', text, oldLineNumber, newLineNumber },\n oldDelta: 1,\n newDelta: 1,\n }\n}\n\nfunction isRawHunkHeader(text: string): boolean {\n return /^@@ -\\d+(?:,\\d+)? \\+\\d+(?:,\\d+)? @@/.test(text.trim())\n}\n\nfunction hunkHeader(hunk: ParsedPatchHunk): string {\n return `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`\n}\n\nfunction collectPatchLines(hunks: readonly DiffHunk[], side: 'old' | 'new'): readonly string[] {\n const lines: string[] = []\n for (const hunk of hunks) appendPatchLines(lines, hunk, side)\n return lines\n}\n\nfunction appendPatchLines(lines: string[], hunk: DiffHunk, side: 'old' | 'new'): void {\n for (const line of hunk.lines) {\n if (side === 'old' && line.type === 'addition') continue\n if (side === 'new' && line.type === 'deletion') continue\n lines.push(line.text)\n }\n}\n\nfunction changeTypeForTextFiles(\n oldFile: DiffTextFile | null,\n newFile: DiffTextFile | null,\n oldPath: string,\n newPath: string,\n): DiffFileChangeType {\n if (!oldFile && newFile) return 'add'\n if (oldFile && !newFile) return 'delete'\n if (oldPath !== newPath) return 'rename-change'\n return 'change'\n}\n\nfunction changeTypeForPatchPaths(oldPath: string, newPath: string): DiffFileChangeType {\n if (oldPath === '/dev/null') return 'add'\n if (newPath === '/dev/null') return 'delete'\n if (oldPath && newPath && oldPath !== newPath) return 'rename-change'\n return 'change'\n}\n\nfunction normalizedFilePath(oldPath: string, newPath: string): string {\n if (newPath && newPath !== '/dev/null') return newPath\n if (oldPath && oldPath !== '/dev/null') return oldPath\n return newPath || oldPath\n}\n\nfunction normalizedOptionalPath(path: string | undefined): string | undefined {\n if (!path || path === '/dev/null') return undefined\n return path\n}\n\nfunction cacheKeyForPatch(prefix: string | undefined, index: number): string | undefined {\n if (!prefix) return undefined\n return `${prefix}-${index}`\n}\n\nfunction gitMetadataByPath(patchText: string): ReadonlyMap<string, GitFileMetadata> {\n const metadata = new Map<string, GitFileMetadata>()\n for (const section of gitFileSections(patchText)) addGitMetadata(metadata, section)\n return metadata\n}\n\nfunction gitFileSections(patchText: string): readonly string[] {\n return patchText\n .split(/(?=^diff --git )/m)\n .map((section) => section.trim())\n .filter((section) => section.startsWith('diff --git '))\n}\n\nfunction addGitMetadata(metadata: Map<string, GitFileMetadata>, section: string): void {\n const parsed = parseGitSection(section)\n const path = normalizedFilePath(parsed.oldPath ?? '', parsed.newPath ?? '')\n if (!path) return\n\n metadata.set(path, parsed)\n if (parsed.oldPath) metadata.set(parsed.oldPath, parsed)\n if (parsed.newPath) metadata.set(parsed.newPath, parsed)\n}\n\nfunction parseGitSection(section: string): GitFileMetadata {\n const lines = section.split('\\n')\n const paths = parseDiffGitPaths(lines[0] ?? '')\n const partial: MutableGitFileMetadata = {\n oldPath: paths.oldPath,\n newPath: paths.newPath,\n }\n\n for (const line of lines) applyGitMetadataLine(partial, line)\n partial.changeType = gitChangeType(partial, section)\n return partial as GitFileMetadata\n}\n\nfunction parseDiffGitPaths(line: string): { readonly oldPath?: string; readonly newPath?: string } {\n const match = line.match(/^diff --git\\s+a\\/(.+?)\\s+b\\/(.+)$/)\n if (!match) return {}\n return {\n oldPath: stripDiffPathPrefix(`a/${match[1]}`),\n newPath: stripDiffPathPrefix(`b/${match[2]}`),\n }\n}\n\nfunction applyGitMetadataLine(partial: MutableGitFileMetadata, line: string): void {\n if (line.startsWith('index ')) applyIndexMetadata(partial, line)\n if (line.startsWith('old mode ')) partial.oldMode = line.slice('old mode '.length).trim()\n if (line.startsWith('new mode ')) partial.newMode = line.slice('new mode '.length).trim()\n if (line.startsWith('new file mode '))\n partial.newMode = line.slice('new file mode '.length).trim()\n if (line.startsWith('deleted file mode ')) {\n partial.oldMode = line.slice('deleted file mode '.length).trim()\n }\n if (line.startsWith('rename from ')) partial.oldPath = line.slice('rename from '.length).trim()\n if (line.startsWith('rename to ')) partial.newPath = line.slice('rename to '.length).trim()\n}\n\nfunction applyIndexMetadata(partial: MutableGitFileMetadata, line: string): void {\n const match = line.match(/^index\\s+([0-9a-f]+)\\.\\.([0-9a-f]+)(?:\\s+(\\d+))?/i)\n if (!match) return\n\n partial.oldObjectId = match[1]\n partial.newObjectId = match[2]\n if (match[3]) {\n partial.oldMode = partial.oldMode ?? match[3]\n partial.newMode = partial.newMode ?? match[3]\n }\n}\n\nfunction gitChangeType(partial: MutableGitFileMetadata, section: string): DiffFileChangeType {\n if (section.includes('\\nnew file mode ')) return 'add'\n if (section.includes('\\ndeleted file mode ')) return 'delete'\n if (partial.oldPath && partial.newPath && partial.oldPath !== partial.newPath) {\n return section.includes('\\n@@ ') ? 'rename-change' : 'rename'\n }\n return 'change'\n}\n"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { DiffFile, DiffRenderRow } from './types';
2
+ export type SplitDiffProjection = {
3
+ readonly leftRows: readonly DiffRenderRow[];
4
+ readonly rightRows: readonly DiffRenderRow[];
5
+ readonly hunkRows: ReadonlyMap<number, number>;
6
+ };
7
+ export type StackedDiffProjection = {
8
+ readonly rows: readonly DiffRenderRow[];
9
+ readonly hunkRows: ReadonlyMap<number, number>;
10
+ };
11
+ type DiffProjectionOptions = {
12
+ /** Keys of the collapsed regions to render expanded, taken from `DiffRenderRow.expandKey`. */
13
+ readonly expandedRegions?: ReadonlySet<string>;
14
+ };
15
+ export declare function createSplitProjection(file: DiffFile, options?: DiffProjectionOptions): SplitDiffProjection;
16
+ export declare function createStackedProjection(file: DiffFile, options?: DiffProjectionOptions): StackedDiffProjection;
17
+ export {};
18
+ //# sourceMappingURL=projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAA2C,aAAa,EAAE,MAAM,SAAS,CAAA;AAE/F,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAA;IAC3C,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAA;IAC5C,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/C,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAA;IACvC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/C,CAAA;AAaD,KAAK,qBAAqB,GAAG;IAC3B,8FAA8F;IAC9F,QAAQ,CAAC,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC/C,CAAA;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,QAAQ,EACd,OAAO,GAAE,qBAA0B,GAClC,mBAAmB,CA4BrB;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,EACd,OAAO,GAAE,qBAA0B,GAClC,qBAAqB,CA0BvB"}
@@ -0,0 +1,281 @@
1
+ //#region src/projection.ts
2
+ function createSplitProjection(file, options = {}) {
3
+ const leftRows = [];
4
+ const rightRows = [];
5
+ const hunkRows = /* @__PURE__ */ new Map();
6
+ let previousOldEnd = 0;
7
+ let previousNewEnd = 0;
8
+ for (const [hunkIndex, hunk] of file.hunks.entries()) {
9
+ const range = skippedRangeBeforeHunk(hunk, previousOldEnd, previousNewEnd);
10
+ const separator = separatorRow(file, range, hunkIndex, options);
11
+ if (separator) pushSplitHunkSeparator(leftRows, rightRows, separator);
12
+ hunkRows.set(hunkIndex, separator ? leftRows.length - 1 : leftRows.length);
13
+ if (separator?.expanded) pushSplitExpandedRows(file, leftRows, rightRows, range, hunkIndex);
14
+ pushSplitHunkRows(leftRows, rightRows, hunk.lines, hunkIndex);
15
+ previousOldEnd = hunkEndLine(hunk.oldStart, hunk.oldLines);
16
+ previousNewEnd = hunkEndLine(hunk.newStart, hunk.newLines);
17
+ }
18
+ const trailing = trailingSkippedRange(file, previousOldEnd, previousNewEnd);
19
+ const trailingSeparator = separatorRow(file, trailing, void 0, options);
20
+ if (trailingSeparator) {
21
+ pushSplitHunkSeparator(leftRows, rightRows, trailingSeparator);
22
+ if (trailingSeparator.expanded) pushSplitExpandedRows(file, leftRows, rightRows, trailing, void 0);
23
+ }
24
+ if (leftRows.length === 0) pushUnchangedFileSplitRows(file, leftRows, rightRows);
25
+ return {
26
+ leftRows,
27
+ rightRows,
28
+ hunkRows
29
+ };
30
+ }
31
+ function createStackedProjection(file, options = {}) {
32
+ const rows = [];
33
+ const hunkRows = /* @__PURE__ */ new Map();
34
+ let previousOldEnd = 0;
35
+ let previousNewEnd = 0;
36
+ for (const [hunkIndex, hunk] of file.hunks.entries()) {
37
+ const range = skippedRangeBeforeHunk(hunk, previousOldEnd, previousNewEnd);
38
+ const separator = separatorRow(file, range, hunkIndex, options);
39
+ if (separator) rows.push(separator);
40
+ hunkRows.set(hunkIndex, separator ? rows.length - 1 : rows.length);
41
+ if (separator?.expanded) pushStackedExpandedRows(file, rows, range, hunkIndex);
42
+ pushStackedHunkRows(rows, hunk.lines, hunkIndex);
43
+ previousOldEnd = hunkEndLine(hunk.oldStart, hunk.oldLines);
44
+ previousNewEnd = hunkEndLine(hunk.newStart, hunk.newLines);
45
+ }
46
+ const trailing = trailingSkippedRange(file, previousOldEnd, previousNewEnd);
47
+ const trailingSeparator = separatorRow(file, trailing, void 0, options);
48
+ if (trailingSeparator) {
49
+ rows.push(trailingSeparator);
50
+ if (trailingSeparator.expanded) pushStackedExpandedRows(file, rows, trailing, void 0);
51
+ }
52
+ if (rows.length === 0) pushUnchangedFileStackedRows(file, rows);
53
+ return {
54
+ rows,
55
+ hunkRows
56
+ };
57
+ }
58
+ function pushSplitHunkSeparator(leftRows, rightRows, row) {
59
+ leftRows.push(row);
60
+ rightRows.push(row);
61
+ }
62
+ function pushSplitHunkRows(leftRows, rightRows, lines, hunkIndex) {
63
+ let index = 0;
64
+ while (index < lines.length) {
65
+ const nextIndex = pushNextSplitRows(leftRows, rightRows, lines, index, hunkIndex);
66
+ index = Math.max(index + 1, nextIndex);
67
+ }
68
+ }
69
+ function pushSplitExpandedRows(file, leftRows, rightRows, range, hunkIndex) {
70
+ for (let index = 0; index < range.count; index += 1) {
71
+ leftRows.push(expandedSideRow(file.oldLines, range.oldStart + index, "old", hunkIndex));
72
+ rightRows.push(expandedSideRow(file.newLines, range.newStart + index, "new", hunkIndex));
73
+ }
74
+ }
75
+ function pushNextSplitRows(leftRows, rightRows, lines, index, hunkIndex) {
76
+ const line = lines[index];
77
+ if (!line) return index + 1;
78
+ if (line.type === "context") {
79
+ pushSplitContextLine(leftRows, rightRows, line, hunkIndex);
80
+ return index + 1;
81
+ }
82
+ const blockEnd = firstContextIndex(lines, index);
83
+ pushSplitChangeBlock(leftRows, rightRows, changeBlock(lines, index, blockEnd), hunkIndex);
84
+ return blockEnd;
85
+ }
86
+ function pushSplitContextLine(leftRows, rightRows, line, hunkIndex) {
87
+ leftRows.push(renderRowFromLine(line, "context", "old", hunkIndex));
88
+ rightRows.push(renderRowFromLine(line, "context", "new", hunkIndex));
89
+ }
90
+ function pushSplitChangeBlock(leftRows, rightRows, block, hunkIndex) {
91
+ const count = Math.max(block.deletions.length, block.additions.length);
92
+ for (let index = 0; index < count; index += 1) {
93
+ leftRows.push(splitDeletionRow(block.deletions[index], hunkIndex));
94
+ rightRows.push(splitAdditionRow(block.additions[index], hunkIndex));
95
+ }
96
+ }
97
+ function splitDeletionRow(line, hunkIndex) {
98
+ if (!line) return placeholderRow(hunkIndex);
99
+ return renderRowFromLine(line, "deletion", "old", hunkIndex, line.oldInlineRanges);
100
+ }
101
+ function splitAdditionRow(line, hunkIndex) {
102
+ if (!line) return placeholderRow(hunkIndex);
103
+ return renderRowFromLine(line, "addition", "new", hunkIndex, line.newInlineRanges);
104
+ }
105
+ function pushStackedHunkRows(rows, lines, hunkIndex) {
106
+ for (const line of lines) rows.push(stackedRowFromLine(line, hunkIndex));
107
+ }
108
+ function pushStackedExpandedRows(file, rows, range, hunkIndex) {
109
+ for (let index = 0; index < range.count; index += 1) rows.push(expandedBothRow(file.newLines, range.oldStart + index, range.newStart + index, hunkIndex));
110
+ }
111
+ function stackedRowFromLine(line, hunkIndex) {
112
+ if (line.type === "deletion") return renderRowFromLine(line, "deletion", "old", hunkIndex, line.oldInlineRanges);
113
+ if (line.type === "addition") return renderRowFromLine(line, "addition", "new", hunkIndex, line.newInlineRanges);
114
+ return renderRowFromLine(line, "context", "both", hunkIndex);
115
+ }
116
+ function renderRowFromLine(line, type, side, hunkIndex, inlineRanges = []) {
117
+ return {
118
+ type,
119
+ text: renderLineText(line.text),
120
+ oldLineNumber: side !== "new" ? line.oldLineNumber : void 0,
121
+ newLineNumber: side !== "old" ? line.newLineNumber : void 0,
122
+ hunkIndex,
123
+ inlineRanges
124
+ };
125
+ }
126
+ function renderLineText(text) {
127
+ if (!isRawHunkHeader(text)) return text;
128
+ return "";
129
+ }
130
+ function isRawHunkHeader(text) {
131
+ return /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/.test(text.trim());
132
+ }
133
+ function separatorRow(file, range, hunkIndex, options) {
134
+ if (range.count <= 0) return null;
135
+ const expandKey = regionKey(range);
136
+ const expanded = options.expandedRegions?.has(expandKey) ?? false;
137
+ const expandable = skippedRangeAvailable(file, range);
138
+ return {
139
+ expanded,
140
+ expandable,
141
+ expandKey,
142
+ type: "hunk",
143
+ text: hunkSeparatorText(range.count, expandable, expanded),
144
+ hunkIndex,
145
+ skippedLines: range.count
146
+ };
147
+ }
148
+ function regionKey(range) {
149
+ return `${range.oldStart}:${range.newStart}`;
150
+ }
151
+ function hunkSeparatorText(lines, expandable, expanded) {
152
+ const suffix = `${lines} unmodified line${lines === 1 ? "" : "s"}`;
153
+ if (!expandable) return suffix;
154
+ return `${expanded ? "Hide" : "Show"} ${suffix}`;
155
+ }
156
+ function skippedRangeAvailable(file, range) {
157
+ if (range.count <= 0) return false;
158
+ if (range.oldStart < 1 || range.newStart < 1) return false;
159
+ if (range.oldStart + range.count - 1 > file.oldLines.length) return false;
160
+ return range.newStart + range.count - 1 <= file.newLines.length;
161
+ }
162
+ function skippedRangeBeforeHunk(hunk, previousOldEnd, previousNewEnd) {
163
+ return {
164
+ count: skippedUnmodifiedLines(hunk, previousOldEnd, previousNewEnd),
165
+ newStart: previousNewEnd + 1,
166
+ oldStart: previousOldEnd + 1
167
+ };
168
+ }
169
+ function trailingSkippedRange(file, previousOldEnd, previousNewEnd) {
170
+ if (file.hunks.length === 0 || file.isPartial) return {
171
+ count: 0,
172
+ newStart: 1,
173
+ oldStart: 1
174
+ };
175
+ const oldRemaining = contentLineCount(file.oldLines) - previousOldEnd;
176
+ const newRemaining = contentLineCount(file.newLines) - previousNewEnd;
177
+ return {
178
+ count: Math.max(oldRemaining, newRemaining, 0),
179
+ newStart: previousNewEnd + 1,
180
+ oldStart: previousOldEnd + 1
181
+ };
182
+ }
183
+ function contentLineCount(lines) {
184
+ if (lines.length === 0) return 0;
185
+ if (lines.at(-1) === "") return lines.length - 1;
186
+ return lines.length;
187
+ }
188
+ function expandedSideRow(lines, lineNumber, side, hunkIndex) {
189
+ const text = lines[lineNumber - 1] ?? "";
190
+ return renderRowFromLine({
191
+ newLineNumber: side === "new" ? lineNumber : void 0,
192
+ oldLineNumber: side === "old" ? lineNumber : void 0,
193
+ text,
194
+ type: "context"
195
+ }, "context", side, hunkIndex);
196
+ }
197
+ function expandedBothRow(lines, oldLineNumber, newLineNumber, hunkIndex) {
198
+ return renderRowFromLine({
199
+ newLineNumber,
200
+ oldLineNumber,
201
+ text: lines[newLineNumber - 1] ?? "",
202
+ type: "context"
203
+ }, "context", "both", hunkIndex);
204
+ }
205
+ function skippedUnmodifiedLines(hunk, previousOldEnd, previousNewEnd) {
206
+ return Math.max(hunk.oldStart - previousOldEnd - 1, hunk.newStart - previousNewEnd - 1, 0);
207
+ }
208
+ function hunkEndLine(start, count) {
209
+ if (count <= 0) return Math.max(0, start);
210
+ return start + count - 1;
211
+ }
212
+ function placeholderRow(hunkIndex) {
213
+ return {
214
+ type: "placeholder",
215
+ text: "",
216
+ hunkIndex
217
+ };
218
+ }
219
+ function emptyRow(text) {
220
+ return {
221
+ type: "empty",
222
+ text
223
+ };
224
+ }
225
+ function pushNoChangesRows(leftRows, rightRows) {
226
+ leftRows.push(emptyRow("No changes"));
227
+ rightRows.push(emptyRow("No changes"));
228
+ }
229
+ /**
230
+ * A whole-file diff with nothing changed still has a file in it — a pure rename, a mode change, two
231
+ * sides that happen to match. Showing the text is the only useful thing a viewer can do with that,
232
+ * and it is what every other diff viewer does; the alternative is a pane holding the word "No
233
+ * changes", which tells the reader less than the file would.
234
+ *
235
+ * Rendered as plain context rows rather than as one collapsed region: there is no change for a
236
+ * collapsed region to sit next to, so the reader would be one click away from every diff they
237
+ * opened. A patch-only file (`isPartial`) has no text to fall back to and keeps the message.
238
+ */
239
+ function pushUnchangedFileSplitRows(file, leftRows, rightRows) {
240
+ const range = unchangedFileRange(file);
241
+ if (range.count <= 0) {
242
+ pushNoChangesRows(leftRows, rightRows);
243
+ return;
244
+ }
245
+ pushSplitExpandedRows(file, leftRows, rightRows, range, void 0);
246
+ }
247
+ function pushUnchangedFileStackedRows(file, rows) {
248
+ const range = unchangedFileRange(file);
249
+ if (range.count <= 0) {
250
+ rows.push(emptyRow("No changes"));
251
+ return;
252
+ }
253
+ pushStackedExpandedRows(file, rows, range, void 0);
254
+ }
255
+ function unchangedFileRange(file) {
256
+ if (file.isPartial) return {
257
+ count: 0,
258
+ newStart: 1,
259
+ oldStart: 1
260
+ };
261
+ return {
262
+ count: Math.max(contentLineCount(file.oldLines), contentLineCount(file.newLines)),
263
+ newStart: 1,
264
+ oldStart: 1
265
+ };
266
+ }
267
+ function firstContextIndex(lines, start) {
268
+ let index = start;
269
+ while (index < lines.length && lines[index]?.type !== "context") index += 1;
270
+ return index;
271
+ }
272
+ function changeBlock(lines, start, end) {
273
+ return {
274
+ deletions: lines.slice(start, end).filter((line) => line.type === "deletion"),
275
+ additions: lines.slice(start, end).filter((line) => line.type === "addition")
276
+ };
277
+ }
278
+ //#endregion
279
+ export { createSplitProjection, createStackedProjection };
280
+
281
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection.js","names":[],"sources":["../src/projection.ts"],"sourcesContent":["import type { DiffFile, DiffHunk, DiffHunkLine, DiffInlineRange, DiffRenderRow } from './types'\n\nexport type SplitDiffProjection = {\n readonly leftRows: readonly DiffRenderRow[]\n readonly rightRows: readonly DiffRenderRow[]\n readonly hunkRows: ReadonlyMap<number, number>\n}\n\nexport type StackedDiffProjection = {\n readonly rows: readonly DiffRenderRow[]\n readonly hunkRows: ReadonlyMap<number, number>\n}\n\ntype ChangeBlock = {\n readonly deletions: readonly DiffHunkLine[]\n readonly additions: readonly DiffHunkLine[]\n}\n\ntype SkippedRange = {\n readonly oldStart: number\n readonly newStart: number\n readonly count: number\n}\n\ntype DiffProjectionOptions = {\n /** Keys of the collapsed regions to render expanded, taken from `DiffRenderRow.expandKey`. */\n readonly expandedRegions?: ReadonlySet<string>\n}\n\nexport function createSplitProjection(\n file: DiffFile,\n options: DiffProjectionOptions = {},\n): SplitDiffProjection {\n const leftRows: DiffRenderRow[] = []\n const rightRows: DiffRenderRow[] = []\n const hunkRows = new Map<number, number>()\n let previousOldEnd = 0\n let previousNewEnd = 0\n\n for (const [hunkIndex, hunk] of file.hunks.entries()) {\n const range = skippedRangeBeforeHunk(hunk, previousOldEnd, previousNewEnd)\n const separator = separatorRow(file, range, hunkIndex, options)\n if (separator) pushSplitHunkSeparator(leftRows, rightRows, separator)\n hunkRows.set(hunkIndex, separator ? leftRows.length - 1 : leftRows.length)\n if (separator?.expanded) pushSplitExpandedRows(file, leftRows, rightRows, range, hunkIndex)\n pushSplitHunkRows(leftRows, rightRows, hunk.lines, hunkIndex)\n previousOldEnd = hunkEndLine(hunk.oldStart, hunk.oldLines)\n previousNewEnd = hunkEndLine(hunk.newStart, hunk.newLines)\n }\n\n const trailing = trailingSkippedRange(file, previousOldEnd, previousNewEnd)\n const trailingSeparator = separatorRow(file, trailing, undefined, options)\n if (trailingSeparator) {\n pushSplitHunkSeparator(leftRows, rightRows, trailingSeparator)\n if (trailingSeparator.expanded)\n pushSplitExpandedRows(file, leftRows, rightRows, trailing, undefined)\n }\n\n if (leftRows.length === 0) pushUnchangedFileSplitRows(file, leftRows, rightRows)\n return { leftRows, rightRows, hunkRows }\n}\n\nexport function createStackedProjection(\n file: DiffFile,\n options: DiffProjectionOptions = {},\n): StackedDiffProjection {\n const rows: DiffRenderRow[] = []\n const hunkRows = new Map<number, number>()\n let previousOldEnd = 0\n let previousNewEnd = 0\n\n for (const [hunkIndex, hunk] of file.hunks.entries()) {\n const range = skippedRangeBeforeHunk(hunk, previousOldEnd, previousNewEnd)\n const separator = separatorRow(file, range, hunkIndex, options)\n if (separator) rows.push(separator)\n hunkRows.set(hunkIndex, separator ? rows.length - 1 : rows.length)\n if (separator?.expanded) pushStackedExpandedRows(file, rows, range, hunkIndex)\n pushStackedHunkRows(rows, hunk.lines, hunkIndex)\n previousOldEnd = hunkEndLine(hunk.oldStart, hunk.oldLines)\n previousNewEnd = hunkEndLine(hunk.newStart, hunk.newLines)\n }\n\n const trailing = trailingSkippedRange(file, previousOldEnd, previousNewEnd)\n const trailingSeparator = separatorRow(file, trailing, undefined, options)\n if (trailingSeparator) {\n rows.push(trailingSeparator)\n if (trailingSeparator.expanded) pushStackedExpandedRows(file, rows, trailing, undefined)\n }\n\n if (rows.length === 0) pushUnchangedFileStackedRows(file, rows)\n return { rows, hunkRows }\n}\n\nfunction pushSplitHunkSeparator(\n leftRows: DiffRenderRow[],\n rightRows: DiffRenderRow[],\n row: DiffRenderRow,\n): void {\n leftRows.push(row)\n rightRows.push(row)\n}\n\nfunction pushSplitHunkRows(\n leftRows: DiffRenderRow[],\n rightRows: DiffRenderRow[],\n lines: readonly DiffHunkLine[],\n hunkIndex: number,\n): void {\n let index = 0\n while (index < lines.length) {\n const nextIndex = pushNextSplitRows(leftRows, rightRows, lines, index, hunkIndex)\n index = Math.max(index + 1, nextIndex)\n }\n}\n\nfunction pushSplitExpandedRows(\n file: DiffFile,\n leftRows: DiffRenderRow[],\n rightRows: DiffRenderRow[],\n range: SkippedRange,\n hunkIndex: number | undefined,\n): void {\n for (let index = 0; index < range.count; index += 1) {\n leftRows.push(expandedSideRow(file.oldLines, range.oldStart + index, 'old', hunkIndex))\n rightRows.push(expandedSideRow(file.newLines, range.newStart + index, 'new', hunkIndex))\n }\n}\n\nfunction pushNextSplitRows(\n leftRows: DiffRenderRow[],\n rightRows: DiffRenderRow[],\n lines: readonly DiffHunkLine[],\n index: number,\n hunkIndex: number,\n): number {\n const line = lines[index]\n if (!line) return index + 1\n if (line.type === 'context') {\n pushSplitContextLine(leftRows, rightRows, line, hunkIndex)\n return index + 1\n }\n\n const blockEnd = firstContextIndex(lines, index)\n pushSplitChangeBlock(leftRows, rightRows, changeBlock(lines, index, blockEnd), hunkIndex)\n return blockEnd\n}\n\nfunction pushSplitContextLine(\n leftRows: DiffRenderRow[],\n rightRows: DiffRenderRow[],\n line: DiffHunkLine,\n hunkIndex: number,\n): void {\n leftRows.push(renderRowFromLine(line, 'context', 'old', hunkIndex))\n rightRows.push(renderRowFromLine(line, 'context', 'new', hunkIndex))\n}\n\nfunction pushSplitChangeBlock(\n leftRows: DiffRenderRow[],\n rightRows: DiffRenderRow[],\n block: ChangeBlock,\n hunkIndex: number,\n): void {\n const count = Math.max(block.deletions.length, block.additions.length)\n for (let index = 0; index < count; index += 1) {\n leftRows.push(splitDeletionRow(block.deletions[index], hunkIndex))\n rightRows.push(splitAdditionRow(block.additions[index], hunkIndex))\n }\n}\n\nfunction splitDeletionRow(line: DiffHunkLine | undefined, hunkIndex: number): DiffRenderRow {\n if (!line) return placeholderRow(hunkIndex)\n return renderRowFromLine(line, 'deletion', 'old', hunkIndex, line.oldInlineRanges)\n}\n\nfunction splitAdditionRow(line: DiffHunkLine | undefined, hunkIndex: number): DiffRenderRow {\n if (!line) return placeholderRow(hunkIndex)\n return renderRowFromLine(line, 'addition', 'new', hunkIndex, line.newInlineRanges)\n}\n\nfunction pushStackedHunkRows(\n rows: DiffRenderRow[],\n lines: readonly DiffHunkLine[],\n hunkIndex: number,\n): void {\n for (const line of lines) rows.push(stackedRowFromLine(line, hunkIndex))\n}\n\nfunction pushStackedExpandedRows(\n file: DiffFile,\n rows: DiffRenderRow[],\n range: SkippedRange,\n hunkIndex: number | undefined,\n): void {\n for (let index = 0; index < range.count; index += 1) {\n rows.push(\n expandedBothRow(file.newLines, range.oldStart + index, range.newStart + index, hunkIndex),\n )\n }\n}\n\nfunction stackedRowFromLine(line: DiffHunkLine, hunkIndex: number): DiffRenderRow {\n if (line.type === 'deletion') {\n return renderRowFromLine(line, 'deletion', 'old', hunkIndex, line.oldInlineRanges)\n }\n if (line.type === 'addition') {\n return renderRowFromLine(line, 'addition', 'new', hunkIndex, line.newInlineRanges)\n }\n\n return renderRowFromLine(line, 'context', 'both', hunkIndex)\n}\n\nfunction renderRowFromLine(\n line: DiffHunkLine,\n type: 'context' | 'addition' | 'deletion',\n side: 'old' | 'new' | 'both',\n hunkIndex: number | undefined,\n inlineRanges: readonly DiffInlineRange[] = [],\n): DiffRenderRow {\n return {\n type,\n text: renderLineText(line.text),\n oldLineNumber: side !== 'new' ? line.oldLineNumber : undefined,\n newLineNumber: side !== 'old' ? line.newLineNumber : undefined,\n hunkIndex,\n inlineRanges,\n }\n}\n\nfunction renderLineText(text: string): string {\n if (!isRawHunkHeader(text)) return text\n return ''\n}\n\nfunction isRawHunkHeader(text: string): boolean {\n return /^@@ -\\d+(?:,\\d+)? \\+\\d+(?:,\\d+)? @@/.test(text.trim())\n}\n\nfunction separatorRow(\n file: DiffFile,\n range: SkippedRange,\n hunkIndex: number | undefined,\n options: DiffProjectionOptions,\n): DiffRenderRow | null {\n if (range.count <= 0) return null\n\n const expandKey = regionKey(range)\n const expanded = options.expandedRegions?.has(expandKey) ?? false\n const expandable = skippedRangeAvailable(file, range)\n const label = hunkSeparatorText(range.count, expandable, expanded)\n return {\n expanded,\n expandable,\n expandKey,\n type: 'hunk',\n text: label,\n hunkIndex,\n skippedLines: range.count,\n }\n}\n\n// A collapsed region is identified by where it starts on both sides rather than\n// by its ordinal, so expansion state cannot land on an unrelated region when the\n// same path is re-diffed into a different set of hunks.\nfunction regionKey(range: SkippedRange): string {\n return `${range.oldStart}:${range.newStart}`\n}\n\nfunction hunkSeparatorText(lines: number, expandable: boolean, expanded: boolean): string {\n const suffix = `${lines} unmodified line${lines === 1 ? '' : 's'}`\n if (!expandable) return suffix\n return `${expanded ? 'Hide' : 'Show'} ${suffix}`\n}\n\nfunction skippedRangeAvailable(file: DiffFile, range: SkippedRange): boolean {\n if (range.count <= 0) return false\n if (range.oldStart < 1 || range.newStart < 1) return false\n if (range.oldStart + range.count - 1 > file.oldLines.length) return false\n return range.newStart + range.count - 1 <= file.newLines.length\n}\n\nfunction skippedRangeBeforeHunk(\n hunk: DiffHunk,\n previousOldEnd: number,\n previousNewEnd: number,\n): SkippedRange {\n return {\n count: skippedUnmodifiedLines(hunk, previousOldEnd, previousNewEnd),\n newStart: previousNewEnd + 1,\n oldStart: previousOldEnd + 1,\n }\n}\n\nfunction trailingSkippedRange(\n file: DiffFile,\n previousOldEnd: number,\n previousNewEnd: number,\n): SkippedRange {\n // Only a whole-file diff knows how long the tail is; a parsed patch carries\n // just the hunk lines, so its trailing region stays unknown and unrendered.\n if (file.hunks.length === 0 || file.isPartial) return { count: 0, newStart: 1, oldStart: 1 }\n\n const oldRemaining = contentLineCount(file.oldLines) - previousOldEnd\n const newRemaining = contentLineCount(file.newLines) - previousNewEnd\n return {\n count: Math.max(oldRemaining, newRemaining, 0),\n newStart: previousNewEnd + 1,\n oldStart: previousOldEnd + 1,\n }\n}\n\nfunction contentLineCount(lines: readonly string[]): number {\n if (lines.length === 0) return 0\n if (lines.at(-1) === '') return lines.length - 1\n return lines.length\n}\n\nfunction expandedSideRow(\n lines: readonly string[],\n lineNumber: number,\n side: 'old' | 'new',\n hunkIndex: number | undefined,\n): DiffRenderRow {\n const text = lines[lineNumber - 1] ?? ''\n\n return renderRowFromLine(\n {\n newLineNumber: side === 'new' ? lineNumber : undefined,\n oldLineNumber: side === 'old' ? lineNumber : undefined,\n text,\n type: 'context',\n },\n 'context',\n side,\n hunkIndex,\n )\n}\n\nfunction expandedBothRow(\n lines: readonly string[],\n oldLineNumber: number,\n newLineNumber: number,\n hunkIndex: number | undefined,\n): DiffRenderRow {\n return renderRowFromLine(\n {\n newLineNumber,\n oldLineNumber,\n text: lines[newLineNumber - 1] ?? '',\n type: 'context',\n },\n 'context',\n 'both',\n hunkIndex,\n )\n}\n\nfunction skippedUnmodifiedLines(\n hunk: DiffHunk,\n previousOldEnd: number,\n previousNewEnd: number,\n): number {\n return Math.max(hunk.oldStart - previousOldEnd - 1, hunk.newStart - previousNewEnd - 1, 0)\n}\n\nfunction hunkEndLine(start: number, count: number): number {\n if (count <= 0) return Math.max(0, start)\n return start + count - 1\n}\n\nfunction placeholderRow(hunkIndex: number): DiffRenderRow {\n return {\n type: 'placeholder',\n text: '',\n hunkIndex,\n }\n}\n\nfunction emptyRow(text: string): DiffRenderRow {\n return {\n type: 'empty',\n text,\n }\n}\n\nfunction pushNoChangesRows(leftRows: DiffRenderRow[], rightRows: DiffRenderRow[]): void {\n leftRows.push(emptyRow('No changes'))\n rightRows.push(emptyRow('No changes'))\n}\n\n/**\n * A whole-file diff with nothing changed still has a file in it — a pure rename, a mode change, two\n * sides that happen to match. Showing the text is the only useful thing a viewer can do with that,\n * and it is what every other diff viewer does; the alternative is a pane holding the word \"No\n * changes\", which tells the reader less than the file would.\n *\n * Rendered as plain context rows rather than as one collapsed region: there is no change for a\n * collapsed region to sit next to, so the reader would be one click away from every diff they\n * opened. A patch-only file (`isPartial`) has no text to fall back to and keeps the message.\n */\nfunction pushUnchangedFileSplitRows(\n file: DiffFile,\n leftRows: DiffRenderRow[],\n rightRows: DiffRenderRow[],\n): void {\n const range = unchangedFileRange(file)\n if (range.count <= 0) {\n pushNoChangesRows(leftRows, rightRows)\n return\n }\n\n pushSplitExpandedRows(file, leftRows, rightRows, range, undefined)\n}\n\nfunction pushUnchangedFileStackedRows(file: DiffFile, rows: DiffRenderRow[]): void {\n const range = unchangedFileRange(file)\n if (range.count <= 0) {\n rows.push(emptyRow('No changes'))\n return\n }\n\n pushStackedExpandedRows(file, rows, range, undefined)\n}\n\nfunction unchangedFileRange(file: DiffFile): SkippedRange {\n if (file.isPartial) return { count: 0, newStart: 1, oldStart: 1 }\n\n const count = Math.max(contentLineCount(file.oldLines), contentLineCount(file.newLines))\n return { count, newStart: 1, oldStart: 1 }\n}\n\nfunction firstContextIndex(lines: readonly DiffHunkLine[], start: number): number {\n let index = start\n while (index < lines.length && lines[index]?.type !== 'context') index += 1\n return index\n}\n\nfunction changeBlock(lines: readonly DiffHunkLine[], start: number, end: number): ChangeBlock {\n return {\n deletions: lines.slice(start, end).filter((line) => line.type === 'deletion'),\n additions: lines.slice(start, end).filter((line) => line.type === 'addition'),\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { EditorDisposable } from '@singapore-editor/core/extensions';
2
+ import type { DiffFile } from './types';
3
+ /**
4
+ * Which collapsed regions are expanded, and who owns that answer.
5
+ *
6
+ * Split mode is two editors holding two documents, which means two plugin instances — and a
7
+ * separator row is one region shown twice, once per side. If each instance kept its own expansion
8
+ * set, one gutter click would expand the left pane and leave the right where it was, and every row
9
+ * below would be misaligned. That is the single property split mode exists to hold (§C7).
10
+ *
11
+ * So expansion is a store the sides *share* rather than state each side owns. A host building a
12
+ * split view creates one store and passes it to both plugins; a stacked host passes nothing and
13
+ * gets a private one. This is not the mirroring §C5 forbids — nobody is keeping a second copy in
14
+ * sync. There is one copy, and both sides read it.
15
+ */
16
+ export type DiffRegionStore = {
17
+ getExpandedRegions(): ReadonlySet<string>;
18
+ isExpanded(key: string): boolean;
19
+ toggleRegion(key: string): void;
20
+ /**
21
+ * Points the store at a diff. Expansion is per *diff*, not per path — see the note below — so
22
+ * this drops the current set whenever the identity changes.
23
+ */
24
+ setFile(file: DiffFile | null): void;
25
+ onDidChange(listener: () => void): EditorDisposable;
26
+ };
27
+ export declare function createDiffRegionStore(): DiffRegionStore;
28
+ //# sourceMappingURL=regions.d.ts.map