From c2a17dc27f401bf4389c88ffbfb87b32f29d0918 Mon Sep 17 00:00:00 2001 From: Shrey Banga Date: Sat, 29 Jun 2024 12:04:21 -0400 Subject: [PATCH 1/9] Checkpoint generalizing to hunks with more than two "parts" The parts here refer essentially to commits. In a combined diff, there are at least 3 commits in consideration. --- src/iterFormatHunk.ts | 186 ++++++++++++++++++------------------- src/iterSideBySideDiffs.ts | 68 +++++++------- 2 files changed, 124 insertions(+), 130 deletions(-) diff --git a/src/iterFormatHunk.ts b/src/iterFormatHunk.ts index cb887c4..4a3f40a 100644 --- a/src/iterFormatHunk.ts +++ b/src/iterFormatHunk.ts @@ -6,70 +6,67 @@ import { getChangesInLines } from './highlightChangesInLine'; import { iterFitTextToWidth } from './iterFitTextToWidth'; import { zip, zipAsync } from './zip'; +export type HunkPart = { + fileName: string; + startLineNo: number; + lines: (string | null)[]; +}; + async function* iterFormatHunkSplit( context: Context, - fileNameA: string, - fileNameB: string, - hunkLinesA: (string | null)[], - hunkLinesB: (string | null)[], - lineNoA: number, - lineNoB: number, + hunkParts: HunkPart[], lineChanges: (Change[] | null)[] ): AsyncIterable { const { MISSING_LINE_COLOR, BLANK_LINE } = context; - for (const [lineA, lineB, changes] of zip( - hunkLinesA, - hunkLinesB, - lineChanges + const lineNos = hunkParts.map((part) => part.startLineNo); + + for (const [changes, ...hunkPartLines] of zip( + lineChanges, + ...hunkParts.map((part) => part.lines) )) { - const formattedLinesA = formatAndFitHunkLine( - context, - fileNameA, - lineNoA, - lineA ?? null, - changes ?? null - ); - const formattedLinesB = formatAndFitHunkLine( - context, - fileNameB, - lineNoB, - lineB ?? null, - changes ?? null + const formattedLineIterables = hunkPartLines.map((hunkPartLine, i) => + formatAndFitHunkLine( + context, + hunkParts[i].fileName, + lineNos[i], + hunkPartLine ?? null, + changes ?? null + ) ); const missingLine = T().appendString(BLANK_LINE, MISSING_LINE_COLOR); - for await (const [formattedLineA, formattedLineB] of zipAsync( - formattedLinesA, - formattedLinesB + for await (const formattedLines of zipAsync( + ...formattedLineIterables )) { - yield T() - .appendSpannedString(formattedLineA ?? missingLine) - .appendSpannedString(formattedLineB ?? missingLine); + const formattedLine = T(); + for (const line of formattedLines) { + formattedLine.appendSpannedString(line ?? missingLine); + } + yield formattedLine; } - if (lineA !== null) { - lineNoA++; - } - if (lineB !== null) { - lineNoB++; - } + hunkPartLines.forEach((hunkPartLine, i) => { + if (hunkPartLine !== null && hunkPartLine !== undefined) { + lineNos[i]++; + } + }); } } async function* iterFormatHunkUnified( context: Context, - fileNameA: string, - fileNameB: string, - hunkLinesA: (string | null)[], - hunkLinesB: (string | null)[], - lineNoA: number, - lineNoB: number, + hunkParts: HunkPart[], lineChanges: (Change[] | null)[] ): AsyncIterable { - let indexA = 0, - indexB = 0; + let [{ fileName: fileNameA, lines: hunkLinesA }, ...restHunkParts] = + hunkParts; + let [indexA, ...restIndexes] = hunkParts.map(() => 0); + let [lineNoA, ...restLineNos] = hunkParts.map( + ({ startLineNo }) => startLineNo + ); + while (indexA < hunkLinesA.length) { const hunkLineA = hunkLinesA[indexA]; const prefixA = hunkLineA?.slice(0, 1) ?? null; @@ -92,21 +89,27 @@ async function* iterFormatHunkUnified( default: // indexA is pointing to an unmodified line, so yield all the // inserted lines from indexB up to this line - while (indexB < indexA) { - const hunkLineB = hunkLinesB[indexB]; - if (hunkLineB !== null) { - yield* formatAndFitHunkLine( - context, - fileNameB, - lineNoB, - hunkLineB, - lineChanges[indexB] - ); - lineNoB++; + for (let i = 0; i < restIndexes.length; i++) { + let indexB = restIndexes[i]; + let lineNoB = restLineNos[i]; + let hunkPartB = restHunkParts[i]; + while (indexB < indexA) { + const hunkLineB = hunkPartB.lines[indexB]; + if (hunkLineB !== null) { + yield* formatAndFitHunkLine( + context, + hunkPartB.fileName, + lineNoB, + hunkLineB, + lineChanges[indexB] + ); + lineNoB++; + } + indexB++; } - indexB++; + restIndexes[i] = indexB; + restLineNos[i] = lineNoB; } - // now yield the unmodified line, which should be present in both yield* formatAndFitHunkLine( context, @@ -116,8 +119,10 @@ async function* iterFormatHunkUnified( changes ); lineNoA++; - lineNoB++; - indexB++; + for (let i = 0; i < restIndexes.length; i++) { + restIndexes[i]++; + restLineNos[i]++; + } } indexA++; @@ -125,31 +130,31 @@ async function* iterFormatHunkUnified( // yield any remaining lines in hunk B, which can happen if there were more // insertions at the end of the hunk - while (indexB < hunkLinesB.length) { - const hunkLineB = hunkLinesB[indexB]; - if (hunkLineB !== null) { - yield* formatAndFitHunkLine( - context, - fileNameB, - lineNoB, - hunkLineB, - lineChanges[indexB] - ); - lineNoB++; + for (let i = 0; i < restIndexes.length; i++) { + let indexB = restIndexes[i]; + let lineNoB = restLineNos[i]; + let hunkPartB = restHunkParts[i]; + while (indexB < hunkPartB.lines.length) { + const hunkLineB = hunkPartB.lines[indexB]; + if (hunkLineB !== null) { + yield* formatAndFitHunkLine( + context, + restHunkParts[i].fileName, + lineNoB, + hunkLineB, + lineChanges[indexB] + ); + lineNoB++; + } + indexB++; } - indexB++; } } export async function* iterFormatHunk( context: Context, hunkHeaderLine: string, - fileNameA: string, - fileNameB: string, - hunkLinesA: (string | null)[], - hunkLinesB: (string | null)[], - lineNoA: number, - lineNoB: number + hunkParts: HunkPart[] ): AsyncIterable { const { HUNK_HEADER_COLOR, SCREEN_WIDTH, SPLIT_DIFFS } = context; @@ -160,29 +165,16 @@ export async function* iterFormatHunk( HUNK_HEADER_COLOR ); - const changes = getChangesInLines(context, hunkLinesA, hunkLinesB); + // TODO: Fix to handle multiple hunk parts + const changes = getChangesInLines( + context, + hunkParts[0].lines, + hunkParts[1].lines + ); if (SPLIT_DIFFS) { - yield* iterFormatHunkSplit( - context, - fileNameA, - fileNameB, - hunkLinesA, - hunkLinesB, - lineNoA, - lineNoB, - changes - ); + yield* iterFormatHunkSplit(context, hunkParts, changes); } else { - yield* iterFormatHunkUnified( - context, - fileNameA, - fileNameB, - hunkLinesA, - hunkLinesB, - lineNoA, - lineNoB, - changes - ); + yield* iterFormatHunkUnified(context, hunkParts, changes); } } diff --git a/src/iterSideBySideDiffs.ts b/src/iterSideBySideDiffs.ts index c9c50e7..af21166 100644 --- a/src/iterSideBySideDiffs.ts +++ b/src/iterSideBySideDiffs.ts @@ -5,7 +5,7 @@ import { applyFormatting, FormattedString, T } from './formattedString'; import { iterFormatCommitBodyLine } from './iterFormatCommitBodyLine'; import { iterFormatCommitHeaderLine } from './iterFormatCommitHeaderLine'; import { iterFormatFileName } from './iterFormatFileName'; -import { iterFormatHunk } from './iterFormatHunk'; +import { HunkPart, iterFormatHunk } from './iterFormatHunk'; const ANSI_COLOR_CODE_REGEX = ansiRegex(); @@ -22,9 +22,10 @@ type State = | 'unknown' | 'commit-header' | 'commit-body' - | 'diff' - | 'hunk-header' - | 'hunk-body'; + // "Unified" diffs (normal diffs) + | 'unified-diff' + | 'unified-diff-hunk-header' + | 'unified-diff-hunk-body'; async function* iterSideBySideDiffsFormatted( context: Context, @@ -45,30 +46,20 @@ async function* iterSideBySideDiffsFormatted( } // Hunk metadata - let startA: number = -1; - let startB: number = -1; + let hunkParts: HunkPart[] = []; let hunkHeaderLine: string = ''; - let hunkLinesA: (string | null)[] = []; - let hunkLinesB: (string | null)[] = []; async function* yieldHunk() { - yield* iterFormatHunk( - context, - hunkHeaderLine, - fileNameA, - fileNameB, - hunkLinesA, - hunkLinesB, - startA, - startB - ); - hunkLinesA = []; - hunkLinesB = []; + yield* iterFormatHunk(context, hunkHeaderLine, hunkParts); + for (const hunkPart of hunkParts) { + hunkPart.startLineNo = -1; + hunkPart.lines = []; + } } async function* flushPending() { - if (state === 'diff') { + if (state === 'unified-diff') { yield* yieldFileName(); - } else if (state === 'hunk-body') { + } else if (state === 'unified-diff-hunk-body') { yield* yieldHunk(); } } @@ -83,11 +74,11 @@ async function* iterSideBySideDiffsFormatted( } else if (state === 'commit-header' && line.startsWith(' ')) { nextState = 'commit-body'; } else if (line.startsWith('diff --git')) { - nextState = 'diff'; + nextState = 'unified-diff'; } else if (line.startsWith('@@ ')) { - nextState = 'hunk-header'; - } else if (state === 'hunk-header') { - nextState = 'hunk-body'; + nextState = 'unified-diff-hunk-header'; + } else if (state === 'unified-diff-hunk-header') { + nextState = 'unified-diff-hunk-body'; } else if ( state === 'commit-body' && line.length > 0 && @@ -102,14 +93,23 @@ async function* iterSideBySideDiffsFormatted( switch (nextState) { case 'commit-header': - if (state === 'hunk-header' || state === 'hunk-body') { + if ( + state === 'unified-diff-hunk-header' || + state === 'unified-diff-hunk-body' + ) { yield HORIZONTAL_SEPARATOR; } break; - case 'diff': + case 'unified-diff': fileNameA = ''; fileNameB = ''; break; + case 'unified-diff-hunk-header': + hunkParts = [ + { fileName: fileNameA, startLineNo: -1, lines: [] }, + { fileName: fileNameB, startLineNo: -1, lines: [] }, + ]; + break; case 'commit-body': isFirstCommitBodyLine = true; break; @@ -137,7 +137,7 @@ async function* iterSideBySideDiffsFormatted( isFirstCommitBodyLine = false; break; } - case 'diff': { + case 'unified-diff': { if (line.startsWith('--- a/')) { fileNameA = line.slice('--- a/'.length); } else if (line.startsWith('+++ b/')) { @@ -167,7 +167,7 @@ async function* iterSideBySideDiffsFormatted( } break; } - case 'hunk-header': { + case 'unified-diff-hunk-header': { const hunkHeaderStart = line.indexOf('@@ '); const hunkHeaderEnd = line.indexOf(' @@', hunkHeaderStart + 1); assert.ok(hunkHeaderStart >= 0); @@ -183,13 +183,15 @@ async function* iterSideBySideDiffsFormatted( const [startBString] = bHeader.split(','); assert.ok(startAString.startsWith('-')); - startA = parseInt(startAString.slice(1), 10); + hunkParts[0].startLineNo = parseInt(startAString.slice(1), 10); assert.ok(startBString.startsWith('+')); - startB = parseInt(startBString.slice(1), 10); + hunkParts[1].startLineNo = parseInt(startBString.slice(1), 10); break; } - case 'hunk-body': { + case 'unified-diff-hunk-body': { + const [{ lines: hunkLinesA }, { lines: hunkLinesB }] = + hunkParts; if (line.startsWith('-')) { hunkLinesA.push(line); } else if (line.startsWith('+')) { From 57dc2426eb05575168d0a95fc4dc7aebcef3f65d Mon Sep 17 00:00:00 2001 From: Shrey Banga Date: Sat, 29 Jun 2024 23:06:40 -0400 Subject: [PATCH 2/9] Checkpoint working combined diffs implementation Took a while to come up with something that looks correct. Essentially we show the final state as the last "part" i.e. vertical section and the rest of the parts show changes made in the corresponding parent commit. The line width is currently incorrect, will fix later. --- src/iterSideBySideDiffs.ts | 104 +++++++++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 4 deletions(-) diff --git a/src/iterSideBySideDiffs.ts b/src/iterSideBySideDiffs.ts index af21166..6306865 100644 --- a/src/iterSideBySideDiffs.ts +++ b/src/iterSideBySideDiffs.ts @@ -25,7 +25,11 @@ type State = // "Unified" diffs (normal diffs) | 'unified-diff' | 'unified-diff-hunk-header' - | 'unified-diff-hunk-body'; + | 'unified-diff-hunk-body' + // "Combined" diffs (diffs with multiple parents) + | 'combined-diff' + | 'combined-diff-hunk-header' + | 'combined-diff-hunk-body'; async function* iterSideBySideDiffsFormatted( context: Context, @@ -57,9 +61,12 @@ async function* iterSideBySideDiffsFormatted( } async function* flushPending() { - if (state === 'unified-diff') { + if (state === 'unified-diff' || state === 'combined-diff') { yield* yieldFileName(); - } else if (state === 'unified-diff-hunk-body') { + } else if ( + state === 'unified-diff-hunk-body' || + state === 'combined-diff-hunk-body' + ) { yield* yieldHunk(); } } @@ -79,6 +86,15 @@ async function* iterSideBySideDiffsFormatted( nextState = 'unified-diff-hunk-header'; } else if (state === 'unified-diff-hunk-header') { nextState = 'unified-diff-hunk-body'; + } else if ( + line.startsWith('diff --cc') || + line.startsWith('diff --combined') + ) { + nextState = 'combined-diff'; + } else if (line.startsWith('@@@ ')) { + nextState = 'combined-diff-hunk-header'; + } else if (state === 'combined-diff-hunk-header') { + nextState = 'combined-diff-hunk-body'; } else if ( state === 'commit-body' && line.length > 0 && @@ -137,7 +153,8 @@ async function* iterSideBySideDiffsFormatted( isFirstCommitBodyLine = false; break; } - case 'unified-diff': { + case 'unified-diff': + case 'combined-diff': { if (line.startsWith('--- a/')) { fileNameA = line.slice('--- a/'.length); } else if (line.startsWith('+++ b/')) { @@ -208,6 +225,85 @@ async function* iterSideBySideDiffsFormatted( } break; } + case 'combined-diff-hunk-header': { + const hunkHeaderStart = line.indexOf('@@@ '); + const hunkHeaderEnd = line.indexOf(' @@@', hunkHeaderStart + 1); + assert.ok(hunkHeaderStart >= 0); + assert.ok(hunkHeaderEnd > hunkHeaderStart); + const hunkHeader = line.slice( + hunkHeaderStart + 4, + hunkHeaderEnd + ); + hunkHeaderLine = line; + + const fileRanges = hunkHeader.split(' '); + hunkParts = []; + for (let i = 0; i < fileRanges.length; i++) { + const fileRange = fileRanges[i]; + const [fileRangeStart] = fileRange.slice(1).split(','); + hunkParts.push({ + fileName: + i === fileRanges.length - 1 ? fileNameB : fileNameA, + startLineNo: parseInt(fileRangeStart, 10), + lines: [], + }); + } + break; + } + case 'combined-diff-hunk-body': { + // A combined diff works differently from a unified diff. See + // https://git-scm.com/docs/git-diff#_combined_diff_format for + // details, but essentially we get a row of prefixes in each + // line indicating whether the line is present on the parent, + // the current commit, or both. We convert this into N+1 parts + // (for N parents) where the first part shows the current state + // and the rest show changes made in the corresponding parent. + const linePrefix = line.slice(0, hunkParts.length - 1); + const lineSuffix = line.slice(hunkParts.length - 1); + const isLineAdded = linePrefix.includes('+'); + const isLineRemoved = linePrefix.includes('-'); + + // First N parts show changes made in the corresponding parent + // Either the line is going to be: + // 1. In the current commit and missing in some parents, which + // will have + prefixes, or + // 2. Missing in the current commit and present in some parents, + // which will have - prefixes. + // 3. Present in all commits, which will all have a space + // prefix. + let i = 0; + while (i < hunkParts.length - 1) { + const hunkPart = hunkParts[i]; + const partPrefix = linePrefix[i]; + if (isLineAdded) { + if (partPrefix === '+') { + hunkPart.lines.push(null); + } else { + hunkPart.lines.push('+' + lineSuffix); + } + } else if (isLineRemoved) { + if (partPrefix === '-') { + hunkPart.lines.push(' ' + lineSuffix); + } else { + hunkPart.lines.push('-' + lineSuffix); + } + } else { + hunkPart.lines.push(' ' + lineSuffix); + } + i++; + } + // Final part shows the current state, so we just display the + // lines that exist in it without any highlighting. + if (isLineRemoved) { + hunkParts[i].lines.push(null); + } else if (isLineAdded) { + hunkParts[i].lines.push('+' + lineSuffix); + } else { + hunkParts[i].lines.push(' ' + lineSuffix); + } + + break; + } } } From 8ce04f9ba8bc730ab573c2ae7161b0517071918d Mon Sep 17 00:00:00 2001 From: Shrey Banga Date: Sun, 30 Jun 2024 11:09:28 -0400 Subject: [PATCH 3/9] Calculate line width correctly for more than 2 parts --- src/context.ts | 12 ------------ src/formatAndFitHunkLine.ts | 11 ++++++----- src/iterFormatHunk.ts | 13 +++++++++++-- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/context.ts b/src/context.ts index 53f3924..17d2f65 100644 --- a/src/context.ts +++ b/src/context.ts @@ -11,8 +11,6 @@ export type Context = Config & { CHALK: ChalkInstance; SPLIT_DIFFS: boolean; SCREEN_WIDTH: number; - LINE_WIDTH: number; - BLANK_LINE: string; HORIZONTAL_SEPARATOR: FormattedString; HIGHLIGHTER?: shikiji.Highlighter; }; @@ -27,14 +25,6 @@ export async function getContextForConfig( // Only split diffs if there's enough room const SPLIT_DIFFS = SCREEN_WIDTH >= config.MIN_LINE_WIDTH * 2; - let LINE_WIDTH: number; - if (SPLIT_DIFFS) { - LINE_WIDTH = Math.floor(SCREEN_WIDTH / 2); - } else { - LINE_WIDTH = SCREEN_WIDTH; - } - - const BLANK_LINE = ''.padStart(LINE_WIDTH); const HORIZONTAL_SEPARATOR = T() .fillWidth(SCREEN_WIDTH, '─') .addSpan(0, SCREEN_WIDTH, config.BORDER_COLOR); @@ -51,8 +41,6 @@ export async function getContextForConfig( CHALK: chalk, SCREEN_WIDTH, SPLIT_DIFFS, - LINE_WIDTH, - BLANK_LINE, HORIZONTAL_SEPARATOR, HIGHLIGHTER, }; diff --git a/src/formatAndFitHunkLine.ts b/src/formatAndFitHunkLine.ts index d88e5f7..1c3f189 100644 --- a/src/formatAndFitHunkLine.ts +++ b/src/formatAndFitHunkLine.ts @@ -11,14 +11,13 @@ const LINE_NUMBER_WIDTH = 5; export async function* formatAndFitHunkLine( context: Context, + lineWidth: number, fileName: string, lineNo: number, line: string | null, changes: Change[] | null ): AsyncIterable { const { - BLANK_LINE, - LINE_WIDTH, MISSING_LINE_COLOR, DELETED_LINE_COLOR, DELETED_LINE_NO_COLOR, @@ -28,10 +27,12 @@ export async function* formatAndFitHunkLine( UNMODIFIED_LINE_NO_COLOR, } = context; + const blankLine = ''.padStart(lineWidth); + // A line number of 0 happens when we read the "No newline at end of file" // message as a line at the end of a deleted/inserted file. if (line === null || lineNo === 0) { - yield T().appendString(BLANK_LINE, MISSING_LINE_COLOR); + yield T().appendString(blankLine, MISSING_LINE_COLOR); return; } @@ -59,9 +60,9 @@ export async function* formatAndFitHunkLine( Each line is rendered as follows: - So (LINE_NUMBER_WIDTH + 2 + 1 + 1 + lineTextWidth) * 2 = LINE_WIDTH + So (LINE_NUMBER_WIDTH + 2 + 1 + 1 + lineTextWidth) * 2 = lineWidth */ - const lineTextWidth = LINE_WIDTH - 2 - 1 - 1 - LINE_NUMBER_WIDTH; + const lineTextWidth = lineWidth - 2 - 1 - 1 - LINE_NUMBER_WIDTH; let isFirstLine = true; const formattedLine = T().appendString(lineText); diff --git a/src/iterFormatHunk.ts b/src/iterFormatHunk.ts index 4a3f40a..fdecd36 100644 --- a/src/iterFormatHunk.ts +++ b/src/iterFormatHunk.ts @@ -17,7 +17,9 @@ async function* iterFormatHunkSplit( hunkParts: HunkPart[], lineChanges: (Change[] | null)[] ): AsyncIterable { - const { MISSING_LINE_COLOR, BLANK_LINE } = context; + const { MISSING_LINE_COLOR } = context; + const lineWidth = context.SCREEN_WIDTH / hunkParts.length; + const blankLine = ''.padStart(lineWidth); const lineNos = hunkParts.map((part) => part.startLineNo); @@ -28,6 +30,7 @@ async function* iterFormatHunkSplit( const formattedLineIterables = hunkPartLines.map((hunkPartLine, i) => formatAndFitHunkLine( context, + lineWidth, hunkParts[i].fileName, lineNos[i], hunkPartLine ?? null, @@ -35,7 +38,7 @@ async function* iterFormatHunkSplit( ) ); - const missingLine = T().appendString(BLANK_LINE, MISSING_LINE_COLOR); + const missingLine = T().appendString(blankLine, MISSING_LINE_COLOR); for await (const formattedLines of zipAsync( ...formattedLineIterables @@ -60,6 +63,8 @@ async function* iterFormatHunkUnified( hunkParts: HunkPart[], lineChanges: (Change[] | null)[] ): AsyncIterable { + const lineWidth = context.SCREEN_WIDTH; + let [{ fileName: fileNameA, lines: hunkLinesA }, ...restHunkParts] = hunkParts; let [indexA, ...restIndexes] = hunkParts.map(() => 0); @@ -79,6 +84,7 @@ async function* iterFormatHunkUnified( case '-': yield* formatAndFitHunkLine( context, + lineWidth, fileNameA, lineNoA, hunkLineA, @@ -98,6 +104,7 @@ async function* iterFormatHunkUnified( if (hunkLineB !== null) { yield* formatAndFitHunkLine( context, + lineWidth, hunkPartB.fileName, lineNoB, hunkLineB, @@ -113,6 +120,7 @@ async function* iterFormatHunkUnified( // now yield the unmodified line, which should be present in both yield* formatAndFitHunkLine( context, + lineWidth, fileNameA, lineNoA, hunkLineA, @@ -139,6 +147,7 @@ async function* iterFormatHunkUnified( if (hunkLineB !== null) { yield* formatAndFitHunkLine( context, + lineWidth, restHunkParts[i].fileName, lineNoB, hunkLineB, From 4f38cb974936e6746a884e874c67e67de9f9ac0c Mon Sep 17 00:00:00 2001 From: Shrey Banga Date: Sun, 30 Jun 2024 12:47:50 -0400 Subject: [PATCH 4/9] Implement unified diff rendering for combined diffs The naming is getting confusing here, but the approach we had for rendering a unified diff for "unified diffs" wasn't great for "combined diffs" because it would end up repeating additions/deletions. So, I added a separate implementation which essentially renders what you would see in the last column of the split diff. This required showing deletions in the split diff as well, which I had previously skipped. I think that still looks fine, so will let it be. --- src/iterFormatHunk.ts | 163 +++-------------------------------- src/iterFormatHunkSplit.ts | 64 ++++++++++++++ src/iterFormatHunkUnified.ts | 139 +++++++++++++++++++++++++++++ src/iterSideBySideDiffs.ts | 15 ++-- 4 files changed, 220 insertions(+), 161 deletions(-) create mode 100644 src/iterFormatHunkSplit.ts create mode 100644 src/iterFormatHunkUnified.ts diff --git a/src/iterFormatHunk.ts b/src/iterFormatHunk.ts index fdecd36..369167b 100644 --- a/src/iterFormatHunk.ts +++ b/src/iterFormatHunk.ts @@ -1,10 +1,12 @@ -import { Change } from 'diff'; import { Context } from './context'; -import { formatAndFitHunkLine } from './formatAndFitHunkLine'; import { T, FormattedString } from './formattedString'; import { getChangesInLines } from './highlightChangesInLine'; import { iterFitTextToWidth } from './iterFitTextToWidth'; -import { zip, zipAsync } from './zip'; +import { iterFormatHunkSplit } from './iterFormatHunkSplit'; +import { + iterFormatCombinedDiffHunkUnified, + iterFormatUnifiedDiffHunkUnified, +} from './iterFormatHunkUnified'; export type HunkPart = { fileName: string; @@ -12,156 +14,9 @@ export type HunkPart = { lines: (string | null)[]; }; -async function* iterFormatHunkSplit( - context: Context, - hunkParts: HunkPart[], - lineChanges: (Change[] | null)[] -): AsyncIterable { - const { MISSING_LINE_COLOR } = context; - const lineWidth = context.SCREEN_WIDTH / hunkParts.length; - const blankLine = ''.padStart(lineWidth); - - const lineNos = hunkParts.map((part) => part.startLineNo); - - for (const [changes, ...hunkPartLines] of zip( - lineChanges, - ...hunkParts.map((part) => part.lines) - )) { - const formattedLineIterables = hunkPartLines.map((hunkPartLine, i) => - formatAndFitHunkLine( - context, - lineWidth, - hunkParts[i].fileName, - lineNos[i], - hunkPartLine ?? null, - changes ?? null - ) - ); - - const missingLine = T().appendString(blankLine, MISSING_LINE_COLOR); - - for await (const formattedLines of zipAsync( - ...formattedLineIterables - )) { - const formattedLine = T(); - for (const line of formattedLines) { - formattedLine.appendSpannedString(line ?? missingLine); - } - yield formattedLine; - } - - hunkPartLines.forEach((hunkPartLine, i) => { - if (hunkPartLine !== null && hunkPartLine !== undefined) { - lineNos[i]++; - } - }); - } -} - -async function* iterFormatHunkUnified( - context: Context, - hunkParts: HunkPart[], - lineChanges: (Change[] | null)[] -): AsyncIterable { - const lineWidth = context.SCREEN_WIDTH; - - let [{ fileName: fileNameA, lines: hunkLinesA }, ...restHunkParts] = - hunkParts; - let [indexA, ...restIndexes] = hunkParts.map(() => 0); - let [lineNoA, ...restLineNos] = hunkParts.map( - ({ startLineNo }) => startLineNo - ); - - while (indexA < hunkLinesA.length) { - const hunkLineA = hunkLinesA[indexA]; - const prefixA = hunkLineA?.slice(0, 1) ?? null; - const changes = lineChanges[indexA]; - - switch (prefixA) { - case null: - // Ignore the missing lines we insert to match up indexes - break; - case '-': - yield* formatAndFitHunkLine( - context, - lineWidth, - fileNameA, - lineNoA, - hunkLineA, - changes - ); - lineNoA++; - break; - default: - // indexA is pointing to an unmodified line, so yield all the - // inserted lines from indexB up to this line - for (let i = 0; i < restIndexes.length; i++) { - let indexB = restIndexes[i]; - let lineNoB = restLineNos[i]; - let hunkPartB = restHunkParts[i]; - while (indexB < indexA) { - const hunkLineB = hunkPartB.lines[indexB]; - if (hunkLineB !== null) { - yield* formatAndFitHunkLine( - context, - lineWidth, - hunkPartB.fileName, - lineNoB, - hunkLineB, - lineChanges[indexB] - ); - lineNoB++; - } - indexB++; - } - restIndexes[i] = indexB; - restLineNos[i] = lineNoB; - } - // now yield the unmodified line, which should be present in both - yield* formatAndFitHunkLine( - context, - lineWidth, - fileNameA, - lineNoA, - hunkLineA, - changes - ); - lineNoA++; - for (let i = 0; i < restIndexes.length; i++) { - restIndexes[i]++; - restLineNos[i]++; - } - } - - indexA++; - } - - // yield any remaining lines in hunk B, which can happen if there were more - // insertions at the end of the hunk - for (let i = 0; i < restIndexes.length; i++) { - let indexB = restIndexes[i]; - let lineNoB = restLineNos[i]; - let hunkPartB = restHunkParts[i]; - while (indexB < hunkPartB.lines.length) { - const hunkLineB = hunkPartB.lines[indexB]; - if (hunkLineB !== null) { - yield* formatAndFitHunkLine( - context, - lineWidth, - restHunkParts[i].fileName, - lineNoB, - hunkLineB, - lineChanges[indexB] - ); - lineNoB++; - } - indexB++; - } - } -} - export async function* iterFormatHunk( context: Context, + diffType: 'unified-diff' | 'combined-diff', hunkHeaderLine: string, hunkParts: HunkPart[] ): AsyncIterable { @@ -183,7 +38,9 @@ export async function* iterFormatHunk( if (SPLIT_DIFFS) { yield* iterFormatHunkSplit(context, hunkParts, changes); - } else { - yield* iterFormatHunkUnified(context, hunkParts, changes); + } else if (diffType === 'unified-diff') { + yield* iterFormatUnifiedDiffHunkUnified(context, hunkParts, changes); + } else if (diffType === 'combined-diff') { + yield* iterFormatCombinedDiffHunkUnified(context, hunkParts, changes); } } diff --git a/src/iterFormatHunkSplit.ts b/src/iterFormatHunkSplit.ts new file mode 100644 index 0000000..a53a681 --- /dev/null +++ b/src/iterFormatHunkSplit.ts @@ -0,0 +1,64 @@ +import { Change } from 'diff'; +import { Context } from './context'; +import { formatAndFitHunkLine } from './formatAndFitHunkLine'; +import { T, FormattedString } from './formattedString'; +import { zip, zipAsync } from './zip'; +import { HunkPart } from './iterFormatHunk'; + +export async function* iterFormatHunkSplit( + context: Context, + hunkParts: HunkPart[], + lineChanges: (Change[] | null)[] +): AsyncIterable { + const { MISSING_LINE_COLOR } = context; + const lineWidth = context.SCREEN_WIDTH / hunkParts.length; + const blankLine = ''.padStart(lineWidth); + + const lineNos = hunkParts.map((part) => part.startLineNo); + const numDeletes = hunkParts.map(() => 0); + + for (const [changes, ...hunkPartLines] of zip( + lineChanges, + ...hunkParts.map((part) => part.lines) + )) { + // Count deletions and adjust line numbers for previous deletions + hunkPartLines.forEach((hunkPartLine, i) => { + const prefix = hunkPartLine?.slice(0, 1) ?? null; + if (prefix === '-') { + numDeletes[i]++; + } else if (prefix === '+') { + lineNos[i] -= numDeletes[i]; + numDeletes[i] = 0; + } + }); + + const formattedLineIterables = hunkPartLines.map((hunkPartLine, i) => + formatAndFitHunkLine( + context, + lineWidth, + hunkParts[i].fileName, + lineNos[i], + hunkPartLine ?? null, + changes ?? null + ) + ); + + const missingLine = T().appendString(blankLine, MISSING_LINE_COLOR); + + for await (const formattedLines of zipAsync( + ...formattedLineIterables + )) { + const formattedLine = T(); + for (const line of formattedLines) { + formattedLine.appendSpannedString(line ?? missingLine); + } + yield formattedLine; + } + + hunkPartLines.forEach((hunkPartLine, i) => { + if (hunkPartLine !== null && hunkPartLine !== undefined) { + lineNos[i]++; + } + }); + } +} diff --git a/src/iterFormatHunkUnified.ts b/src/iterFormatHunkUnified.ts new file mode 100644 index 0000000..70f5267 --- /dev/null +++ b/src/iterFormatHunkUnified.ts @@ -0,0 +1,139 @@ +import { Change } from 'diff'; +import { Context } from './context'; +import { formatAndFitHunkLine } from './formatAndFitHunkLine'; +import { FormattedString } from './formattedString'; +import { HunkPart } from './iterFormatHunk'; + +/** + * Formats a "unified diff" hunk i.e. a hunk from a traditional diff. + */ +export async function* iterFormatUnifiedDiffHunkUnified( + context: Context, + hunkParts: HunkPart[], + lineChanges: (Change[] | null)[] +): AsyncIterable { + const lineWidth = context.SCREEN_WIDTH; + + let [ + { fileName: fileNameA, lines: hunkLinesA, startLineNo: lineNoA }, + { fileName: fileNameB, lines: hunkLinesB, startLineNo: lineNoB }, + ] = hunkParts; + + let indexA = 0, + indexB = 0; + while (indexA < hunkLinesA.length) { + const hunkLineA = hunkLinesA[indexA]; + const prefixA = hunkLineA?.slice(0, 1) ?? null; + + switch (prefixA) { + case null: + // Ignore the missing lines we insert to match up indexes + break; + case '-': + yield* formatAndFitHunkLine( + context, + lineWidth, + fileNameA, + lineNoA, + hunkLineA, + lineChanges[indexA] + ); + lineNoA++; + break; + default: + // indexA is pointing to an unmodified line, so yield all the + // inserted lines from indexB up to this line + while (indexB < indexA) { + const hunkLineB = hunkLinesB[indexB]; + if (hunkLineB !== null) { + yield* formatAndFitHunkLine( + context, + lineWidth, + fileNameB, + lineNoB, + hunkLineB, + lineChanges[indexB] + ); + lineNoB++; + } + indexB++; + } + + // now yield the unmodified line, which should be present in both + yield* formatAndFitHunkLine( + context, + lineWidth, + fileNameA, + lineNoA, + hunkLineA, + lineChanges[indexB] + ); + lineNoA++; + lineNoB++; + indexB++; + } + + indexA++; + } + + // yield any remaining lines in hunk B, which can happen if there were more + // insertions at the end of the hunk + while (indexB < hunkLinesB.length) { + const hunkLineB = hunkLinesB[indexB]; + if (hunkLineB !== null) { + yield* formatAndFitHunkLine( + context, + lineWidth, + fileNameB, + lineNoB, + hunkLineB, + lineChanges[indexB] + ); + lineNoB++; + } + indexB++; + } +} + +/** + * Formats a "combined diff" hunk i.e. a hunk from a combined diff, generated by + * --cc or --combined options, which are defaults for merge commits. + */ +export async function* iterFormatCombinedDiffHunkUnified( + context: Context, + hunkParts: HunkPart[], + lineChanges: (Change[] | null)[] +): AsyncIterable { + const lineWidth = context.SCREEN_WIDTH; + + // The final hunk part shows the current state of the file, so we just + // display that with additions and deletions highlighted. + let { fileName, lines, startLineNo } = hunkParts[hunkParts.length - 1]; + let lineNo = startLineNo; + let numDeletes = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const prefix = line?.slice(0, 1) ?? null; + switch (prefix) { + case '-': + numDeletes++; + break; + case '+': + lineNo -= numDeletes; + numDeletes = 0; + break; + default: + break; + } + yield* formatAndFitHunkLine( + context, + lineWidth, + fileName, + lineNo, + line, + lineChanges[i] + ); + lineNo++; + } +} diff --git a/src/iterSideBySideDiffs.ts b/src/iterSideBySideDiffs.ts index 6306865..33a3254 100644 --- a/src/iterSideBySideDiffs.ts +++ b/src/iterSideBySideDiffs.ts @@ -52,8 +52,8 @@ async function* iterSideBySideDiffsFormatted( // Hunk metadata let hunkParts: HunkPart[] = []; let hunkHeaderLine: string = ''; - async function* yieldHunk() { - yield* iterFormatHunk(context, hunkHeaderLine, hunkParts); + async function* yieldHunk(diffType: 'unified-diff' | 'combined-diff') { + yield* iterFormatHunk(context, diffType, hunkHeaderLine, hunkParts); for (const hunkPart of hunkParts) { hunkPart.startLineNo = -1; hunkPart.lines = []; @@ -63,11 +63,10 @@ async function* iterSideBySideDiffsFormatted( async function* flushPending() { if (state === 'unified-diff' || state === 'combined-diff') { yield* yieldFileName(); - } else if ( - state === 'unified-diff-hunk-body' || - state === 'combined-diff-hunk-body' - ) { - yield* yieldHunk(); + } else if (state === 'unified-diff-hunk-body') { + yield* yieldHunk('unified-diff'); + } else if (state === 'combined-diff-hunk-body') { + yield* yieldHunk('combined-diff'); } } @@ -295,7 +294,7 @@ async function* iterSideBySideDiffsFormatted( // Final part shows the current state, so we just display the // lines that exist in it without any highlighting. if (isLineRemoved) { - hunkParts[i].lines.push(null); + hunkParts[i].lines.push('-' + lineSuffix); } else if (isLineAdded) { hunkParts[i].lines.push('+' + lineSuffix); } else { From de57f10e9a4a9936072c8e655d404403e9edd380 Mon Sep 17 00:00:00 2001 From: Shrey Banga Date: Sun, 30 Jun 2024 22:43:03 -0400 Subject: [PATCH 5/9] Make sure lineWidth is not fractional --- src/iterFormatHunkSplit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iterFormatHunkSplit.ts b/src/iterFormatHunkSplit.ts index a53a681..42c33d1 100644 --- a/src/iterFormatHunkSplit.ts +++ b/src/iterFormatHunkSplit.ts @@ -11,7 +11,7 @@ export async function* iterFormatHunkSplit( lineChanges: (Change[] | null)[] ): AsyncIterable { const { MISSING_LINE_COLOR } = context; - const lineWidth = context.SCREEN_WIDTH / hunkParts.length; + const lineWidth = Math.floor(context.SCREEN_WIDTH / hunkParts.length); const blankLine = ''.padStart(lineWidth); const lineNos = hunkParts.map((part) => part.startLineNo); From 0c62e0f60f00de67ffedb19eae74fdf2f582eb31 Mon Sep 17 00:00:00 2001 From: Shrey Banga Date: Sun, 30 Jun 2024 22:43:59 -0400 Subject: [PATCH 6/9] Handle more than 2 parents in combined diffs The "@"s in the header vary by number of parents, so we need to handle that. --- src/iterSideBySideDiffs.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/iterSideBySideDiffs.ts b/src/iterSideBySideDiffs.ts index 33a3254..21dc152 100644 --- a/src/iterSideBySideDiffs.ts +++ b/src/iterSideBySideDiffs.ts @@ -18,6 +18,9 @@ const ANSI_COLOR_CODE_REGEX = ansiRegex(); const BINARY_FILES_DIFF_REGEX = /^Binary files (?:a\/(.*)|\/dev\/null) and (?:b\/(.*)|\/dev\/null) differ$/; +// Combined hunk headers begin with N+1 @ characters for N parents +const COMBINED_HUNK_HEADER_START_REGEX = /^(@{2,}) /; + type State = | 'unknown' | 'commit-header' @@ -90,7 +93,7 @@ async function* iterSideBySideDiffsFormatted( line.startsWith('diff --combined') ) { nextState = 'combined-diff'; - } else if (line.startsWith('@@@ ')) { + } else if (COMBINED_HUNK_HEADER_START_REGEX.test(line)) { nextState = 'combined-diff-hunk-header'; } else if (state === 'combined-diff-hunk-header') { nextState = 'combined-diff-hunk-body'; @@ -225,14 +228,13 @@ async function* iterSideBySideDiffsFormatted( break; } case 'combined-diff-hunk-header': { - const hunkHeaderStart = line.indexOf('@@@ '); - const hunkHeaderEnd = line.indexOf(' @@@', hunkHeaderStart + 1); + const match = COMBINED_HUNK_HEADER_START_REGEX.exec(line); + assert.ok(match); + const hunkHeaderStart = match.index + match[0].length; // End of the opening "@@@ " + const hunkHeaderEnd = line.lastIndexOf(' ' + match[1]); // Start of the closing " @@@" assert.ok(hunkHeaderStart >= 0); assert.ok(hunkHeaderEnd > hunkHeaderStart); - const hunkHeader = line.slice( - hunkHeaderStart + 4, - hunkHeaderEnd - ); + const hunkHeader = line.slice(hunkHeaderStart, hunkHeaderEnd); hunkHeaderLine = line; const fileRanges = hunkHeader.split(' '); From 93cd259a479fed3bbe36d8e18327efd7ec40e3aa Mon Sep 17 00:00:00 2001 From: Shrey Banga Date: Sun, 30 Jun 2024 22:44:41 -0400 Subject: [PATCH 7/9] Switch how we show deleted lines in combined diffs On re-reading "A - character in the column N means that the line appears in fileN but it does not appear in the result.", we should be showing the line in the files where it has a '-' prefix and not showing it in other files. --- src/iterSideBySideDiffs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/iterSideBySideDiffs.ts b/src/iterSideBySideDiffs.ts index 21dc152..ae38fed 100644 --- a/src/iterSideBySideDiffs.ts +++ b/src/iterSideBySideDiffs.ts @@ -284,9 +284,9 @@ async function* iterSideBySideDiffsFormatted( } } else if (isLineRemoved) { if (partPrefix === '-') { - hunkPart.lines.push(' ' + lineSuffix); - } else { hunkPart.lines.push('-' + lineSuffix); + } else { + hunkPart.lines.push(null); } } else { hunkPart.lines.push(' ' + lineSuffix); From 787ea909444308a9e0964446806a9bc362b74c16 Mon Sep 17 00:00:00 2001 From: Shrey Banga Date: Sun, 30 Jun 2024 22:56:18 -0400 Subject: [PATCH 8/9] Add test cases for merge commits Found some commits in the TypeScript repo that have non-empty combined diffs using: ``` git rev-list --merges --all --min-parents=2 | while read commit; do if [ -n "$(git show --format='' $commit)" ]; then echo $commit; fi; done ``` --- src/__snapshots__/index.test.ts.snap | 1045 ++++++++++++++++++++++++++ src/index.test.ts | 119 +++ 2 files changed, 1164 insertions(+) diff --git a/src/__snapshots__/index.test.ts.snap b/src/__snapshots__/index.test.ts.snap index d09ee0c..c11cc91 100644 --- a/src/__snapshots__/index.test.ts.snap +++ b/src/__snapshots__/index.test.ts.snap @@ -281,6 +281,118 @@ exports[`inlineChangesHighlighted empty 1`] = ` " `; +exports[`inlineChangesHighlighted merge commit with 2 parents 1`] = ` +" +commit 3f504f4fbc1caf9c10814d48d8897a34f8a34dec +Merge: 2439767601 fbcdb8cf4f +Author: Gabriela Araujo Britto +Date: Thu Dec 21 17:57:42 2023 -0800 + + Merge branch 'main' into gabritto/d2 + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/binder.ts +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +@@@ -137,9 -136,9 +137,10 @@@ import + 137 isBlock, 136 isBlock, 137 isBlock, + 138 isBlockOrCatchScoped, 137 isBlockOrCatchScoped, 138 isBlockOrCatchScoped, + 139 IsBlockScopedContainer, 138 IsBlockScopedContainer, 139 IsBlockScopedContainer, + 139 + isBooleanLiteral, 140 + isBooleanLiteral, + 140 isCallExpression, 140 isCallExpression, 141 isCallExpression, + 141 isClassStaticBlockDeclarati 141 isClassStaticBlockDeclarati 142 isClassStaticBlockDeclarati + 142 + isConditionalExpression, 143 + isConditionalExpression, + 143 isConditionalTypeNode, 142 isConditionalTypeNode, 144 isConditionalTypeNode, + 144 IsContainer, 143 IsContainer, 145 IsContainer, + 145 isDeclaration, 144 isDeclaration, 146 isDeclaration, +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/types.ts +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +@@@ -5985,8 -6063,7 +6063,8 @@@ export interface NodeLinks + 5985 decoratorSignature?: Signat 6063 decoratorSignature?: Signat 6063 decoratorSignature?: Signat + 5986 spreadIndices?: { first: nu 6064 spreadIndices?: { first: nu 6064 spreadIndices?: { first: nu + 5987 parameterInitializerContain 6065 parameterInitializerContain 6065 parameterInitializerContain + 5988 - fakeScopeForSignatureDeclar 6066 - fakeScopeForSignatureDeclar + 5988 + contextualReturnType?: Type 6066 + contextualReturnType?: Type + 6066 + fakeScopeForSignatureDeclar 6067 + fakeScopeForSignatureDeclar + 5989 assertionExpressionType?: T 6067 assertionExpressionType?: T 6068 assertionExpressionType?: T + 5990 } 6068 } 6069 } +" +`; + +exports[`inlineChangesHighlighted merge commit with 3 parents 1`] = ` +" +commit d6d6a4aedfa78794c1b611c13d2ed1d3a66e1798 +Merge: 0dc976df1e 5f16a48236 3eadbf6c96 +Author: Andy Hanson +Date: Thu Sep 1 12:52:42 2016 -0700 + + Merge branch 'goto_definition_super', remote-tracking branch 'origin' into constructor_references + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ■■ src/services/services.ts +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +@@@@ -2788,26 -2792,18 -2788,34 +2792,42 @@@@ namespace ts + 2788 return node & 2792 return node & 2788 return node & 2792 return node & + 2789 } 2793 } 2789 } 2793 } + 2790 2794 2790 2794 + 2791 + function climbPas 2791 + function climbPas 2795 + function climbPas + 2792 + return isRigh 2792 + return isRigh 2796 + return isRigh + 2793 + } 2793 + } 2797 + } + 2794 + 2794 + 2798 + + 2795 - function climbPas 2799 - function climbPas + 2796 - return isRigh 2800 - return isRigh + 2799 + /** Get \`C\` given + 2800 + function tryGetCl + 2801 + return tryGet + 2795 + } 2802 + } + 2796 + 2803 + + 2795 function isCallEx 2795 function isCallEx 2797 function isCallEx 2804 function isCallEx + 2796 - if (isRightSi 2805 - if (isRightSi + 2797 - node = no 2806 - node = no + 2798 - } 2807 - } + 2798 - node = climbP 2808 - node = climbP + 2799 - return node & 2799 - return node & 2809 - return node & + 2796 + return isCall 2805 + return isCall + 2797 } 2800 } 2800 } 2806 } + 2798 2801 2801 2807 + 2799 function isNewExp 2802 function isNewExp 2802 function isNewExp 2808 function isNewExp + 2803 - if (isRightSi 2809 - if (isRightSi + 2804 - node = no 2810 - node = no + 2805 - } 2811 - } + 2803 - node = climbP 2812 - node = climbP + 2806 - return node & 2804 - return node & 2813 - return node & + 2800 + return isCall 2809 + return isCall + 2801 + } 2810 + } + 2802 + 2811 + + 2803 + function isCallOr 2812 + function isCallOr + 2804 + const target 2813 + const target + 2805 + return target 2814 + return target + 2806 + } 2815 + } + 2807 + 2816 + + 2808 - /** Get \`C\` given 2817 - /** Get \`C\` given + 2809 - function tryGetCl 2818 - function tryGetCl + 2810 - return tryGet 2819 - return tryGet + 2817 + function climbPas + 2818 + return isRigh + 2801 + } 2819 + } + 2802 + 2820 + + 2803 + /** Returns a Cal 2821 + /** Returns a Cal + 2804 + function getAnces 2822 + function getAnces + 2805 + const target 2823 + const target + 2806 + const callLik 2824 + const callLik + 2807 + return callLi 2825 + return callLi + 2808 + } 2826 + } + 2809 + 2827 + + 2810 + function tryGetSi 2828 + function tryGetSi + 2811 + const callLik 2829 + const callLik + 2812 + return callLi 2830 + return callLi + 2811 } 2807 } 2813 } 2831 } + 2812 2808 2814 2832 + 2813 function isNameOf 2809 function isNameOf 2815 function isNameOf 2833 function isNameOf + 2814 2810 2816 2834 +" +`; + exports[`inlineChangesHighlighted multiple inserts and deletes in the same hunk 1`] = ` " commit e5f896655402f8cf2d947c528d45e1d56bbf5717 @@ -658,6 +770,458 @@ exports[`splitWithWrapping empty 1`] = ` " `; +exports[`splitWithWrapping merge commit with 2 parents 1`] = ` +" +commit 3f504f4fbc1caf9c10814d48d8897a34f8a34dec +Merge: 2439767601 fbcdb8cf4f +Author: Gabriela Araujo Britto +Date: Thu Dec 21 17:57:42 2023 -0800 + + Merge branch 'main' into gabritto/d2 + +──────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/binder.ts +──────────────────────────────────────────────────────────────────────────────── +@@@ -137,9 -136,9 +137,10 @@@ import + 137 isBlock, 136 isBlock, 137 isBlock, + 138 isBlockOrCatc 137 isBlockOrCatc 138 isBlockOrCatc + hScoped, hScoped, hScoped, + 139 IsBlockScoped 138 IsBlockScoped 139 IsBlockScoped + Container, Container, Container, + 139 + 140 + + isBooleanLiteral, isBooleanLiteral, + 140 140 141 + isCallExpression, isCallExpression, isCallExpression, + 141 isClassStatic 141 isClassStatic 142 isClassStatic + BlockDeclaration, BlockDeclaration, BlockDeclaration, + 142 + isConditional 143 + isConditional + Expression, Expression, + 143 isConditional 142 isConditional 144 isConditional + TypeNode, TypeNode, TypeNode, + 144 IsContainer, 143 IsContainer, 145 IsContainer, + 145 144 146 + isDeclaration, isDeclaration, isDeclaration, +──────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/types.ts +──────────────────────────────────────────────────────────────────────────────── +@@@ -5985,8 -6063,7 +6063,8 @@@ export interface NodeLinks + 5985 decoratorSign 6063 decoratorSign 6063 decoratorSign + ature?: ature?: ature?: + Signature; // Signature; // Signature; // + Signature for Signature for Signature for + decorator as if decorator as if decorator as if + invoked by the invoked by the invoked by the + runtime. runtime. runtime. + 5986 6064 6064 + spreadIndices?: { spreadIndices?: { spreadIndices?: { + first: number | first: number | first: number | + undefined, last: undefined, last: undefined, last: + number | number | number | + undefined }; // undefined }; // undefined }; // + Indices of first Indices of first Indices of first + and last spread and last spread and last spread + elements in array elements in array elements in array + literal literal literal + 5987 parameterInit 6065 parameterInit 6065 parameterInit + ializerContainsUn ializerContainsUn ializerContainsUn + defined?: defined?: defined?: + boolean; // True boolean; // True boolean; // True + if this is a if this is a if this is a + parameter parameter parameter + declaration whose declaration whose declaration whose + type annotation type annotation type annotation + contains contains contains + "undefined". "undefined". "undefined". + 5988 - fakeScopeForS 6066 - fakeScopeForS + ignatureDeclarati ignatureDeclarati + on?: boolean; // on?: boolean; // + True if this is a True if this is a + fake scope fake scope + injected into an injected into an + enclosing enclosing + declaration declaration + chain. chain. + 5988 + contextualRet 6066 + contextualRet + urnType?: Type; urnType?: Type; + // If the // If the + node is a return node is a return + statement's statement's + expression, then expression, then + this is the this is the + contextual return contextual return + type. type. + 6066 + fakeScopeForS 6067 + fakeScopeForS + ignatureDeclarati ignatureDeclarati + on?: "params" | on?: "params" | + "typeParams"; // "typeParams"; // + If present, this If present, this + is a fake scope is a fake scope + injected into an injected into an + enclosing enclosing + declaration declaration + chain. chain. + 5989 assertionExpr 6067 assertionExpr 6068 assertionExpr + essionType?: essionType?: essionType?: + Type; // Type; // Type; // + Cached type of Cached type of Cached type of + the expression of the expression of the expression of + a type assertion a type assertion a type assertion + 5990 } 6068 } 6069 } +" +`; + +exports[`splitWithWrapping merge commit with 3 parents 1`] = ` +" +commit d6d6a4aedfa78794c1b611c13d2ed1d3a66e1798 +Merge: 0dc976df1e 5f16a48236 3eadbf6c96 +Author: Andy Hanson +Date: Thu Sep 1 12:52:42 2016 -0700 + + Merge branch 'goto_definition_super', remote-tracking branch 'origin' into +constructor_references + +──────────────────────────────────────────────────────────────────────────────── + ■■ src/services/services.ts +──────────────────────────────────────────────────────────────────────────────── +@@@@ -2788,26 -2792,18 -2788,34 +2792,42 @@@@ namespace ts + 2788 2792 2788 2792 + return node return node return node return node + && && && && + node.parent node.parent node.parent node.parent + && node.pa && node.pa && node.pa && node.pa + rent.kind rent.kind rent.kind rent.kind + === SyntaxK === SyntaxK === SyntaxK === SyntaxK + ind.Propert ind.Propert ind.Propert ind.Propert + yAccessExpr yAccessExpr yAccessExpr yAccessExpr + ession && ( ession && ( ession && ( ession && ( + node.pa ion>node.pa ion>node.pa ion>node.pa + rent).name rent).name rent).name rent).name + === node; === node; === node; === node; + 2789 } 2793 } 2789 } 2793 } + 2790 2794 2790 2794 + 2791 + 2791 + 2795 + + function cl function cl function cl + imbPastProp imbPastProp imbPastProp + ertyAccess( ertyAccess( ertyAccess( + node: Node) node: Node) node: Node) + { { { + 2792 + 2792 + 2796 + + return isRi return isRi return isRi + ghtSideOfPr ghtSideOfPr ghtSideOfPr + opertyAcces opertyAcces opertyAcces + s(node) ? s(node) ? s(node) ? + node.parent node.parent node.parent + : node; : node; : node; + 2793 + } 2793 + } 2797 + } + 2794 + 2794 + 2798 + + 2795 - 2799 - + function cl function cl + imbPastMany imbPastMany + PropertyAcc PropertyAcc + esses(node: esses(node: + Node): Node): + Node { Node { + 2796 - 2800 - + return isRi return isRi + ghtSideOfPr ghtSideOfPr + opertyAcces opertyAcces + s(node) ? c s(node) ? c + limbPastMan limbPastMan + yPropertyAc yPropertyAc + cesses(node cesses(node + .parent) : .parent) : + node; node; + 2799 + /** Get + \`C\` given + \`N\` if \`N\` + is in the + position + \`class C + extends N\` + or \`class C + extends + foo.N\` + where \`N\` + is an + identifier. + */ + 2800 + + function tr + yGetClassEx + tendingIden + tifier(node + : Node): Cl + assLikeDecl + aration | + undefined { + 2801 + + return tryG + etClassExte + ndingExpres + sionWithTyp + eArguments( + climbPastPr + opertyAcces + s(node).par + ent); + 2795 + } 2802 + } + 2796 + 2803 + + 2795 2795 2797 2804 + function is function is function is function is + CallExpress CallExpress CallExpress CallExpress + ionTarget(n ionTarget(n ionTarget(n ionTarget(n + ode: Node): ode: Node): ode: Node): ode: Node): + boolean { boolean { boolean { boolean { + 2796 - if 2805 - if + (isRightSid (isRightSid + eOfProperty eOfProperty + Access(node Access(node + )) { )) { + 2797 - 2806 - + node = nod node = nod + e.parent; e.parent; + 2798 - } 2807 - } + 2798 - 2808 - + node = clim node = clim + bPastProper bPastProper + tyAccess(no tyAccess(no + de); de); + 2799 - 2799 - 2809 - + return node return node return node + && && && + node.parent node.parent node.parent + && node.pa && node.pa && node.pa + rent.kind rent.kind rent.kind + === SyntaxK === SyntaxK === SyntaxK + ind.CallExp ind.CallExp ind.CallExp + ression && ression && ression && + (node. ssion>node. ssion>node. + parent).exp parent).exp parent).exp + ression === ression === ression === + node; node; node; + 2796 + 2805 + + return isCa return isCa + llOrNewExpr llOrNewExpr + essionTarge essionTarge + t(node, Syn t(node, Syn + taxKind.Cal taxKind.Cal + lExpression lExpression + ); ); + 2797 } 2800 } 2800 } 2806 } + 2798 2801 2801 2807 + 2799 2802 2802 2808 + function is function is function is function is + NewExpressi NewExpressi NewExpressi NewExpressi + onTarget(no onTarget(no onTarget(no onTarget(no + de: Node): de: Node): de: Node): de: Node): + boolean { boolean { boolean { boolean { + 2803 - if 2809 - if + (isRightSid (isRightSid + eOfProperty eOfProperty + Access(node Access(node + )) { )) { + 2804 - 2810 - + node = nod node = nod + e.parent; e.parent; + 2805 - } 2811 - } + 2803 - 2812 - + node = clim node = clim + bPastProper bPastProper + tyAccess(no tyAccess(no + de); de); + 2806 - 2804 - 2813 - + return node return node return node + && && && + node.parent node.parent node.parent + && node.pa && node.pa && node.pa + rent.kind rent.kind rent.kind + === SyntaxK === SyntaxK === SyntaxK + ind.NewExpr ind.NewExpr ind.NewExpr + ession && ( ession && ( ession && ( + node.p sion>node.p sion>node.p + arent).expr arent).expr arent).expr + ession === ession === ession === + node; node; node; + 2800 + 2809 + + return isCa return isCa + llOrNewExpr llOrNewExpr + essionTarge essionTarge + t(node, Syn t(node, Syn + taxKind.New taxKind.New + Expression) Expression) + ; ; + 2801 + } 2810 + } + 2802 + 2811 + + 2803 + 2812 + + function is function is + CallOrNewEx CallOrNewEx + pressionTar pressionTar + get(node: get(node: + Node, kind: Node, kind: + + SyntaxKind) SyntaxKind) + { { + 2804 + 2813 + + const const + target = cl target = cl + imbPastProp imbPastProp + ertyAccess( ertyAccess( + node); node); + 2805 + 2814 + + return return + target && t target && t + arget.paren arget.paren + t && target t && target + .parent.kin .parent.kin + d === kind d === kind + && (ta pression>ta + rget.parent rget.parent + ).expressio ).expressio + n === n === + target; target; + 2806 + } 2815 + } + 2807 + 2816 + + 2808 - /** Get 2817 - /** Get + \`C\` given \`C\` given + \`N\` if \`N\` \`N\` if \`N\` + is in the is in the + position position + \`class C \`class C + extends N\` extends N\` + or \`class C or \`class C + extends extends + foo.N\` foo.N\` + where \`N\` where \`N\` + is an is an + identifier. identifier. + */ */ + 2809 - 2818 - + function tr function tr + yGetClassEx yGetClassEx + tendingIden tendingIden + tifier(node tifier(node + : Node): Cl : Node): Cl + assLikeDecl assLikeDecl + aration | aration | + undefined { undefined { + 2810 - 2819 - + return tryG return tryG + etClassExte etClassExte + ndingExpres ndingExpres + sionWithTyp sionWithTyp + eArguments( eArguments( + climbPastPr climbPastPr + opertyAcces opertyAcces + s(node).par s(node).par + ent); ent); + 2817 + + function cl + imbPastMany + PropertyAcc + esses(node: + Node): + Node { + 2818 + + return isRi + ghtSideOfPr + opertyAcces + s(node) ? c + limbPastMan + yPropertyAc + cesses(node + .parent) : + node; + 2801 + } 2819 + } + 2802 + 2820 + + 2803 + /** 2821 + /** + Returns a C Returns a C + allLikeExpr allLikeExpr + ession ession + where where + \`node\` is \`node\` is + the target the target + being being + invoked. */ invoked. */ + 2804 + 2822 + + function ge function ge + tAncestorCa tAncestorCa + llLikeExpre llLikeExpre + ssion(node: ssion(node: + Node): Cal Node): Cal + lLikeExpres lLikeExpres + sion | sion | + undefined { undefined { + 2805 + 2823 + + const const + target = cl target = cl + imbPastMany imbPastMany + PropertyAcc PropertyAcc + esses(node) esses(node) + ; ; + 2806 + 2824 + + const const + callLike = callLike = + target.pare target.pare + nt; nt; + 2807 + 2825 + + return return + callLike && callLike && + isCallLike isCallLike + Expression( Expression( + callLike) callLike) + && getInvok && getInvok + edExpressio edExpressio + n(callLike) n(callLike) + === target === target + && && + callLike; callLike; + 2808 + } 2826 + } + 2809 + 2827 + + 2810 + 2828 + + function tr function tr + yGetSignatu yGetSignatu + reDeclarati reDeclarati + on(typeChec on(typeChec + ker: TypeCh ker: TypeCh + ecker, ecker, + node: node: + Node): Sign Node): Sign + atureDeclar atureDeclar + ation | ation | + undefined { undefined { + 2811 + 2829 + + const const + callLike = callLike = + getAncestor getAncestor + CallLikeExp CallLikeExp + ression(nod ression(nod + e); e); + 2812 + 2830 + + return return + callLike && callLike && + typeChecke typeChecke + r.getResolv r.getResolv + edSignature edSignature + (callLike). (callLike). + declaration declaration + ; ; + 2811 } 2807 } 2813 } 2831 } + 2812 2808 2814 2832 + 2813 2809 2815 2833 + function is function is function is function is + NameOfModul NameOfModul NameOfModul NameOfModul + eDeclaratio eDeclaratio eDeclaratio eDeclaratio + n(node: n(node: n(node: n(node: + Node) { Node) { Node) { Node) { + 2814 2810 2816 2834 +" +`; + exports[`splitWithWrapping multiple inserts and deletes in the same hunk 1`] = ` " commit e5f896655402f8cf2d947c528d45e1d56bbf5717 @@ -1023,6 +1587,118 @@ exports[`splitWithoutWrapping empty 1`] = ` " `; +exports[`splitWithoutWrapping merge commit with 2 parents 1`] = ` +" +commit 3f504f4fbc1caf9c10814d48d8897a34f8a34dec +Merge: 2439767601 fbcdb8cf4f +Author: Gabriela Araujo Britto +Date: Thu Dec 21 17:57:42 2023 -0800 + + Merge branch 'main' into gabritto/d2 + +──────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/binder.ts +──────────────────────────────────────────────────────────────────────────────── +@@@ -137,9 -136,9 +137,10 @@@ import + 137 isBlock, 136 isBlock, 137 isBlock, + 138 isBlockOrCatc 137 isBlockOrCatc 138 isBlockOrCatc + 139 IsBlockScoped 138 IsBlockScoped 139 IsBlockScoped + 139 + isBooleanLite 140 + isBooleanLite + 140 isCallExpress 140 isCallExpress 141 isCallExpress + 141 isClassStatic 141 isClassStatic 142 isClassStatic + 142 + isConditional 143 + isConditional + 143 isConditional 142 isConditional 144 isConditional + 144 IsContainer, 143 IsContainer, 145 IsContainer, + 145 isDeclaration 144 isDeclaration 146 isDeclaration +──────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/types.ts +──────────────────────────────────────────────────────────────────────────────── +@@@ -5985,8 -6063,7 +6063,8 @@@ export interface NodeLinks + 5985 decoratorSign 6063 decoratorSign 6063 decoratorSign + 5986 spreadIndices 6064 spreadIndices 6064 spreadIndices + 5987 parameterInit 6065 parameterInit 6065 parameterInit + 5988 - fakeScopeForS 6066 - fakeScopeForS + 5988 + contextualRet 6066 + contextualRet + 6066 + fakeScopeForS 6067 + fakeScopeForS + 5989 assertionExpr 6067 assertionExpr 6068 assertionExpr + 5990 } 6068 } 6069 } +" +`; + +exports[`splitWithoutWrapping merge commit with 3 parents 1`] = ` +" +commit d6d6a4aedfa78794c1b611c13d2ed1d3a66e1798 +Merge: 0dc976df1e 5f16a48236 3eadbf6c96 +Author: Andy Hanson +Date: Thu Sep 1 12:52:42 2016 -0700 + + Merge branch 'goto_definition_super', remote-tracking branch 'origin' into c + +──────────────────────────────────────────────────────────────────────────────── + ■■ src/services/services.ts +──────────────────────────────────────────────────────────────────────────────── +@@@@ -2788,26 -2792,18 -2788,34 +2792,42 @@@@ namespace ts + 2788 ret 2792 ret 2788 ret 2792 ret + 2789 } 2793 } 2789 } 2793 } + 2790 2794 2790 2794 + 2791 + functio 2791 + functio 2795 + functio + 2792 + ret 2792 + ret 2796 + ret + 2793 + } 2793 + } 2797 + } + 2794 + 2794 + 2798 + + 2795 - functio 2799 - functio + 2796 - ret 2800 - ret + 2799 + /** Get + 2800 + functio + 2801 + ret + 2795 + } 2802 + } + 2796 + 2803 + + 2795 functio 2795 functio 2797 functio 2804 functio + 2796 - if 2805 - if + 2797 - 2806 - + 2798 - } 2807 - } + 2798 - nod 2808 - nod + 2799 - ret 2799 - ret 2809 - ret + 2796 + ret 2805 + ret + 2797 } 2800 } 2800 } 2806 } + 2798 2801 2801 2807 + 2799 functio 2802 functio 2802 functio 2808 functio + 2803 - if 2809 - if + 2804 - 2810 - + 2805 - } 2811 - } + 2803 - nod 2812 - nod + 2806 - ret 2804 - ret 2813 - ret + 2800 + ret 2809 + ret + 2801 + } 2810 + } + 2802 + 2811 + + 2803 + functio 2812 + functio + 2804 + con 2813 + con + 2805 + ret 2814 + ret + 2806 + } 2815 + } + 2807 + 2816 + + 2808 - /** Get 2817 - /** Get + 2809 - functio 2818 - functio + 2810 - ret 2819 - ret + 2817 + functio + 2818 + ret + 2801 + } 2819 + } + 2802 + 2820 + + 2803 + /** Ret 2821 + /** Ret + 2804 + functio 2822 + functio + 2805 + con 2823 + con + 2806 + con 2824 + con + 2807 + ret 2825 + ret + 2808 + } 2826 + } + 2809 + 2827 + + 2810 + functio 2828 + functio + 2811 + con 2829 + con + 2812 + ret 2830 + ret + 2811 } 2807 } 2813 } 2831 } + 2812 2808 2814 2832 + 2813 functio 2809 functio 2815 functio 2833 functio + 2814 2810 2816 2834 +" +`; + exports[`splitWithoutWrapping multiple inserts and deletes in the same hunk 1`] = ` " commit e5f896655402f8cf2d947c528d45e1d56bbf5717 @@ -1366,6 +2042,118 @@ exports[`syntaxHighlighted empty 1`] = ` " `; +exports[`syntaxHighlighted merge commit with 2 parents 1`] = ` +" +commit 3f504f4fbc1caf9c10814d48d8897a34f8a34dec +Merge: 2439767601 fbcdb8cf4f +Author: Gabriela Araujo Britto +Date: Thu Dec 21 17:57:42 2023 -0800 + + Merge branch 'main' into gabritto/d2 + +──────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/binder.ts +──────────────────────────────────────────────────────────────────────────────── +@@@ -137,9 -136,9 +137,10 @@@ import + 137 ░░░░░░░░░░░░ 136 ░░░░░░░░░░░░ 137 ░░░░░░░░░░░░ + 138 ░░░░░░░░░░░░░░░░░ 137 ░░░░░░░░░░░░░░░░░ 138 ░░░░░░░░░░░░░░░░░ + 139 ░░░░░░░░░░░░░░░░░ 138 ░░░░░░░░░░░░░░░░░ 139 ░░░░░░░░░░░░░░░░░ + 139 + ░░░░░░░░░░░░░░░░░ 140 + ░░░░░░░░░░░░░░░░░ + 140 ░░░░░░░░░░░░░░░░░ 140 ░░░░░░░░░░░░░░░░░ 141 ░░░░░░░░░░░░░░░░░ + 141 ░░░░░░░░░░░░░░░░░ 141 ░░░░░░░░░░░░░░░░░ 142 ░░░░░░░░░░░░░░░░░ + 142 + ░░░░░░░░░░░░░░░░░ 143 + ░░░░░░░░░░░░░░░░░ + 143 ░░░░░░░░░░░░░░░░░ 142 ░░░░░░░░░░░░░░░░░ 144 ░░░░░░░░░░░░░░░░░ + 144 ░░░░░░░░░░░░░░░░ 143 ░░░░░░░░░░░░░░░░ 145 ░░░░░░░░░░░░░░░░ + 145 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +──────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/types.ts +──────────────────────────────────────────────────────────────────────────────── +@@@ -5985,8 -6063,7 +6063,8 @@@ export interface NodeLinks + 5985 ░░░░░░░░░░░░░░░░░ 6063 ░░░░░░░░░░░░░░░░░ 6063 ░░░░░░░░░░░░░░░░░ + 5986 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + 5987 ░░░░░░░░░░░░░░░░░ 6065 ░░░░░░░░░░░░░░░░░ 6065 ░░░░░░░░░░░░░░░░░ + 5988 - ░░░░░░░░░░░░░░░░░ 6066 - ░░░░░░░░░░░░░░░░░ + 5988 + ░░░░░░░░░░░░░░░░░ 6066 + ░░░░░░░░░░░░░░░░░ + 6066 + ░░░░░░░░░░░░░░░░░ 6067 + ░░░░░░░░░░░░░░░░░ + 5989 ░░░░░░░░░░░░░░░░░ 6067 ░░░░░░░░░░░░░░░░░ 6068 ░░░░░░░░░░░░░░░░░ + 5990 ░ 6068 ░ 6069 ░ +" +`; + +exports[`syntaxHighlighted merge commit with 3 parents 1`] = ` +" +commit d6d6a4aedfa78794c1b611c13d2ed1d3a66e1798 +Merge: 0dc976df1e 5f16a48236 3eadbf6c96 +Author: Andy Hanson +Date: Thu Sep 1 12:52:42 2016 -0700 + + Merge branch 'goto_definition_super', remote-tracking branch 'origin' into c + +──────────────────────────────────────────────────────────────────────────────── + ■■ src/services/services.ts +──────────────────────────────────────────────────────────────────────────────── +@@@@ -2788,26 -2792,18 -2788,34 +2792,42 @@@@ namespace ts + 2788 ░░░░░░░░░░░ 2792 ░░░░░░░░░░░ 2788 ░░░░░░░░░░░ 2792 ░░░░░░░░░░░ + 2789 ░░░░░ 2793 ░░░░░ 2789 ░░░░░ 2793 ░░░░░ + 2790 2794 2790 2794 + 2791 + ░░░░░░░░░░░ 2791 + ░░░░░░░░░░░ 2795 + ░░░░░░░░░░░ + 2792 + ░░░░░░░░░░░ 2792 + ░░░░░░░░░░░ 2796 + ░░░░░░░░░░░ + 2793 + ░░░░░ 2793 + ░░░░░ 2797 + ░░░░░ + 2794 + 2794 + 2798 + + 2795 - ░░░░░░░░░░░ 2799 - ░░░░░░░░░░░ + 2796 - ░░░░░░░░░░░ 2800 - ░░░░░░░░░░░ + 2799 + ░░░░░░░░░░░ + 2800 + ░░░░░░░░░░░ + 2801 + ░░░░░░░░░░░ + 2795 + ░░░░░ 2802 + ░░░░░ + 2796 + 2803 + + 2795 ░░░░░░░░░░░ 2795 ░░░░░░░░░░░ 2797 ░░░░░░░░░░░ 2804 ░░░░░░░░░░░ + 2796 - ░░░░░░░░░░░ 2805 - ░░░░░░░░░░░ + 2797 - ░░░░░░░░░░░ 2806 - ░░░░░░░░░░░ + 2798 - ░░░░░░░░░ 2807 - ░░░░░░░░░ + 2798 - ░░░░░░░░░░░ 2808 - ░░░░░░░░░░░ + 2799 - ░░░░░░░░░░░ 2799 - ░░░░░░░░░░░ 2809 - ░░░░░░░░░░░ + 2796 + ░░░░░░░░░░░ 2805 + ░░░░░░░░░░░ + 2797 ░░░░░ 2800 ░░░░░ 2800 ░░░░░ 2806 ░░░░░ + 2798 2801 2801 2807 + 2799 ░░░░░░░░░░░ 2802 ░░░░░░░░░░░ 2802 ░░░░░░░░░░░ 2808 ░░░░░░░░░░░ + 2803 - ░░░░░░░░░░░ 2809 - ░░░░░░░░░░░ + 2804 - ░░░░░░░░░░░ 2810 - ░░░░░░░░░░░ + 2805 - ░░░░░░░░░ 2811 - ░░░░░░░░░ + 2803 - ░░░░░░░░░░░ 2812 - ░░░░░░░░░░░ + 2806 - ░░░░░░░░░░░ 2804 - ░░░░░░░░░░░ 2813 - ░░░░░░░░░░░ + 2800 + ░░░░░░░░░░░ 2809 + ░░░░░░░░░░░ + 2801 + ░░░░░ 2810 + ░░░░░ + 2802 + 2811 + + 2803 + ░░░░░░░░░░░ 2812 + ░░░░░░░░░░░ + 2804 + ░░░░░░░░░░░ 2813 + ░░░░░░░░░░░ + 2805 + ░░░░░░░░░░░ 2814 + ░░░░░░░░░░░ + 2806 + ░░░░░ 2815 + ░░░░░ + 2807 + 2816 + + 2808 - ░░░░░░░░░░░ 2817 - ░░░░░░░░░░░ + 2809 - ░░░░░░░░░░░ 2818 - ░░░░░░░░░░░ + 2810 - ░░░░░░░░░░░ 2819 - ░░░░░░░░░░░ + 2817 + ░░░░░░░░░░░ + 2818 + ░░░░░░░░░░░ + 2801 + ░░░░░ 2819 + ░░░░░ + 2802 + 2820 + + 2803 + ░░░░░░░░░░░ 2821 + ░░░░░░░░░░░ + 2804 + ░░░░░░░░░░░ 2822 + ░░░░░░░░░░░ + 2805 + ░░░░░░░░░░░ 2823 + ░░░░░░░░░░░ + 2806 + ░░░░░░░░░░░ 2824 + ░░░░░░░░░░░ + 2807 + ░░░░░░░░░░░ 2825 + ░░░░░░░░░░░ + 2808 + ░░░░░ 2826 + ░░░░░ + 2809 + 2827 + + 2810 + ░░░░░░░░░░░ 2828 + ░░░░░░░░░░░ + 2811 + ░░░░░░░░░░░ 2829 + ░░░░░░░░░░░ + 2812 + ░░░░░░░░░░░ 2830 + ░░░░░░░░░░░ + 2811 ░░░░░ 2807 ░░░░░ 2813 ░░░░░ 2831 ░░░░░ + 2812 2808 2814 2832 + 2813 ░░░░░░░░░░░ 2809 ░░░░░░░░░░░ 2815 ░░░░░░░░░░░ 2833 ░░░░░░░░░░░ + 2814 2810 2816 2834 +" +`; + exports[`syntaxHighlighted multiple inserts and deletes in the same hunk 1`] = ` " commit e5f896655402f8cf2d947c528d45e1d56bbf5717 @@ -1718,6 +2506,118 @@ exports[`unifiedWithInlineChangesHighlighted empty 1`] = ` " `; +exports[`unifiedWithInlineChangesHighlighted merge commit with 2 parents 1`] = ` +" +commit 3f504f4fbc1caf9c10814d48d8897a34f8a34dec +Merge: 2439767601 fbcdb8cf4f +Author: Gabriela Araujo Britto +Date: Thu Dec 21 17:57:42 2023 -0800 + + Merge branch 'main' into gabritto/d2 + +──────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/binder.ts +──────────────────────────────────────────────────────────────────────────────── +@@@ -137,9 -136,9 +137,10 @@@ import + 137 isBlock, + 138 isBlockOrCatchScoped, + 139 IsBlockScopedContainer, + 140 + isBooleanLiteral, + 141 isCallExpression, + 142 isClassStaticBlockDeclaration, + 143 + isConditionalExpression, + 144 isConditionalTypeNode, + 145 IsContainer, + 146 isDeclaration, +──────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/types.ts +──────────────────────────────────────────────────────────────────────────────── +@@@ -5985,8 -6063,7 +6063,8 @@@ export interface NodeLinks + 6063 decoratorSignature?: Signature; // Signature for decorator as i + 6064 spreadIndices?: { first: number | undefined, last: number | undefin + 6065 parameterInitializerContainsUndefined?: boolean; // True if this is + 6066 - fakeScopeForSignatureDeclaration?: boolean; // True if this is a fa + 6066 + contextualReturnType?: Type; // If the node is a return stat + 6067 + fakeScopeForSignatureDeclaration?: "params" | "typeParams"; // If p + 6068 assertionExpressionType?: Type; // Cached type of the expressio + 6069 } +" +`; + +exports[`unifiedWithInlineChangesHighlighted merge commit with 3 parents 1`] = ` +" +commit d6d6a4aedfa78794c1b611c13d2ed1d3a66e1798 +Merge: 0dc976df1e 5f16a48236 3eadbf6c96 +Author: Andy Hanson +Date: Thu Sep 1 12:52:42 2016 -0700 + + Merge branch 'goto_definition_super', remote-tracking branch 'origin' into c + +──────────────────────────────────────────────────────────────────────────────── + ■■ src/services/services.ts +──────────────────────────────────────────────────────────────────────────────── +@@@@ -2788,26 -2792,18 -2788,34 +2792,42 @@@@ namespace ts + 2792 return node && node.parent && node.parent.kind === SyntaxKind.P + 2793 } + 2794 + 2795 + function climbPastPropertyAccess(node: Node) { + 2796 + return isRightSideOfPropertyAccess(node) ? node.parent : node; + 2797 + } + 2798 + + 2799 - function climbPastManyPropertyAccesses(node: Node): Node { + 2800 - return isRightSideOfPropertyAccess(node) ? climbPastManyPropert + 2799 + /** Get \`C\` given \`N\` if \`N\` is in the position \`class C extends N\` + 2800 + function tryGetClassExtendingIdentifier(node: Node): ClassLikeDecla + 2801 + return tryGetClassExtendingExpressionWithTypeArguments(climbPas + 2802 + } + 2803 + + 2804 function isCallExpressionTarget(node: Node): boolean { + 2805 - if (isRightSideOfPropertyAccess(node)) { + 2806 - node = node.parent; + 2807 - } + 2808 - node = climbPastPropertyAccess(node); + 2809 - return node && node.parent && node.parent.kind === SyntaxKind.C + 2805 + return isCallOrNewExpressionTarget(node, SyntaxKind.CallExpress + 2806 } + 2807 + 2808 function isNewExpressionTarget(node: Node): boolean { + 2809 - if (isRightSideOfPropertyAccess(node)) { + 2810 - node = node.parent; + 2811 - } + 2812 - node = climbPastPropertyAccess(node); + 2813 - return node && node.parent && node.parent.kind === SyntaxKind.N + 2809 + return isCallOrNewExpressionTarget(node, SyntaxKind.NewExpressi + 2810 + } + 2811 + + 2812 + function isCallOrNewExpressionTarget(node: Node, kind: SyntaxKind) + 2813 + const target = climbPastPropertyAccess(node); + 2814 + return target && target.parent && target.parent.kind === kind & + 2815 + } + 2816 + + 2817 - /** Get \`C\` given \`N\` if \`N\` is in the position \`class C extends N\` + 2818 - function tryGetClassExtendingIdentifier(node: Node): ClassLikeDecla + 2819 - return tryGetClassExtendingExpressionWithTypeArguments(climbPas + 2817 + function climbPastManyPropertyAccesses(node: Node): Node { + 2818 + return isRightSideOfPropertyAccess(node) ? climbPastManyPropert + 2819 + } + 2820 + + 2821 + /** Returns a CallLikeExpression where \`node\` is the target being i + 2822 + function getAncestorCallLikeExpression(node: Node): CallLikeExpress + 2823 + const target = climbPastManyPropertyAccesses(node); + 2824 + const callLike = target.parent; + 2825 + return callLike && isCallLikeExpression(callLike) && getInvoked + 2826 + } + 2827 + + 2828 + function tryGetSignatureDeclaration(typeChecker: TypeChecker, node: + 2829 + const callLike = getAncestorCallLikeExpression(node); + 2830 + return callLike && typeChecker.getResolvedSignature(callLike).d + 2831 } + 2832 + 2833 function isNameOfModuleDeclaration(node: Node) { + 2834 +" +`; + exports[`unifiedWithInlineChangesHighlighted multiple inserts and deletes in the same hunk 1`] = ` " commit e5f896655402f8cf2d947c528d45e1d56bbf5717 @@ -2094,6 +2994,151 @@ exports[`unifiedWithWrapping empty 1`] = ` " `; +exports[`unifiedWithWrapping merge commit with 2 parents 1`] = ` +" +commit 3f504f4fbc1caf9c10814d48d8897a34f8a34dec +Merge: 2439767601 fbcdb8cf4f +Author: Gabriela Araujo Britto +Date: Thu Dec 21 17:57:42 2023 -0800 + + Merge branch 'main' into gabritto/d2 + +──────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/binder.ts +──────────────────────────────────────────────────────────────────────────────── +@@@ -137,9 -136,9 +137,10 @@@ import + 137 isBlock, + 138 isBlockOrCatchScoped, + 139 IsBlockScopedContainer, + 140 + isBooleanLiteral, + 141 isCallExpression, + 142 isClassStaticBlockDeclaration, + 143 + isConditionalExpression, + 144 isConditionalTypeNode, + 145 IsContainer, + 146 isDeclaration, +──────────────────────────────────────────────────────────────────────────────── + ■■ src/compiler/types.ts +──────────────────────────────────────────────────────────────────────────────── +@@@ -5985,8 -6063,7 +6063,8 @@@ export interface NodeLinks + 6063 decoratorSignature?: Signature; // Signature for decorator as + if invoked by the runtime. + 6064 spreadIndices?: { first: number | undefined, last: number | + undefined }; // Indices of first and last spread elements in array + literal + 6065 parameterInitializerContainsUndefined?: boolean; // True if this is + a parameter declaration whose type annotation contains "undefined". + 6066 - fakeScopeForSignatureDeclaration?: boolean; // True if this is a + fake scope injected into an enclosing declaration chain. + 6066 + contextualReturnType?: Type; // If the node is a return + statement's expression, then this is the contextual return type. + 6067 + fakeScopeForSignatureDeclaration?: "params" | "typeParams"; // If + present, this is a fake scope injected into an enclosing declaration + chain. + 6068 assertionExpressionType?: Type; // Cached type of the + expression of a type assertion + 6069 } +" +`; + +exports[`unifiedWithWrapping merge commit with 3 parents 1`] = ` +" +commit d6d6a4aedfa78794c1b611c13d2ed1d3a66e1798 +Merge: 0dc976df1e 5f16a48236 3eadbf6c96 +Author: Andy Hanson +Date: Thu Sep 1 12:52:42 2016 -0700 + + Merge branch 'goto_definition_super', remote-tracking branch 'origin' into +constructor_references + +──────────────────────────────────────────────────────────────────────────────── + ■■ src/services/services.ts +──────────────────────────────────────────────────────────────────────────────── +@@@@ -2788,26 -2792,18 -2788,34 +2792,42 @@@@ namespace ts + 2792 return node && node.parent && node.parent.kind === + SyntaxKind.PropertyAccessExpression && + (node.parent).name === node; + 2793 } + 2794 + 2795 + function climbPastPropertyAccess(node: Node) { + 2796 + return isRightSideOfPropertyAccess(node) ? node.parent : node; + 2797 + } + 2798 + + 2799 - function climbPastManyPropertyAccesses(node: Node): Node { + 2800 - return isRightSideOfPropertyAccess(node) ? + climbPastManyPropertyAccesses(node.parent) : node; + 2799 + /** Get \`C\` given \`N\` if \`N\` is in the position \`class C extends N\` + or \`class C extends foo.N\` where \`N\` is an identifier. */ + 2800 + function tryGetClassExtendingIdentifier(node: Node): + ClassLikeDeclaration | undefined { + 2801 + return tryGetClassExtendingExpressionWithTypeArguments(climbPas + tPropertyAccess(node).parent); + 2802 + } + 2803 + + 2804 function isCallExpressionTarget(node: Node): boolean { + 2805 - if (isRightSideOfPropertyAccess(node)) { + 2806 - node = node.parent; + 2807 - } + 2808 - node = climbPastPropertyAccess(node); + 2809 - return node && node.parent && node.parent.kind === + SyntaxKind.CallExpression && (node.parent).expression + === node; + 2805 + return isCallOrNewExpressionTarget(node, + SyntaxKind.CallExpression); + 2806 } + 2807 + 2808 function isNewExpressionTarget(node: Node): boolean { + 2809 - if (isRightSideOfPropertyAccess(node)) { + 2810 - node = node.parent; + 2811 - } + 2812 - node = climbPastPropertyAccess(node); + 2813 - return node && node.parent && node.parent.kind === + SyntaxKind.NewExpression && (node.parent).expression + === node; + 2809 + return isCallOrNewExpressionTarget(node, + SyntaxKind.NewExpression); + 2810 + } + 2811 + + 2812 + function isCallOrNewExpressionTarget(node: Node, kind: SyntaxKind) + { + 2813 + const target = climbPastPropertyAccess(node); + 2814 + return target && target.parent && target.parent.kind === kind + && (target.parent).expression === target; + 2815 + } + 2816 + + 2817 - /** Get \`C\` given \`N\` if \`N\` is in the position \`class C extends N\` + or \`class C extends foo.N\` where \`N\` is an identifier. */ + 2818 - function tryGetClassExtendingIdentifier(node: Node): + ClassLikeDeclaration | undefined { + 2819 - return tryGetClassExtendingExpressionWithTypeArguments(climbPas + tPropertyAccess(node).parent); + 2817 + function climbPastManyPropertyAccesses(node: Node): Node { + 2818 + return isRightSideOfPropertyAccess(node) ? + climbPastManyPropertyAccesses(node.parent) : node; + 2819 + } + 2820 + + 2821 + /** Returns a CallLikeExpression where \`node\` is the target being + invoked. */ + 2822 + function getAncestorCallLikeExpression(node: Node): + CallLikeExpression | undefined { + 2823 + const target = climbPastManyPropertyAccesses(node); + 2824 + const callLike = target.parent; + 2825 + return callLike && isCallLikeExpression(callLike) && + getInvokedExpression(callLike) === target && callLike; + 2826 + } + 2827 + + 2828 + function tryGetSignatureDeclaration(typeChecker: TypeChecker, node: + Node): SignatureDeclaration | undefined { + 2829 + const callLike = getAncestorCallLikeExpression(node); + 2830 + return callLike && + typeChecker.getResolvedSignature(callLike).declaration; + 2831 } + 2832 + 2833 function isNameOfModuleDeclaration(node: Node) { + 2834 +" +`; + exports[`unifiedWithWrapping multiple inserts and deletes in the same hunk 1`] = ` " commit e5f896655402f8cf2d947c528d45e1d56bbf5717 diff --git a/src/index.test.ts b/src/index.test.ts index 10516f1..301637a 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -494,5 +494,124 @@ index 095ee29..439621e 100644 This is file2`) ).toMatchSnapshot(); }); + + test('merge commit with 2 parents', async function () { + // Source: the TypeScript repo + expect( + await transform(` +commit 3f504f4fbc1caf9c10814d48d8897a34f8a34dec +Merge: 2439767601 fbcdb8cf4f +Author: Gabriela Araujo Britto +Date: Thu Dec 21 17:57:42 2023 -0800 + + Merge branch 'main' into gabritto/d2 + +diff --cc src/compiler/binder.ts +index b2f0d9f384,6ea9b82695..c638984e3b +--- a/src/compiler/binder.ts ++++ b/src/compiler/binder.ts +@@@ -137,9 -136,9 +137,10 @@@ import + isBlock, + isBlockOrCatchScoped, + IsBlockScopedContainer, ++ isBooleanLiteral, + isCallExpression, + isClassStaticBlockDeclaration, + + isConditionalExpression, + isConditionalTypeNode, + IsContainer, + isDeclaration, +diff --cc src/compiler/types.ts +index 2e204671f7,e56bba5ab4..ab6229d1b6 +--- a/src/compiler/types.ts ++++ b/src/compiler/types.ts +@@@ -5985,8 -6063,7 +6063,8 @@@ export interface NodeLinks + decoratorSignature?: Signature; // Signature for decorator as if invoked by the runtime. + spreadIndices?: { first: number | undefined, last: number | undefined }; // Indices of first and last spread elements in array literal + parameterInitializerContainsUndefined?: boolean; // True if this is a parameter declaration whose type annotation contains "undefined". +- fakeScopeForSignatureDeclaration?: boolean; // True if this is a fake scope injected into an enclosing declaration chain. + + contextualReturnType?: Type; // If the node is a return statement's expression, then this is the contextual return type. ++ fakeScopeForSignatureDeclaration?: "params" | "typeParams"; // If present, this is a fake scope injected into an enclosing declaration chain. + assertionExpressionType?: Type; // Cached type of the expression of a type assertion + }`) + ).toMatchSnapshot(); + }); + + test('merge commit with 3 parents', async function () { + // Source: the TypeScript repo + expect( + await transform(` +commit d6d6a4aedfa78794c1b611c13d2ed1d3a66e1798 +Merge: 0dc976df1e 5f16a48236 3eadbf6c96 +Author: Andy Hanson +Date: Thu Sep 1 12:52:42 2016 -0700 + + Merge branch 'goto_definition_super', remote-tracking branch 'origin' into constructor_references + +diff --cc src/services/services.ts +index b95feb9207,c19eb487d7,83a2192659..7e9a356e73 +--- a/src/services/services.ts ++++ b/src/services/services.ts +@@@@ -2788,26 -2792,18 -2788,34 +2792,42 @@@@ namespace ts + return node && node.parent && node.parent.kind === SyntaxKind.PropertyAccessExpression && (node.parent).name === node; + } + + + function climbPastPropertyAccess(node: Node) { + + return isRightSideOfPropertyAccess(node) ? node.parent : node; + + } + + + - function climbPastManyPropertyAccesses(node: Node): Node { + - return isRightSideOfPropertyAccess(node) ? climbPastManyPropertyAccesses(node.parent) : node; ++++ /** Get \`C\` given \`N\` if \`N\` is in the position \`class C extends N\` or \`class C extends foo.N\` where \`N\` is an identifier. */ ++++ function tryGetClassExtendingIdentifier(node: Node): ClassLikeDeclaration | undefined { ++++ return tryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).parent); +++ } +++ + function isCallExpressionTarget(node: Node): boolean { + - if (isRightSideOfPropertyAccess(node)) { + - node = node.parent; + - } + - node = climbPastPropertyAccess(node); + -- return node && node.parent && node.parent.kind === SyntaxKind.CallExpression && (node.parent).expression === node; + ++ return isCallOrNewExpressionTarget(node, SyntaxKind.CallExpression); + } + + function isNewExpressionTarget(node: Node): boolean { + - if (isRightSideOfPropertyAccess(node)) { + - node = node.parent; + - } + - node = climbPastPropertyAccess(node); + -- return node && node.parent && node.parent.kind === SyntaxKind.NewExpression && (node.parent).expression === node; + ++ return isCallOrNewExpressionTarget(node, SyntaxKind.NewExpression); + ++ } + ++ + ++ function isCallOrNewExpressionTarget(node: Node, kind: SyntaxKind) { + ++ const target = climbPastPropertyAccess(node); + ++ return target && target.parent && target.parent.kind === kind && (target.parent).expression === target; + ++ } + ++ +- /** Get \`C\` given \`N\` if \`N\` is in the position \`class C extends N\` or \`class C extends foo.N\` where \`N\` is an identifier. */ +- function tryGetClassExtendingIdentifier(node: Node): ClassLikeDeclaration | undefined { +- return tryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).parent); ++++ function climbPastManyPropertyAccesses(node: Node): Node { ++++ return isRightSideOfPropertyAccess(node) ? climbPastManyPropertyAccesses(node.parent) : node; +++ } +++ +++ /** Returns a CallLikeExpression where \`node\` is the target being invoked. */ +++ function getAncestorCallLikeExpression(node: Node): CallLikeExpression | undefined { +++ const target = climbPastManyPropertyAccesses(node); +++ const callLike = target.parent; +++ return callLike && isCallLikeExpression(callLike) && getInvokedExpression(callLike) === target && callLike; +++ } +++ +++ function tryGetSignatureDeclaration(typeChecker: TypeChecker, node: Node): SignatureDeclaration | undefined { +++ const callLike = getAncestorCallLikeExpression(node); +++ return callLike && typeChecker.getResolvedSignature(callLike).declaration; + } + + function isNameOfModuleDeclaration(node: Node) { +`) + ).toMatchSnapshot(); + }); }); } From 4e342c4d27e58cc8b3204c7c62553f917c84ab36 Mon Sep 17 00:00:00 2001 From: Shrey Banga Date: Sun, 30 Jun 2024 23:00:17 -0400 Subject: [PATCH 9/9] Check-off todo --- todo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todo.md b/todo.md index 87a2d0c..5331022 100644 --- a/todo.md +++ b/todo.md @@ -35,7 +35,7 @@ - [x] Tests for syntax highlighting - [x] Load shiki languages on-demand (by switching to shikiji) - [ ] Benchmark for syntax highlighting -- [ ] Display 3-way merge diffs (e.g. during merge conflicts) +- [x] Display 3-way merge diffs (e.g. during merge conflicts) - [ ] See why `less` occasionally goes into search mode - [ ] Test on linux - [ ] Support custom themes