From 4d97fa6829746b6be71956d976c752b078906a7d Mon Sep 17 00:00:00 2001 From: John Traas Date: Wed, 6 May 2026 15:26:39 +0200 Subject: [PATCH 01/15] feat(code-diff): add SearchScope type and pickDefaultScope helper --- src/components/code-diff/search-utils.spec.ts | 21 ++++++++++++++++++ src/components/code-diff/search-utils.ts | 22 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/components/code-diff/search-utils.spec.ts b/src/components/code-diff/search-utils.spec.ts index fcc63c9a80..da5f9f9669 100644 --- a/src/components/code-diff/search-utils.spec.ts +++ b/src/components/code-diff/search-utils.spec.ts @@ -3,6 +3,7 @@ import { escapeRegex, buildSearchRegex, navigateMatchIndex, + pickDefaultScope, } from './search-utils'; describe('escapeRegex', () => { @@ -66,3 +67,23 @@ describe('navigateMatchIndex', () => { expect(navigateMatchIndex(0, -1, 5)).toBe(4); }); }); + +describe('pickDefaultScope', () => { + it('returns "removed" when there are deletions', () => { + expect(pickDefaultScope({ additions: 0, deletions: 3 })).toBe( + 'removed' + ); + }); + + it('returns "removed" when both additions and deletions exist', () => { + expect(pickDefaultScope({ additions: 5, deletions: 2 })).toBe( + 'removed' + ); + }); + + it('returns "added" when only additions exist', () => { + expect(pickDefaultScope({ additions: 4, deletions: 0 })).toBe( + 'added' + ); + }); +}); diff --git a/src/components/code-diff/search-utils.ts b/src/components/code-diff/search-utils.ts index 129f6e5e35..17c73abbd6 100644 --- a/src/components/code-diff/search-utils.ts +++ b/src/components/code-diff/search-utils.ts @@ -2,6 +2,11 @@ * Pure utility functions for search-within-diff functionality. */ +/** + * The line types that the in-diff search operates on. + */ +export type SearchScope = 'removed' | 'added' | 'changed'; + /** * Escape special regex characters in a search term so it can * be used as a literal pattern in a RegExp constructor. @@ -48,3 +53,20 @@ export function navigateMatchIndex( return (currentIndex + direction + total) % total; } + +/** + * Pick the default `SearchScope` to use when the search panel opens. + * + * Falls back to `'added'` when there are no removed lines, so the + * panel never opens with a scope that has zero matches. + */ +export function pickDefaultScope(stats: { + additions: number; + deletions: number; +}): SearchScope { + if (stats.deletions > 0) { + return 'removed'; + } + + return 'added'; +} From 1a3715b896ebd8904734df491d30dcdad31d243d Mon Sep 17 00:00:00 2001 From: John Traas Date: Wed, 6 May 2026 15:31:03 +0200 Subject: [PATCH 02/15] feat(code-diff): add lineMatchesScope helper --- src/components/code-diff/search-utils.spec.ts | 45 +++++++++++++++++++ src/components/code-diff/search-utils.ts | 19 ++++++++ 2 files changed, 64 insertions(+) diff --git a/src/components/code-diff/search-utils.spec.ts b/src/components/code-diff/search-utils.spec.ts index da5f9f9669..bc7d1d0be1 100644 --- a/src/components/code-diff/search-utils.spec.ts +++ b/src/components/code-diff/search-utils.spec.ts @@ -4,6 +4,7 @@ import { buildSearchRegex, navigateMatchIndex, pickDefaultScope, + lineMatchesScope, } from './search-utils'; describe('escapeRegex', () => { @@ -87,3 +88,47 @@ describe('pickDefaultScope', () => { ); }); }); + +describe('lineMatchesScope', () => { + describe('scope "removed"', () => { + it('matches removed lines', () => { + expect(lineMatchesScope('removed', 'removed')).toBe(true); + }); + + it('does not match added lines', () => { + expect(lineMatchesScope('added', 'removed')).toBe(false); + }); + + it('does not match context lines', () => { + expect(lineMatchesScope('context', 'removed')).toBe(false); + }); + }); + + describe('scope "added"', () => { + it('matches added lines', () => { + expect(lineMatchesScope('added', 'added')).toBe(true); + }); + + it('does not match removed lines', () => { + expect(lineMatchesScope('removed', 'added')).toBe(false); + }); + + it('does not match context lines', () => { + expect(lineMatchesScope('context', 'added')).toBe(false); + }); + }); + + describe('scope "changed"', () => { + it('matches removed lines', () => { + expect(lineMatchesScope('removed', 'changed')).toBe(true); + }); + + it('matches added lines', () => { + expect(lineMatchesScope('added', 'changed')).toBe(true); + }); + + it('does not match context lines', () => { + expect(lineMatchesScope('context', 'changed')).toBe(false); + }); + }); +}); diff --git a/src/components/code-diff/search-utils.ts b/src/components/code-diff/search-utils.ts index 17c73abbd6..4e3ceeb0f8 100644 --- a/src/components/code-diff/search-utils.ts +++ b/src/components/code-diff/search-utils.ts @@ -70,3 +70,22 @@ export function pickDefaultScope(stats: { return 'added'; } + +/** + * Whether a line of the given type participates in the active + * `SearchScope`. Context lines are never included. + */ +export function lineMatchesScope( + lineType: 'added' | 'removed' | 'context', + scope: SearchScope +): boolean { + if (lineType === 'context') { + return false; + } + + if (scope === 'changed') { + return true; + } + + return lineType === scope; +} From face07870fa21c6ba38796e5d02d1716b2574ddd Mon Sep 17 00:00:00 2001 From: John Traas Date: Wed, 6 May 2026 15:33:42 +0200 Subject: [PATCH 03/15] refactor(code-diff): tighten lineMatchesScope param type to DiffLine['type'] --- src/components/code-diff/search-utils.spec.ts | 6 +++--- src/components/code-diff/search-utils.ts | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/code-diff/search-utils.spec.ts b/src/components/code-diff/search-utils.spec.ts index bc7d1d0be1..e28c2b73d0 100644 --- a/src/components/code-diff/search-utils.spec.ts +++ b/src/components/code-diff/search-utils.spec.ts @@ -90,7 +90,7 @@ describe('pickDefaultScope', () => { }); describe('lineMatchesScope', () => { - describe('scope "removed"', () => { + describe('scope: removed', () => { it('matches removed lines', () => { expect(lineMatchesScope('removed', 'removed')).toBe(true); }); @@ -104,7 +104,7 @@ describe('lineMatchesScope', () => { }); }); - describe('scope "added"', () => { + describe('scope: added', () => { it('matches added lines', () => { expect(lineMatchesScope('added', 'added')).toBe(true); }); @@ -118,7 +118,7 @@ describe('lineMatchesScope', () => { }); }); - describe('scope "changed"', () => { + describe('scope: changed', () => { it('matches removed lines', () => { expect(lineMatchesScope('removed', 'changed')).toBe(true); }); diff --git a/src/components/code-diff/search-utils.ts b/src/components/code-diff/search-utils.ts index 4e3ceeb0f8..7108944308 100644 --- a/src/components/code-diff/search-utils.ts +++ b/src/components/code-diff/search-utils.ts @@ -2,6 +2,8 @@ * Pure utility functions for search-within-diff functionality. */ +import type { DiffLine } from './types'; + /** * The line types that the in-diff search operates on. */ @@ -76,7 +78,7 @@ export function pickDefaultScope(stats: { * `SearchScope`. Context lines are never included. */ export function lineMatchesScope( - lineType: 'added' | 'removed' | 'context', + lineType: DiffLine['type'], scope: SearchScope ): boolean { if (lineType === 'context') { From 8c906111fa453fd10845bd8a9049aec30f65b560 Mon Sep 17 00:00:00 2001 From: John Traas Date: Wed, 6 May 2026 15:38:02 +0200 Subject: [PATCH 04/15] feat(code-diff): add searchScope state and reset on toggle --- src/components/code-diff/code-diff.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/code-diff/code-diff.tsx b/src/components/code-diff/code-diff.tsx index e32de84496..f4866b32c0 100644 --- a/src/components/code-diff/code-diff.tsx +++ b/src/components/code-diff/code-diff.tsx @@ -12,7 +12,13 @@ import { import { ActionBarItem } from '../action-bar/action-bar.types'; import { buildSplitLines, computeDiff, normalizeForDiff } from './diff-engine'; import { tokenize, SyntaxToken } from './syntax-highlighter'; -import { buildSearchRegex, navigateMatchIndex } from './search-utils'; +import { + buildSearchRegex, + navigateMatchIndex, + pickDefaultScope, + lineMatchesScope, + SearchScope, +} from './search-utils'; import { extractRemovedContent, extractRemovedContentFromSplit, @@ -140,6 +146,9 @@ export class CodeDiff { @State() private currentMatchIndex: number = 0; + @State() + private searchScope: SearchScope = 'removed'; + private focusedRowIndex: number = -1; private normalizedOldText: string = ''; @@ -457,9 +466,12 @@ export class CodeDiff { private toggleSearch() { this.searchVisible = !this.searchVisible; - if (!this.searchVisible) { + if (this.searchVisible) { + this.searchScope = pickDefaultScope(this.diffResult); + } else { this.searchTerm = ''; this.currentMatchIndex = 0; + this.searchScope = 'removed'; } } From e6fae08eb4f8ba7fd60abc36d5ee340edfce7504 Mon Sep 17 00:00:00 2001 From: John Traas Date: Wed, 6 May 2026 15:50:06 +0200 Subject: [PATCH 05/15] feat(code-diff): make search highlighting scope-aware --- src/components/code-diff/code-diff.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/code-diff/code-diff.tsx b/src/components/code-diff/code-diff.tsx index f4866b32c0..4877b81579 100644 --- a/src/components/code-diff/code-diff.tsx +++ b/src/components/code-diff/code-diff.tsx @@ -165,10 +165,11 @@ export class CodeDiff { private totalSearchMatches: number = 0; /** - * Whether the current render is inside a removed line, - * so search highlighting knows when to activate. + * Whether the current line being rendered participates in the + * active search scope, so search highlighting knows when to + * activate. */ - private isRenderingRemovedLine: boolean = false; + private isRenderingSearchableLine: boolean = false; /** * Cached search regex for the current render pass. @@ -781,8 +782,9 @@ export class CodeDiff { } private renderContent(line: DiffLine) { - this.isRenderingRemovedLine = - line.type === 'removed' && this.searchTerm.length > 0; + this.isRenderingSearchableLine = + this.searchTerm.length > 0 && + lineMatchesScope(line.type, this.searchScope); if (!line.segments || line.segments.length === 0) { return this.renderSyntaxTokens(line.content); @@ -827,7 +829,7 @@ export class CodeDiff { } private renderSearchableText(text: string): any { - if (!this.isRenderingRemovedLine || !this.activeSearchRegex) { + if (!this.isRenderingSearchableLine || !this.activeSearchRegex) { return text; } From eb803a7c9c90b9b01e62c8b27a6a034e7c797902 Mon Sep 17 00:00:00 2001 From: John Traas Date: Wed, 6 May 2026 15:52:22 +0200 Subject: [PATCH 06/15] docs(code-diff): fix searchMatchCounter comment for scope-awareness --- src/components/code-diff/code-diff.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/code-diff/code-diff.tsx b/src/components/code-diff/code-diff.tsx index 4877b81579..ca05dcef5c 100644 --- a/src/components/code-diff/code-diff.tsx +++ b/src/components/code-diff/code-diff.tsx @@ -154,7 +154,7 @@ export class CodeDiff { /** * Render-time counter that increments for each search match - * found while rendering removed lines. Used to determine which + * found while rendering in-scope lines. Used to determine which * match is the "current" one for navigation highlighting. */ private searchMatchCounter: number = 0; From 93e426fe6bbca4528d1234fb876a061c24f703b6 Mon Sep 17 00:00:00 2001 From: John Traas Date: Wed, 6 May 2026 16:04:50 +0200 Subject: [PATCH 07/15] feat(code-diff): add scope picker to search bar --- src/components/code-diff/code-diff.tsx | 39 +++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/components/code-diff/code-diff.tsx b/src/components/code-diff/code-diff.tsx index ca05dcef5c..67b5efa5a5 100644 --- a/src/components/code-diff/code-diff.tsx +++ b/src/components/code-diff/code-diff.tsx @@ -10,6 +10,7 @@ import { SplitDiffLine, } from './types'; import { ActionBarItem } from '../action-bar/action-bar.types'; +import { Button } from '../button/button.types'; import { buildSplitLines, computeDiff, normalizeForDiff } from './diff-engine'; import { tokenize, SyntaxToken } from './syntax-highlighter'; import { @@ -386,7 +387,7 @@ export class CodeDiff { )} {hasDiff && this.renderCopyButton()} - {deletions > 0 && this.renderSearchToggle()} + {hasDiff && this.renderSearchToggle()} ); @@ -447,6 +448,14 @@ export class CodeDiff { return (