File: HostWorkspace\FileWatching\SimpleFileChangeWatcher.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 Microsoft.CodeAnalysis.ProjectSystem;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching;
 
/// <summary>
/// A trivial 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 is not remotely efficient, but is available as a fallback implementation. If this needs to regularly be used, then this should get some improvements.
/// </remarks>
internal sealed class SimpleFileChangeWatcher : IFileChangeWatcher
{
    public IFileChangeContext CreateContext(ImmutableArray<WatchedDirectory> watchedDirectories)
        => new FileChangeContext(watchedDirectories);
 
    private sealed class FileChangeContext : IFileChangeContext
    {
        private readonly ImmutableArray<WatchedDirectory> _watchedDirectories;
 
        /// <summary>
        /// The directory watchers for the <see cref="_watchedDirectories"/>.
        /// </summary>
        private readonly ImmutableArray<FileSystemWatcher> _directoryFileSystemWatchers;
        private readonly ConcurrentSet<IndividualWatchedFile> _individualWatchedFiles = [];
 
        public FileChangeContext(ImmutableArray<WatchedDirectory> watchedDirectories)
        {
            var watchedDirectoriesBuilder = ImmutableArray.CreateBuilder<WatchedDirectory>(watchedDirectories.Length);
            var watcherBuilder = ImmutableArray.CreateBuilder<FileSystemWatcher>(watchedDirectories.Length);
 
            foreach (var watchedDirectory in watchedDirectories)
            {
                // If the directory doesn't exist, we can't create a watcher for changes inside of it. In this case, we'll just skip this as a directory
                // to watch; any requests for a watch within that directory will still create a one-off watcher for that specific file. That's not likely
                // to be an issue in practice: directories that are missing would be things like global reference directories -- if it's not there, we
                // probably won't ever see a watch for a file under there later anyways.
                if (Directory.Exists(watchedDirectory.Path))
                {
                    var watcher = new FileSystemWatcher(watchedDirectory.Path);
                    watcher.IncludeSubdirectories = true;
 
                    foreach (var filter in watchedDirectory.ExtensionFilters)
                        watcher.Filters.Add('*' + filter);
 
                    watcher.Changed += RaiseEvent;
                    watcher.Created += RaiseEvent;
                    watcher.Deleted += RaiseEvent;
                    watcher.Renamed += RaiseEvent;
 
                    watcher.EnableRaisingEvents = true;
 
                    watchedDirectoriesBuilder.Add(watchedDirectory);
                    watcherBuilder.Add(watcher);
                }
            }
 
            _watchedDirectories = watchedDirectoriesBuilder.ToImmutable();
            _directoryFileSystemWatchers = watcherBuilder.ToImmutable();
        }
 
        public event EventHandler<string>? FileChanged;
 
        public IWatchedFile EnqueueWatchingFile(string filePath)
        {
            // If this path is already covered by one of our directory watchers, nothing further to do
            if (WatchedDirectory.FilePathCoveredByWatchedDirectories(_watchedDirectories, filePath, StringComparison.Ordinal))
                return NoOpWatchedFile.Instance;
 
            var individualWatchedFile = new IndividualWatchedFile(filePath, this);
            _individualWatchedFiles.Add(individualWatchedFile);
            return individualWatchedFile;
        }
 
        private void RaiseEvent(object sender, FileSystemEventArgs e)
        {
            FileChanged?.Invoke(this, e.FullPath);
        }
 
        public void Dispose()
        {
            foreach (var directoryWatcher in _directoryFileSystemWatchers)
                directoryWatcher.Dispose();
        }
 
        private class IndividualWatchedFile : IWatchedFile
        {
            private readonly FileChangeContext _context;
            private readonly FileSystemWatcher? _watcher;
 
            public IndividualWatchedFile(string filePath, FileChangeContext context)
            {
                _context = context;
 
                // We always must create a watch on an entire directory, so create that, filtered to the single file name
                var directoryPath = Path.GetDirectoryName(filePath)!;
 
                // TODO: support missing directories properly
                if (Directory.Exists(directoryPath))
                {
                    _watcher = new FileSystemWatcher(directoryPath, Path.GetFileName(filePath));
                    _watcher.IncludeSubdirectories = false;
 
                    _watcher.Changed += _context.RaiseEvent;
                    _watcher.Created += _context.RaiseEvent;
                    _watcher.Deleted += _context.RaiseEvent;
                    _watcher.Renamed += _context.RaiseEvent;
 
                    _watcher.EnableRaisingEvents = true;
                }
                else
                {
                    _watcher = null;
                }
            }
 
            public void Dispose()
            {
                if (_context._individualWatchedFiles.Remove(this) && _watcher != null)
                {
                    _watcher.Changed -= _context.RaiseEvent;
                    _watcher.Created -= _context.RaiseEvent;
                    _watcher.Deleted -= _context.RaiseEvent;
                    _watcher.Renamed -= _context.RaiseEvent;
                    _watcher.Dispose();
                }
            }
        }
    }
}