Skip to content

Commit 8cc056d

Browse files
committed
enhance: supports searching/filtering unstaged changes (#960)
Signed-off-by: leo <[email protected]>
1 parent 35abeae commit 8cc056d

File tree

2 files changed

+165
-81
lines changed

2 files changed

+165
-81
lines changed

src/ViewModels/WorkingCopy.cs

Lines changed: 130 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,34 @@ public bool UseAmend
9999
}
100100
}
101101

102+
public string UnstagedFilter
103+
{
104+
get => _unstagedFilter;
105+
set
106+
{
107+
if (SetProperty(ref _unstagedFilter, value))
108+
{
109+
if (_isLoadingData)
110+
return;
111+
112+
VisibleUnstaged = GetVisibleUnstagedChanges();
113+
SelectedUnstaged = [];
114+
}
115+
}
116+
}
117+
102118
public List<Models.Change> Unstaged
103119
{
104120
get => _unstaged;
105121
private set => SetProperty(ref _unstaged, value);
106122
}
107123

124+
public List<Models.Change> VisibleUnstaged
125+
{
126+
get => _visibleUnstaged;
127+
private set => SetProperty(ref _visibleUnstaged, value);
128+
}
129+
108130
public List<Models.Change> Staged
109131
{
110132
get => _staged;
@@ -191,8 +213,9 @@ public void Cleanup()
191213
_selectedStaged.Clear();
192214
OnPropertyChanged(nameof(SelectedStaged));
193215

216+
_visibleUnstaged.Clear();
194217
_unstaged.Clear();
195-
OnPropertyChanged(nameof(Unstaged));
218+
OnPropertyChanged(nameof(VisibleUnstaged));
196219

197220
_staged.Clear();
198221
OnPropertyChanged(nameof(Staged));
@@ -249,20 +272,26 @@ public void SetData(List<Models.Change> changes)
249272
}
250273

251274
var unstaged = new List<Models.Change>();
252-
var selectedUnstaged = new List<Models.Change>();
253275
var hasConflict = false;
254276
foreach (var c in changes)
255277
{
256278
if (c.WorkTree != Models.ChangeState.None)
257279
{
258280
unstaged.Add(c);
259281
hasConflict |= c.IsConflit;
260-
261-
if (lastSelectedUnstaged.Contains(c.Path))
262-
selectedUnstaged.Add(c);
263282
}
264283
}
265284

285+
_unstaged = unstaged;
286+
287+
var visibleUnstaged = GetVisibleUnstagedChanges();
288+
var selectedUnstaged = new List<Models.Change>();
289+
foreach (var c in visibleUnstaged)
290+
{
291+
if (lastSelectedUnstaged.Contains(c.Path))
292+
selectedUnstaged.Add(c);
293+
}
294+
266295
var staged = GetStagedChanges();
267296
var selectedStaged = new List<Models.Change>();
268297
foreach (var c in staged)
@@ -275,7 +304,7 @@ public void SetData(List<Models.Change> changes)
275304
{
276305
_isLoadingData = true;
277306
HasUnsolvedConflicts = hasConflict;
278-
Unstaged = unstaged;
307+
VisibleUnstaged = visibleUnstaged;
279308
Staged = staged;
280309
SelectedUnstaged = selectedUnstaged;
281310
SelectedStaged = selectedStaged;
@@ -336,46 +365,7 @@ public void StageSelected(Models.Change next)
336365

337366
public void StageAll()
338367
{
339-
StageChanges(_unstaged, null);
340-
}
341-
342-
public async void StageChanges(List<Models.Change> changes, Models.Change next)
343-
{
344-
if (_unstaged.Count == 0 || changes.Count == 0)
345-
return;
346-
347-
// Use `_selectedUnstaged` instead of `SelectedUnstaged` to avoid UI refresh.
348-
_selectedUnstaged = next != null ? [next] : [];
349-
350-
IsStaging = true;
351-
_repo.SetWatcherEnabled(false);
352-
if (changes.Count == _unstaged.Count)
353-
{
354-
await Task.Run(() => new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Exec());
355-
}
356-
else if (Native.OS.GitVersion >= Models.GitVersions.ADD_WITH_PATHSPECFILE)
357-
{
358-
var paths = new List<string>();
359-
foreach (var c in changes)
360-
paths.Add(c.Path);
361-
362-
var tmpFile = Path.GetTempFileName();
363-
File.WriteAllLines(tmpFile, paths);
364-
await Task.Run(() => new Commands.Add(_repo.FullPath, tmpFile).Exec());
365-
File.Delete(tmpFile);
366-
}
367-
else
368-
{
369-
for (int i = 0; i < changes.Count; i += 10)
370-
{
371-
var count = Math.Min(10, changes.Count - i);
372-
var step = changes.GetRange(i, count);
373-
await Task.Run(() => new Commands.Add(_repo.FullPath, step).Exec());
374-
}
375-
}
376-
_repo.MarkWorkingCopyDirtyManually();
377-
_repo.SetWatcherEnabled(true);
378-
IsStaging = false;
368+
StageChanges(_visibleUnstaged, null);
379369
}
380370

381371
public void UnstageSelected(Models.Change next)
@@ -388,44 +378,17 @@ public void UnstageAll()
388378
UnstageChanges(_staged, null);
389379
}
390380

391-
public async void UnstageChanges(List<Models.Change> changes, Models.Change next)
392-
{
393-
if (_staged.Count == 0 || changes.Count == 0)
394-
return;
395-
396-
// Use `_selectedStaged` instead of `SelectedStaged` to avoid UI refresh.
397-
_selectedStaged = next != null ? [next] : [];
398-
399-
IsUnstaging = true;
400-
_repo.SetWatcherEnabled(false);
401-
if (_useAmend)
402-
{
403-
await Task.Run(() => new Commands.UnstageChangesForAmend(_repo.FullPath, changes).Exec());
404-
}
405-
else if (changes.Count == _staged.Count)
406-
{
407-
await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec());
408-
}
409-
else
410-
{
411-
for (int i = 0; i < changes.Count; i += 10)
412-
{
413-
var count = Math.Min(10, changes.Count - i);
414-
var step = changes.GetRange(i, count);
415-
await Task.Run(() => new Commands.Reset(_repo.FullPath, step).Exec());
416-
}
417-
}
418-
_repo.MarkWorkingCopyDirtyManually();
419-
_repo.SetWatcherEnabled(true);
420-
IsUnstaging = false;
421-
}
422-
423381
public void Discard(List<Models.Change> changes)
424382
{
425383
if (_repo.CanCreatePopup())
426384
_repo.ShowPopup(new Discard(_repo, changes));
427385
}
428386

387+
public void ClearUnstagedFilter()
388+
{
389+
UnstagedFilter = string.Empty;
390+
}
391+
429392
public async void UseTheirs(List<Models.Change> changes)
430393
{
431394
var files = new List<string>();
@@ -1496,6 +1459,22 @@ public ContextMenu CreateContextForOpenAI()
14961459
}
14971460
}
14981461

1462+
private List<Models.Change> GetVisibleUnstagedChanges()
1463+
{
1464+
if (string.IsNullOrEmpty(_unstagedFilter))
1465+
return _unstaged;
1466+
1467+
var visible = new List<Models.Change>();
1468+
1469+
foreach (var c in _unstaged)
1470+
{
1471+
if (c.Path.Contains(_unstagedFilter, StringComparison.OrdinalIgnoreCase))
1472+
visible.Add(c);
1473+
}
1474+
1475+
return visible;
1476+
}
1477+
14991478
private List<Models.Change> GetStagedChanges()
15001479
{
15011480
if (_useAmend)
@@ -1511,6 +1490,77 @@ public ContextMenu CreateContextForOpenAI()
15111490
return rs;
15121491
}
15131492

1493+
private async void StageChanges(List<Models.Change> changes, Models.Change next)
1494+
{
1495+
if (changes.Count == 0)
1496+
return;
1497+
1498+
// Use `_selectedUnstaged` instead of `SelectedUnstaged` to avoid UI refresh.
1499+
_selectedUnstaged = next != null ? [next] : [];
1500+
1501+
IsStaging = true;
1502+
_repo.SetWatcherEnabled(false);
1503+
if (changes.Count == _unstaged.Count)
1504+
{
1505+
await Task.Run(() => new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Exec());
1506+
}
1507+
else if (Native.OS.GitVersion >= Models.GitVersions.ADD_WITH_PATHSPECFILE)
1508+
{
1509+
var paths = new List<string>();
1510+
foreach (var c in changes)
1511+
paths.Add(c.Path);
1512+
1513+
var tmpFile = Path.GetTempFileName();
1514+
File.WriteAllLines(tmpFile, paths);
1515+
await Task.Run(() => new Commands.Add(_repo.FullPath, tmpFile).Exec());
1516+
File.Delete(tmpFile);
1517+
}
1518+
else
1519+
{
1520+
for (int i = 0; i < changes.Count; i += 10)
1521+
{
1522+
var count = Math.Min(10, changes.Count - i);
1523+
var step = changes.GetRange(i, count);
1524+
await Task.Run(() => new Commands.Add(_repo.FullPath, step).Exec());
1525+
}
1526+
}
1527+
_repo.MarkWorkingCopyDirtyManually();
1528+
_repo.SetWatcherEnabled(true);
1529+
IsStaging = false;
1530+
}
1531+
1532+
private async void UnstageChanges(List<Models.Change> changes, Models.Change next)
1533+
{
1534+
if (changes.Count == 0)
1535+
return;
1536+
1537+
// Use `_selectedStaged` instead of `SelectedStaged` to avoid UI refresh.
1538+
_selectedStaged = next != null ? [next] : [];
1539+
1540+
IsUnstaging = true;
1541+
_repo.SetWatcherEnabled(false);
1542+
if (_useAmend)
1543+
{
1544+
await Task.Run(() => new Commands.UnstageChangesForAmend(_repo.FullPath, changes).Exec());
1545+
}
1546+
else if (changes.Count == _staged.Count)
1547+
{
1548+
await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec());
1549+
}
1550+
else
1551+
{
1552+
for (int i = 0; i < changes.Count; i += 10)
1553+
{
1554+
var count = Math.Min(10, changes.Count - i);
1555+
var step = changes.GetRange(i, count);
1556+
await Task.Run(() => new Commands.Reset(_repo.FullPath, step).Exec());
1557+
}
1558+
}
1559+
_repo.MarkWorkingCopyDirtyManually();
1560+
_repo.SetWatcherEnabled(true);
1561+
IsUnstaging = false;
1562+
}
1563+
15141564
private void SetDetail(Models.Change change, bool isUnstaged)
15151565
{
15161566
if (_isLoadingData)
@@ -1609,11 +1659,13 @@ private bool IsChanged(List<Models.Change> old, List<Models.Change> cur)
16091659
private bool _hasRemotes = false;
16101660
private List<Models.Change> _cached = [];
16111661
private List<Models.Change> _unstaged = [];
1662+
private List<Models.Change> _visibleUnstaged = [];
16121663
private List<Models.Change> _staged = [];
16131664
private List<Models.Change> _selectedUnstaged = [];
16141665
private List<Models.Change> _selectedStaged = [];
16151666
private int _count = 0;
16161667
private object _detailContext = null;
1668+
private string _unstagedFilter = string.Empty;
16171669
private string _commitMessage = string.Empty;
16181670

16191671
private bool _hasUnsolvedConflicts = false;

src/Views/WorkingCopy.axaml

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
</Grid.RowDefinitions>
2626

2727
<!-- Unstaged -->
28-
<Grid Grid.Row="0" RowDefinitions="28,*">
28+
<Grid Grid.Row="0" RowDefinitions="28,36,*">
2929
<!-- Unstaged Toolbar -->
3030
<Border Grid.Row="0" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}">
3131
<Grid ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto,Auto,Auto,Auto,Auto">
@@ -75,15 +75,47 @@
7575
</Grid>
7676
</Border>
7777

78+
<!-- Unstaged Filter -->
79+
<Border Grid.Row="1" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}">
80+
<TextBox Height="24"
81+
Margin="4,0"
82+
BorderThickness="1"
83+
CornerRadius="12"
84+
Text="{Binding UnstagedFilter, Mode=TwoWay}"
85+
BorderBrush="{DynamicResource Brush.Border2}"
86+
VerticalContentAlignment="Center">
87+
<TextBox.InnerLeftContent>
88+
<Path Width="14" Height="14"
89+
Margin="6,0,0,0"
90+
Fill="{DynamicResource Brush.FG2}"
91+
Data="{StaticResource Icons.Search}"/>
92+
</TextBox.InnerLeftContent>
93+
94+
<TextBox.InnerRightContent>
95+
<Button Classes="icon_button"
96+
Width="16"
97+
Margin="0,0,6,0"
98+
Command="{Binding ClearUnstagedFilter}"
99+
IsVisible="{Binding UnstagedFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
100+
HorizontalAlignment="Right">
101+
<Path Width="14" Height="14"
102+
Margin="0,1,0,0"
103+
Fill="{DynamicResource Brush.FG1}"
104+
Data="{StaticResource Icons.Clear}"/>
105+
</Button>
106+
</TextBox.InnerRightContent>
107+
</TextBox>
108+
</Border>
109+
78110
<!-- Unstaged Changes -->
79-
<v:ChangeCollectionView Grid.Row="1"
111+
<v:ChangeCollectionView Grid.Row="2"
80112
x:Name="UnstagedChangesView"
81113
Focusable="True"
82114
IsUnstagedChange="True"
83115
SelectionMode="Multiple"
84116
Background="{DynamicResource Brush.Contents}"
85117
ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=UnstagedChangeViewMode}"
86-
Changes="{Binding Unstaged}"
118+
Changes="{Binding VisibleUnstaged}"
87119
SelectedChanges="{Binding SelectedUnstaged, Mode=TwoWay}"
88120
ContextRequested="OnUnstagedContextRequested"
89121
ChangeDoubleTapped="OnUnstagedChangeDoubleTapped"

0 commit comments

Comments
 (0)