Skip to content
Open
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
68 changes: 65 additions & 3 deletions src/BlazorTextDiff/TextDiff.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@
}

<CascadingValue Value="@CollapseContent" Name="@nameof(CollapseContent)">
<div class="diff-panes" style="@(isCollapsed ? $"max-height: {MaxHeight}px; overflow: auto;" : "max-height: auto; overflow: auto;")">
<div class="diff-panes"
style="@(isCollapsed ? $"max-height: {MaxHeight}px; overflow: auto;" : "max-height: auto; overflow: auto;")">
@if (diff is not null)
{
<div class="diff-pane">
<TextDiffPane Model="diff.OldText" PanePosition="@PanePosition.Left"></TextDiffPane>
<TextDiffPane Model="diff.OldText"
PanePosition="@PanePosition.Left"
VisibleLines="@visibleLines"
OnShowHiddenLines="ShowHiddenLines" />
</div>
<div class="diff-pane">
<TextDiffPane Model="diff.NewText" PanePosition="@PanePosition.Right"></TextDiffPane>
<TextDiffPane Model="diff.NewText"
PanePosition="@PanePosition.Right"
VisibleLines="@visibleLines"
OnShowHiddenLines="ShowHiddenLines" />
</div>
}
</div>
Expand Down Expand Up @@ -50,6 +57,16 @@
[Parameter] public int MaxHeight { get; set; } = 300;
[Parameter] public bool CollapseContent { get; set; }

/// <summary>
/// When true, unchanged lines will be hidden, showing only changed lines and some context.
/// </summary>
[Parameter] public bool HideUnchangedLines { get; set; } = false;

/// <summary>
/// The number of unchanged lines to show around each change when <see cref="HideUnchangedLines"/> is true.
/// </summary>
[Parameter] public int ContextLines { get; set; } = 3;

/// <summary>
/// The text before any changes.
/// </summary>
Expand Down Expand Up @@ -77,6 +94,7 @@

private SideBySideDiffModel? diff;
private bool isCollapsed;
private bool[]? visibleLines;

/// <summary>
/// When parameters set update the component.
Expand Down Expand Up @@ -108,6 +126,36 @@
}
}

/// <summary> Creates a visibility state array for each line based on diff changes </summary>
private void CalculateVisibility()
{
if (diff == null || !HideUnchangedLines)
{
visibleLines = null;
return;
}
Comment on lines +129 to +136
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CalculateVisibility() is never invoked, so setting HideUnchangedLines=true will not change what TextDiffPane renders (and visibleLines will remain null). Call CalculateVisibility() at the end of OnParametersSet() (after computing/post-processing diff), and ensure visibleLines is cleared when diff isn't built.

Copilot uses AI. Check for mistakes.

var lineCount = diff.OldText.Lines.Count;
visibleLines = new bool[lineCount];

for (var i = 0; i < lineCount; i++)
{
var oldLineUnchanged = diff.OldText.Lines[i].Type == ChangeType.Unchanged;
var newLineUnchanged = diff.NewText.Lines[i].Type == ChangeType.Unchanged;

if (oldLineUnchanged && newLineUnchanged)
continue;

var contextLinesStart = Math.Max(0, i - ContextLines);
var contextLinesEnd = Math.Min(lineCount - 1, i + ContextLines);

Comment on lines +149 to +151
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContextLines is a public parameter but negative values will produce an empty/invalid context range (and can end up hiding even the changed line). Clamp ContextLines to 0+ (or throw an ArgumentOutOfRangeException) before computing contextLinesStart/contextLinesEnd.

Copilot uses AI. Check for mistakes.
for (var j = contextLinesStart; j <= contextLinesEnd; j++)
{
visibleLines[j] = true;
}
}
}

/// <summary>
/// For lines whose only sub-piece changes are whitespace characters,
/// downgrade them to Unchanged so they don't show as modified.
Expand Down Expand Up @@ -145,4 +193,18 @@
{
isCollapsed = !isCollapsed;
}

private void ShowHiddenLines((int Start, int Count) range)
{
if (visibleLines == null)
return;

var rangeEnd = Math.Min(range.Start + range.Count, visibleLines.Length);
for (var i = range.Start; i < rangeEnd; i++)
{
visibleLines[i] = true;
}

StateHasChanged();
}
}
32 changes: 31 additions & 1 deletion src/BlazorTextDiff/TextDiffPane.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,36 @@
<table class="diff">
@if (Model?.Lines is not null)
{
@foreach (var diffLine in Model.Lines)
@for (var i = 0; i < Model.Lines.Count; i++)
{
var diffLine = Model.Lines[i];
if (VisibleLines is not null && !VisibleLines[i])
{
// If we are at the start of a hidden block, show a placeholder
if (i == 0 || VisibleLines[i - 1])
{
var hiddenCount = 0;
var lineIdx = i;

// count next hidden lines
for (var j = lineIdx; j < Model.Lines.Count && !VisibleLines[j]; j++)
{
hiddenCount++;
}

<tr class="diff-hidden-summary">
<td class="line-number">...</td>
<td class="line">
<button type="button" class="show-hidden-button hidden-count"
@onclick="() => OnShowHiddenLines.InvokeAsync((lineIdx, hiddenCount))"
title="Click here to show hidden lines">
<span>@hiddenCount exact lines hidden (show)</span>
</button>
</td>
</tr>
}
continue;
}
<tr>
<td class="line-number @diffLine.Type.ToString().ToLower()">
@((MarkupString)(diffLine.Position?.ToString() ?? "&nbsp;"))
Expand All @@ -24,4 +52,6 @@
@code {
[Parameter, EditorRequired] public DiffPaneModel Model { get; set; } = default!;
[Parameter] public PanePosition PanePosition { get; set; }
[Parameter] public bool[]? VisibleLines { get; set; }
[Parameter] public EventCallback<(int Start, int Count)> OnShowHiddenLines { get; set; }
}
40 changes: 40 additions & 0 deletions src/BlazorTextDiff/wwwroot/css/BlazorDiff.css
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,46 @@ table.diff tr:hover td.line {
background-color: color-mix(in srgb, var(--diff-border-primary) 30%, transparent);
}

.diff-hidden-summary td {
background-color: var(--diff-bg-tertiary) !important;
color: var(--diff-text-muted);
font-size: 12px;
padding: 2px 12px !important;
border-top: 1px solid var(--diff-border-primary);
border-bottom: 1px solid var(--diff-border-primary);
}

.diff-hidden-summary .line-number {
text-align: center !important;
border-right: 1px solid var(--diff-border-primary);
}

.diff-hidden-summary .hidden-count {
font-style: italic;
font-weight: 500;
}

.show-hidden-button {
background: none;
border: none;
color: var(--diff-text-muted);
cursor: pointer;
padding: 0;
font-family: inherit;
font-size: inherit;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s, background-color 0.2s;
}

.show-hidden-button:hover {
color: var(--diff-text-accent);
background-color: var(--diff-bg-secondary);
}

/* Responsive improvements */
@media (max-width: 768px) {
.diff-container .diff-panes {
Expand Down
Loading