Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/components/code-diff/code-diff.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,42 @@
display: flex;
align-items: center;
gap: 0.25rem;
// Pushes the toolbar to the right edge regardless of whether the labels
// block is rendered (it isn't in split mode).
margin-left: auto;
}

// ─── Split-mode column labels ────────────────────────────────────────
// Renders below the actions toolbar, mirroring the 4-cell layout of
// `.diff-line--split` so the `oldHeading` / `newHeading` text sits
// directly above the columns they describe.
.diff-header__column-labels {
display: flex;
align-items: stretch;
border-bottom: 1px solid var(--diff-border-color);
@include mixins.font-family(sans-serif);
font-size: 0.875rem;
}

.diff-header__column-gutter {
width: var(--limel-line-number-min-width);
flex-shrink: 0;
background: var(--diff-gutter-bg);
}

.diff-header__column-label {
flex: 1;
min-width: 0;
padding: 0.375rem 0.75rem;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

&--old {
// Divider matches the split divider between the diff columns below.
border-right: 1px solid var(--diff-split-divider-color);
}
}

.search-toggle--active {
Expand Down Expand Up @@ -158,6 +194,10 @@
flex-grow: 1;
}

limel-button-group {
flex-shrink: 0;
}

&__count {
color: var(--diff-collapsed-text-color);
white-space: nowrap;
Expand Down
106 changes: 89 additions & 17 deletions src/components/code-diff/code-diff.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@
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 { buildSearchRegex, navigateMatchIndex } from './search-utils';
import {
buildSearchRegex,
navigateMatchIndex,
pickDefaultScope,
lineMatchesScope,
SearchScope,
} from './search-utils';
import {
extractRemovedContent,
extractRemovedContentFromSplit,
Expand Down Expand Up @@ -140,12 +147,15 @@
@State()
private currentMatchIndex: number = 0;

@State()
private searchScope: SearchScope = 'removed';

private focusedRowIndex: number = -1;
private normalizedOldText: string = '';

/**
* 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;
Expand All @@ -156,10 +166,11 @@
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.
Expand Down Expand Up @@ -359,27 +370,45 @@

const { additions, deletions } = this.diffResult;
const hasDiff = additions > 0 || deletions > 0;
const isSplit = this.layout === 'split';

return (
return [
<div class="diff-header">
<div class="diff-header__labels">
<span class="diff-header__old">{oldHeading}</span>
<span class="diff-header__new">{newHeading}</span>
</div>
{!isSplit && (
<div class="diff-header__labels">
<span class="diff-header__old">{oldHeading}</span>
<span class="diff-header__new">{newHeading}</span>
</div>
)}
<div class="diff-header__actions">
<div class="diff-header__stats">
{additions > 0 && (
<span class="stat stat--added">+{additions}</span>
)}
{deletions > 0 && (
<span class="stat stat--removed">-{deletions}</span>
)}
</div>
{hasDiff && this.renderCopyButton()}
{deletions > 0 && this.renderSearchToggle()}
{hasDiff && this.renderSearchToggle()}
</div>
</div>
);
</div>,

Check warning on line 395 in src/components/code-diff/code-diff.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Missing "key" prop for element in array

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZ4W5btlZzDF3vdK9beK&open=AZ4W5btlZzDF3vdK9beK&pullRequest=4061
isSplit && (
// In split mode the labels live in their own row below the
// actions toolbar, mirroring the 4-cell layout of `.diff-line--split`
// so each label sits above the column it describes.
<div class="diff-header__column-labels" role="row">

Check warning on line 400 in src/components/code-diff/code-diff.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use <tr> instead of the "row" role to ensure accessibility across all devices.

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZ4W5btlZzDF3vdK9beL&open=AZ4W5btlZzDF3vdK9beL&pullRequest=4061
<span class="diff-header__column-gutter" />
<span class="diff-header__column-label diff-header__column-label--old">
{oldHeading}
</span>
<span class="diff-header__column-gutter" />
<span class="diff-header__column-label diff-header__column-label--new">
{newHeading}
</span>
</div>
),
];
}

private renderCopyButton() {
Expand Down Expand Up @@ -437,6 +466,14 @@

return (
<div class="search-bar">
<limel-button-group
class="search-bar__scope"
aria-label={this.getTranslation('code-diff.search-scope')}
value={this.getScopeButtons()}
onChange={(e: CustomEvent<Button>) =>
this.onScopeChange(e.detail)
}
/>
<limel-input-field
class="search-bar__input"
type="search"
Expand All @@ -457,9 +494,12 @@

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';
}
}

Expand Down Expand Up @@ -515,6 +555,37 @@
];
}

private getScopeButtons(): Button[] {
return [
{
id: 'removed',
title: this.getTranslation('code-diff.search-scope-removed'),
icon: 'minus',
selected: this.searchScope === 'removed',
},
{
id: 'added',
title: this.getTranslation('code-diff.search-scope-added'),
icon: 'plus_math',
selected: this.searchScope === 'added',
},
{
id: 'changed',
title: this.getTranslation('code-diff.search-scope-changed'),
icon: 'compare_arrows',
selected: this.searchScope === 'changed',
},
];
}

private onScopeChange(button: Button) {
const id = button.id;
if (id === 'removed' || id === 'added' || id === 'changed') {
this.searchScope = id;
this.currentMatchIndex = 0;
}
}

private onSearchAction(event: CustomEvent<ActionBarItem>) {
const { value } = event.detail;
if (value === 'prev') {
Expand Down Expand Up @@ -769,8 +840,9 @@
}

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);
Expand Down Expand Up @@ -815,7 +887,7 @@
}

private renderSearchableText(text: string): any {
if (!this.isRenderingRemovedLine || !this.activeSearchRegex) {
if (!this.isRenderingSearchableLine || !this.activeSearchRegex) {
return text;
}

Expand Down
64 changes: 64 additions & 0 deletions src/components/code-diff/search-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
escapeRegex,
buildSearchRegex,
navigateMatchIndex,
pickDefaultScope,
lineMatchesScope,
} from './search-utils';

describe('escapeRegex', () => {
Expand Down Expand Up @@ -66,3 +68,65 @@ 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');
});
});

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);
});
});
});
51 changes: 51 additions & 0 deletions src/components/code-diff/search-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
* Pure utility functions for search-within-diff functionality.
*/

import type { DiffLine } from './types';

/**
* 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.
Expand Down Expand Up @@ -48,3 +55,47 @@ 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.
*
* @param stats - the current diff statistics
* @param stats.additions - number of added lines in the diff
* @param stats.deletions - number of removed lines in the diff
* @returns the scope that should be active when the panel opens
*/
export function pickDefaultScope(stats: {
additions: number;
deletions: number;
}): SearchScope {
if (stats.deletions > 0) {
return 'removed';
}

return 'added';
}

/**
* Whether a line of the given type participates in the active
* `SearchScope`. Context lines are never included.
*
* @param lineType - the type of the diff line being considered
* @param scope - the active search scope
* @returns true when the line participates in the scope, false otherwise
*/
export function lineMatchesScope(
lineType: DiffLine['type'],
scope: SearchScope
): boolean {
if (lineType === 'context') {
return false;
}

if (scope === 'changed') {
return true;
}

return lineType === scope;
}
6 changes: 5 additions & 1 deletion src/translations/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ Du kan fortsætte med at blokere billeder (e-mailen kan se ufuldstændig ud) ell
'code-diff.copied': 'Kopieret!',
'code-diff.copied-to-clipboard': 'Kopieret til udklipsholder',
'code-diff.copy-change': 'Kopiér gammel version af denne ændring',
'code-diff.search': 'Søg i fjernede linjer',
'code-diff.search': 'Søg',
'code-diff.search-scope': 'Søgeområde',
'code-diff.search-scope-removed': 'Fjernede linjer',
'code-diff.search-scope-added': 'Tilføjede linjer',
'code-diff.search-scope-changed': 'Ændrede linjer',
'code-diff.previous-match': 'Forrige match',
'code-diff.next-match': 'Næste match',
'code-diff.close-search': 'Luk søgning',
Expand Down
Loading
Loading