|
|
|
@@ -98,6 +98,14 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
|
|
|
|
|
|
|
|
|
|
public Action? CloseAction { get; set; }
|
|
|
|
|
|
|
|
|
|
// Parsing a large diff (UnifiedDiffParser.Parse + DiffTree.Build, both pure CPU work) is
|
|
|
|
|
// offloaded to a threadpool thread so it never blocks the UI dispatcher.
|
|
|
|
|
public OperationStatus ParseOp { get; } = new();
|
|
|
|
|
|
|
|
|
|
// Guards the fire-and-forget planning parse below against a stale write: a second
|
|
|
|
|
// DisplayedDiff change while the first parse is still running must not clobber PlanningFiles.
|
|
|
|
|
private int _planningFilesGeneration;
|
|
|
|
|
|
|
|
|
|
public DiffViewerViewModel(GitService git, IWorkerClient worker, AppSettings settings)
|
|
|
|
|
{
|
|
|
|
|
_git = git;
|
|
|
|
@@ -145,6 +153,16 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
|
|
|
|
|
public Task LoadAsync(CancellationToken ct = default) =>
|
|
|
|
|
Mode == DiffViewerMode.Planning ? LoadPlanningAsync() : LoadFilesAsync(ct);
|
|
|
|
|
|
|
|
|
|
// Shared by both parse call sites (LoadFilesAsync, ApplyParsedPlanningFilesAsync) so the
|
|
|
|
|
// ParseOp begin/offload/end ceremony isn't duplicated. Begin runs synchronously on the UI
|
|
|
|
|
// thread (the caller hasn't awaited anything yet), and the `using` guarantees End runs when
|
|
|
|
|
// the offloaded parse finishes — even if the caller's own result turns out to be stale.
|
|
|
|
|
private async Task<T> ParseOffUiThreadAsync<T>(Func<T> parse, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
using (ParseOp.Begin(Loc.T("ops.diff.parsing")))
|
|
|
|
|
return await Task.Run(parse, ct);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadFilesAsync(CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
FileTree.Clear();
|
|
|
|
@@ -179,8 +197,13 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var files = UnifiedDiffParser.Parse(raw).ToList();
|
|
|
|
|
foreach (var node in DiffTree.Build(files))
|
|
|
|
|
var (files, tree) = await ParseOffUiThreadAsync(() =>
|
|
|
|
|
{
|
|
|
|
|
var parsed = UnifiedDiffParser.Parse(raw);
|
|
|
|
|
return (parsed, DiffTree.Build(parsed));
|
|
|
|
|
}, ct);
|
|
|
|
|
|
|
|
|
|
foreach (var node in tree)
|
|
|
|
|
FileTree.Add(node);
|
|
|
|
|
|
|
|
|
|
SelectedNode = DiffTree.FirstLeaf(FileTree);
|
|
|
|
@@ -254,10 +277,24 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
|
|
|
|
|
|
|
|
|
|
partial void OnIsCombinedModeChanged(bool value) => ToggleCombinedCommand.Execute(null);
|
|
|
|
|
|
|
|
|
|
// DisplayedDiff is set from four places (OnSelectedSubtaskChanged plus three branches of
|
|
|
|
|
// ToggleCombinedAsync), and OnSelectedSubtaskChanged is itself a synchronous, non-awaitable
|
|
|
|
|
// callback — funneling the offload through this single change handler covers all of them
|
|
|
|
|
// without touching every call site. Fire-and-forget is safe because the generation counter
|
|
|
|
|
// below discards a parse whose result is no longer the current DisplayedDiff.
|
|
|
|
|
partial void OnDisplayedDiffChanged(string value)
|
|
|
|
|
{
|
|
|
|
|
var generation = ++_planningFilesGeneration;
|
|
|
|
|
_ = ApplyParsedPlanningFilesAsync(value, generation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ApplyParsedPlanningFilesAsync(string diff, int generation)
|
|
|
|
|
{
|
|
|
|
|
var files = await ParseOffUiThreadAsync(() => UnifiedDiffParser.Parse(diff));
|
|
|
|
|
if (generation != _planningFilesGeneration) return;
|
|
|
|
|
|
|
|
|
|
PlanningFiles.Clear();
|
|
|
|
|
foreach (var file in UnifiedDiffParser.Parse(value))
|
|
|
|
|
foreach (var file in files)
|
|
|
|
|
PlanningFiles.Add(file);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|