File: HostWorkspace\FileWatching\DefaultFileChangeWatcher.cs
Web Access
Project: src\src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj (Microsoft.CodeAnalysis.LanguageServer)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis.ProjectSystem;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching;
 
/// <summary>
/// An implementation of <see cref="IFileChangeWatcher" /> that is built atop the framework <see cref="FileSystemWatcher" />. This is used if we can't
/// use the LSP one.
/// </summary>
/// <remarks>
/// This implementation creates one <see cref="FileSystemWatcher"/> per root drive and uses filtering to route file
/// change events to the appropriate watchers. The watchers are shared between all <see cref="FileChangeContext"/>
/// instances and are disposed when all contexts using them have been disposed.
/// </remarks>
internal sealed partial class DefaultFileChangeWatcher : IFileChangeWatcher
{
    private static readonly StringComparer s_pathStringComparer = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
        ? StringComparer.OrdinalIgnoreCase
        : StringComparer.Ordinal;
    private static readonly StringComparison s_pathStringComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
        ? StringComparison.OrdinalIgnoreCase
        : StringComparison.Ordinal;
 
    private readonly ReferenceCountedDisposableCache<string, FileSystemWatcher> _sharedRootWatchers = new(s_pathStringComparer);
 
    public IFileChangeContext CreateContext(ImmutableArray<WatchedDirectory> watchedDirectories)
        => new FileChangeContext(this, watchedDirectories);
 
    private IReferenceCountedDisposable<ICacheEntry<string, FileSystemWatcher>> GetOrCreateSharedWatcher(string rootPath)
    {
        var rootWatcher = _sharedRootWatchers.GetOrCreate<object?>(rootPath, static (key, _) => new FileSystemWatcher(key), arg: null);
        rootWatcher.Target.Value.IncludeSubdirectories = true;
        rootWatcher.Target.Value.EnableRaisingEvents = true;
        return rootWatcher;
    }
 
    private static void AttachWatcher(IEventRaiser eventRaiser, IReferenceCountedDisposable<ICacheEntry<string, FileSystemWatcher>> watcher)
    {
        watcher.Target.Value.Changed += eventRaiser.RaiseEvent;
        watcher.Target.Value.Created += eventRaiser.RaiseEvent;
        watcher.Target.Value.Deleted += eventRaiser.RaiseEvent;
        watcher.Target.Value.Renamed += eventRaiser.RaiseEvent;
    }
 
    private static void DetachAndDisposeWatcher(IEventRaiser eventRaiser, IReferenceCountedDisposable<ICacheEntry<string, FileSystemWatcher>> watcher)
    {
        watcher.Target.Value.Changed -= eventRaiser.RaiseEvent;
        watcher.Target.Value.Created -= eventRaiser.RaiseEvent;
        watcher.Target.Value.Deleted -= eventRaiser.RaiseEvent;
        watcher.Target.Value.Renamed -= eventRaiser.RaiseEvent;
        watcher.Dispose();
    }
 
    internal interface IEventRaiser
    {
        void RaiseEvent(object? sender, FileSystemEventArgs e);
    }
 
    internal static class TestAccessor
    {
        public static IEnumerable<string> GetWatchedRootPaths(DefaultFileChangeWatcher watcher)
            => ReferenceCountedDisposableCache<string, FileSystemWatcher>.TestAccessor.GetCacheKeys(watcher._sharedRootWatchers);
    }
}