Skip to content

Commit 321d23a

Browse files
fix(files): title renames by their new path and expand collapsed lines one run at a time
1 parent 2602094 commit 321d23a

3 files changed

Lines changed: 55 additions & 14 deletions

File tree

‎apps/sim/components/diff/diff-view.tsx‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ function CollapsedRun({ count, onExpand }: CollapsedRunProps) {
101101
}
102102

103103
/** Splits a hunk into visible lines and collapsed runs of unchanged lines. */
104-
function visibleItems(lines: DiffLine[], expanded: boolean) {
104+
function visibleItems(lines: DiffLine[], isExpanded: (start: number) => boolean) {
105105
const items: Array<{ line: DiffLine } | { collapsed: number; key: number }> = []
106106
for (let index = 0; index < lines.length; ) {
107107
if (lines[index].type !== 'context') {
@@ -115,7 +115,11 @@ function visibleItems(lines: DiffLine[], expanded: boolean) {
115115
const trailing = index === lines.length
116116
const keepBefore = leading ? 0 : CONTEXT_EDGE
117117
const keepAfter = trailing ? 0 : CONTEXT_EDGE
118-
if (expanded || run.length <= COLLAPSE_AFTER || run.length <= keepBefore + keepAfter + 1) {
118+
if (
119+
isExpanded(start) ||
120+
run.length <= COLLAPSE_AFTER ||
121+
run.length <= keepBefore + keepAfter + 1
122+
) {
119123
for (const line of run) items.push({ line })
120124
continue
121125
}
@@ -147,7 +151,8 @@ function marker(line: DiffLine | undefined) {
147151
* when the hunk headers carry them.
148152
*/
149153
export function DiffView({ hunks }: DiffViewProps) {
150-
const [expanded, setExpanded] = useState<ReadonlySet<number>>(() => new Set())
154+
/** Expanded runs, keyed `hunk:start` so each collapsed run opens on its own. */
155+
const [expanded, setExpanded] = useState<ReadonlySet<string>>(() => new Set())
151156
const numbered = hunks.some((hunk) =>
152157
hunk.lines.some((line) => line.oldLine !== undefined || line.newLine !== undefined)
153158
)
@@ -157,8 +162,7 @@ export function DiffView({ hunks }: DiffViewProps) {
157162
<div className={cn('grid w-max min-w-full font-mono text-caption leading-[1.6]', columns)}>
158163
{hunks.map((hunk, hunkIndex) => {
159164
const segments = wordSegments(hunk.lines)
160-
const items = visibleItems(hunk.lines, expanded.has(hunkIndex))
161-
const expand = () => setExpanded((current) => new Set(current).add(hunkIndex))
165+
const items = visibleItems(hunk.lines, (start) => expanded.has(`${hunkIndex}:${start}`))
162166
return (
163167
<Fragment key={hunkIndex}>
164168
{(hunk.file || hunk.heading || hunkIndex > 0) && (
@@ -181,7 +185,13 @@ export function DiffView({ hunks }: DiffViewProps) {
181185
</span>
182186
</div>
183187
) : (
184-
<CollapsedRun key={`c${item.key}`} count={item.collapsed} onExpand={expand} />
188+
<CollapsedRun
189+
key={`c${item.key}`}
190+
count={item.collapsed}
191+
onExpand={() =>
192+
setExpanded((current) => new Set(current).add(`${hunkIndex}:${item.key}`))
193+
}
194+
/>
185195
)
186196
)}
187197
</Fragment>

‎apps/sim/lib/diff/unified.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@ describe('parseUnifiedDiff', () => {
5858
])
5959
})
6060

61+
it('titles a renamed file by its new path', () => {
62+
expect(parseUnifiedDiff('--- a/old.ts\n+++ b/new.ts\n@@ -1 +1 @@\n-a\n+b').path).toBe('new.ts')
63+
})
64+
65+
it('reads a changed line starting with --- after the headers as a change', () => {
66+
expect(
67+
parseUnifiedDiff('--- a/x.md\n+++ b/x.md\n--- divider\n+=== divider').hunks[0].lines
68+
).toEqual([
69+
{ type: 'del', text: '-- divider' },
70+
{ type: 'add', text: '=== divider' },
71+
])
72+
})
73+
6174
it('names each side of a two-document comparison', () => {
6275
const diff = parseUnifiedDiff(
6376
'--- sim:knowledge/kb/old\n+++ sim:knowledge/kb/new\n-Refunds are available within 14 days.\n+Annual plans can be refunded within 30 days.'
@@ -72,6 +85,11 @@ describe('parseUnifiedDiff', () => {
7285
['a knowledge source without a document', '--- sim:knowledge/kb_1\n-x\n+y', 'sim:knowledge/'],
7386
['a line without a marker', '@@ x @@\n-a\nplain', 'Line 3'],
7487
['no changes', ' just context', 'at least one + or - line'],
88+
[
89+
'a sim: source mixed with file paths',
90+
'diff --git a/x b/x\n--- sim:file/f1\n+++ sim:file/f1\n-a\n+b\ndiff --git a/y b/y\n--- a/y.ts\n+++ b/y.ts\n-c\n+d',
91+
'cannot also include ordinary file paths',
92+
],
7593
])('rejects %s', (_, text, message) => {
7694
expect(() => parseUnifiedDiff(text)).toThrow(message)
7795
})

‎apps/sim/lib/diff/unified.ts‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,27 +68,38 @@ export function parseUnifiedDiff(text: string): UnifiedDiff {
6868
const rows = text.replace(/\r\n?/g, '\n').replace(/\n$/, '').split('\n')
6969
let oldSource: DiffSource | null = null
7070
let newSource: DiffSource | null = null
71-
let path: string | null = null
7271
const hunks: DiffHunk[] = []
7372
let hunk: DiffHunk | null = null
7473
let oldLine: number | undefined
7574
let newLine: number | undefined
7675

7776
let file: string | null = null
77+
let namesPath = false
78+
/** Each file takes one `---` and one `+++` header; a later line starting that way is a change. */
79+
let seenOld = false
80+
let seenNew = false
7881
for (const [index, row] of rows.entries()) {
7982
if (row.startsWith('diff --git ')) {
8083
hunk = null
8184
file = null
85+
seenOld = false
86+
seenNew = false
8287
continue
8388
}
84-
if (!hunk && (row.startsWith('--- ') || row.startsWith('+++ '))) {
89+
const isOld = row.startsWith('--- ')
90+
const isNew = row.startsWith('+++ ')
91+
if (!hunk && ((isOld && !seenOld) || (isNew && !seenNew))) {
92+
if (isOld) seenOld = true
93+
else seenNew = true
8594
const target = row.slice(4).trim()
8695
const parsed = parseSource(target)
87-
if (row.startsWith('--- ')) oldSource = parsed ?? oldSource
88-
else newSource = parsed ?? newSource
89-
if (!parsed && target !== '/dev/null' && (row.startsWith('+++ ') || !file))
90-
file = target.replace(/^[ab]\//, '').split('\t')[0]
91-
path ??= file
96+
if (parsed) {
97+
if (isOld) oldSource = parsed
98+
else newSource = parsed
99+
} else if (target !== '/dev/null') {
100+
namesPath = true
101+
if (isNew || !file) file = target.replace(/^[ab]\//, '').split('\t')[0]
102+
}
92103
continue
93104
}
94105
if (!hunk && GIT_HEADER.test(row)) continue
@@ -121,10 +132,12 @@ export function parseUnifiedDiff(text: string): UnifiedDiff {
121132

122133
if (!hunks.some((entry) => entry.lines.some((line) => line.type !== 'context')))
123134
throw new Error('A diff needs at least one + or - line')
135+
if (namesPath && (oldSource || newSource))
136+
throw new Error('A diff with a sim: source cannot also include ordinary file paths')
124137
oldSource ??= newSource
125138
newSource ??= oldSource
126139
const files = new Set(hunks.map((entry) => entry.file))
140+
const path = files.size > 1 ? `${files.size} files` : (hunks[0]?.file ?? file)
127141
if (files.size <= 1) for (const entry of hunks) entry.file = undefined
128-
else path = `${files.size} files`
129142
return { oldSource, newSource, path: oldSource ? null : path, hunks }
130143
}

0 commit comments

Comments
 (0)