Skip to content

Commit d96c3d2

Browse files
committed
Improve file change post-processing
1 parent 6f4b3b1 commit d96c3d2

File tree

17 files changed

+528
-224
lines changed

17 files changed

+528
-224
lines changed

src/BuiltInTools/dotnet-watch/FileItem.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,12 @@ internal readonly record struct FileItem
1010

1111
/// <summary>
1212
/// List of all projects that contain this file (does not contain duplicates).
13-
/// Empty if <see cref="Change"/> is <see cref="ChangeKind.Add"/> and the
14-
/// item has not been assigned to a project yet.
13+
/// Empty if the item is added but not been assigned to a project yet.
1514
/// </summary>
1615
public required List<string> ContainingProjectPaths { get; init; }
1716

1817
public string? StaticWebAssetPath { get; init; }
1918

20-
public ChangeKind Change { get; init; }
21-
2219
public bool IsStaticFile => StaticWebAssetPath != null;
2320
}
2421
}

src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates update
364364
switch (updates.Status)
365365
{
366366
case ModuleUpdateStatus.None:
367-
_reporter.Output("No C# changes to apply.");
367+
_reporter.Report(MessageDescriptor.NoHotReloadChangesToApply);
368368
break;
369369

370370
case ModuleUpdateStatus.Ready:

src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs

Lines changed: 169 additions & 70 deletions
Large diffs are not rendered by default.

src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ internal sealed class FileWatcher(IReporter reporter) : IDisposable
99
private readonly Dictionary<string, IDirectoryWatcher> _watchers = [];
1010

1111
private bool _disposed;
12-
public event Action<string, ChangeKind>? OnFileChange;
12+
public event Action<ChangedPath>? OnFileChange;
13+
14+
public bool SuppressEvents { get; set; }
1315

1416
public void Dispose()
1517
{
@@ -78,9 +80,12 @@ private void WatcherErrorHandler(object? sender, Exception error)
7880
}
7981
}
8082

81-
private void WatcherChangedHandler(object? sender, (string changedPath, ChangeKind kind) args)
83+
private void WatcherChangedHandler(object? sender, ChangedPath change)
8284
{
83-
OnFileChange?.Invoke(args.changedPath, args.kind);
85+
if (!SuppressEvents)
86+
{
87+
OnFileChange?.Invoke(change);
88+
}
8489
}
8590

8691
private void DisposeWatcher(string directory)
@@ -98,45 +103,43 @@ private void DisposeWatcher(string directory)
98103
private static string EnsureTrailingSlash(string path)
99104
=> (path is [.., var last] && last != Path.DirectorySeparatorChar) ? path + Path.DirectorySeparatorChar : path;
100105

101-
public Task<ChangedFile?> WaitForFileChangeAsync(Action? startedWatching, CancellationToken cancellationToken)
102-
=> WaitForFileChangeAsync(
103-
changeFilter: (path, kind) => new ChangedFile(new FileItem() { FilePath = path, ContainingProjectPaths = [] }, kind),
104-
startedWatching,
105-
cancellationToken);
106-
107-
public Task<ChangedFile?> WaitForFileChangeAsync(IReadOnlyDictionary<string, FileItem> fileSet, Action? startedWatching, CancellationToken cancellationToken)
108-
=> WaitForFileChangeAsync(
109-
changeFilter: (path, kind) => fileSet.TryGetValue(path, out var fileItem) ? new ChangedFile(fileItem, kind) : null,
106+
public async Task<ChangedFile?> WaitForFileChangeAsync(IReadOnlyDictionary<string, FileItem> fileSet, Action? startedWatching, CancellationToken cancellationToken)
107+
{
108+
var changedPath = await WaitForFileChangeAsync(
109+
acceptChange: change => fileSet.ContainsKey(change.Path),
110110
startedWatching,
111111
cancellationToken);
112112

113-
public async Task<ChangedFile?> WaitForFileChangeAsync(Func<string, ChangeKind, ChangedFile?> changeFilter, Action? startedWatching, CancellationToken cancellationToken)
113+
return changedPath.HasValue ? new ChangedFile(fileSet[changedPath.Value.Path], changedPath.Value.Kind) : null;
114+
}
115+
116+
public async Task<ChangedPath?> WaitForFileChangeAsync(Predicate<ChangedPath> acceptChange, Action? startedWatching, CancellationToken cancellationToken)
114117
{
115-
var fileChangedSource = new TaskCompletionSource<ChangedFile?>(TaskCreationOptions.RunContinuationsAsynchronously);
118+
var fileChangedSource = new TaskCompletionSource<ChangedPath?>(TaskCreationOptions.RunContinuationsAsynchronously);
116119
cancellationToken.Register(() => fileChangedSource.TrySetResult(null));
117120

118-
void FileChangedCallback(string path, ChangeKind kind)
121+
void FileChangedCallback(ChangedPath change)
119122
{
120-
if (changeFilter(path, kind) is { } changedFile)
123+
if (acceptChange(change))
121124
{
122-
fileChangedSource.TrySetResult(changedFile);
125+
fileChangedSource.TrySetResult(change);
123126
}
124127
}
125128

126-
ChangedFile? changedFile;
129+
ChangedPath? change;
127130

128131
OnFileChange += FileChangedCallback;
129132
try
130133
{
131134
startedWatching?.Invoke();
132-
changedFile = await fileChangedSource.Task;
135+
change = await fileChangedSource.Task;
133136
}
134137
finally
135138
{
136139
OnFileChange -= FileChangedCallback;
137140
}
138141

139-
return changedFile;
142+
return change;
140143
}
141144

142145
public static async ValueTask WaitForFileChangeAsync(string filePath, IReporter reporter, Action? startedWatching, CancellationToken cancellationToken)
@@ -146,7 +149,7 @@ public static async ValueTask WaitForFileChangeAsync(string filePath, IReporter
146149
watcher.WatchDirectories([Path.GetDirectoryName(filePath)!]);
147150

148151
var fileChange = await watcher.WaitForFileChangeAsync(
149-
changeFilter: (path, kind) => path == filePath ? new ChangedFile(new FileItem { FilePath = path, ContainingProjectPaths = [] }, kind) : null,
152+
acceptChange: change => change.Path == filePath,
150153
startedWatching,
151154
cancellationToken);
152155

src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ internal enum ChangeKind
1010
Delete
1111
}
1212

13-
internal readonly record struct ChangedFile(FileItem Item, ChangeKind Change);
13+
internal readonly record struct ChangedFile(FileItem Item, ChangeKind Kind);
14+
15+
internal readonly record struct ChangedPath(string Path, ChangeKind Kind);

src/BuiltInTools/dotnet-watch/Internal/FileWatcher/EventBasedDirectoryWatcher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Microsoft.DotNet.Watch
77
{
88
internal sealed class EventBasedDirectoryWatcher : IDirectoryWatcher
99
{
10-
public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
10+
public event EventHandler<ChangedPath>? OnFileChange;
1111

1212
public event EventHandler<Exception>? OnError;
1313

@@ -118,7 +118,7 @@ private void WatcherAddedHandler(object sender, FileSystemEventArgs e)
118118
private void NotifyChange(string fullPath, ChangeKind kind)
119119
{
120120
// Only report file changes
121-
OnFileChange?.Invoke(this, (fullPath, kind));
121+
OnFileChange?.Invoke(this, new ChangedPath(fullPath, kind));
122122
}
123123

124124
private void CreateFileSystemWatcher()

src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IDirectoryWatcher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Microsoft.DotNet.Watch
55
{
66
internal interface IDirectoryWatcher : IDisposable
77
{
8-
event EventHandler<(string filePath, ChangeKind kind)> OnFileChange;
8+
event EventHandler<ChangedPath> OnFileChange;
99

1010
event EventHandler<Exception> OnError;
1111

src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingDirectoryWatcher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ internal sealed class PollingDirectoryWatcher : IDirectoryWatcher
2121

2222
private volatile bool _disposed;
2323

24-
public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
24+
public event EventHandler<ChangedPath>? OnFileChange;
2525

2626
#pragma warning disable CS0067 // not used
2727
public event EventHandler<Exception>? OnError;
@@ -212,7 +212,7 @@ private void NotifyChanges()
212212
break;
213213
}
214214

215-
OnFileChange?.Invoke(this, (path, kind));
215+
OnFileChange?.Invoke(this, new ChangedPath(path, kind));
216216
}
217217
}
218218

src/BuiltInTools/dotnet-watch/Internal/IReporter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ public bool TryGetMessage(string? prefix, object?[] args, [NotNullWhen(true)] ou
7272
public static readonly MessageDescriptor ApplyUpdate_FileContentDoesNotMatchBuiltSource = new("{0} Expected if a source file is updated that is linked to project whose build is not up-to-date.", "⌚", MessageSeverity.Verbose, s_id++);
7373
public static readonly MessageDescriptor ConfiguredToLaunchBrowser = new("dotnet-watch is configured to launch a browser on ASP.NET Core application startup.", "⌚", MessageSeverity.Verbose, s_id++);
7474
public static readonly MessageDescriptor ConfiguredToUseBrowserRefresh = new("Configuring the app to use browser-refresh middleware", "⌚", MessageSeverity.Verbose, s_id++);
75+
public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = new("Ignoring change in hidden directory '{0}': {1} '{2}'", "⌚", MessageSeverity.Verbose, s_id++);
76+
public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = new("Ignoring change in output directory: {0} '{1}'", "⌚", MessageSeverity.Verbose, s_id++);
77+
public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = new("File addition triggered re-evaluation.", "⌚", MessageSeverity.Verbose, s_id++);
78+
public static readonly MessageDescriptor NoHotReloadChangesToApply = new ("No C# changes to apply.", "⌚", MessageSeverity.Output, s_id++);
7579
}
7680

7781
internal interface IReporter
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.DotNet.Watch;
5+
6+
internal static class PathUtilities
7+
{
8+
public static readonly IEqualityComparer<string> OSSpecificPathComparer = Path.DirectorySeparatorChar == '\\' ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
9+
10+
public static bool ContainsPath(IReadOnlySet<string> directories, string fullPath)
11+
{
12+
fullPath = Path.TrimEndingDirectorySeparator(fullPath);
13+
14+
while (true)
15+
{
16+
if (directories.Contains(fullPath))
17+
{
18+
return true;
19+
}
20+
21+
var containingDir = Path.GetDirectoryName(fullPath);
22+
if (containingDir == null)
23+
{
24+
return false;
25+
}
26+
27+
fullPath = containingDir;
28+
}
29+
}
30+
31+
public static IEnumerable<string> GetContainingDirectories(string path)
32+
{
33+
while (true)
34+
{
35+
var containingDir = Path.GetDirectoryName(path);
36+
if (containingDir == null)
37+
{
38+
yield break;
39+
}
40+
41+
yield return containingDir;
42+
path = containingDir;
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)