diff --git a/src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs b/src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs index 218513e78b..ca5d487cda 100644 --- a/src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs +++ b/src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs @@ -5,6 +5,7 @@ public enum FileChangeType Unspecified = 0, Change, Create, - Delete + Delete, + DirectoryDelete } } diff --git a/src/OmniSharp.Abstractions/FileWatching/IFileSystemWatcher.cs b/src/OmniSharp.Abstractions/FileWatching/IFileSystemWatcher.cs index 90bccf5196..200146cea4 100644 --- a/src/OmniSharp.Abstractions/FileWatching/IFileSystemWatcher.cs +++ b/src/OmniSharp.Abstractions/FileWatching/IFileSystemWatcher.cs @@ -12,5 +12,6 @@ public interface IFileSystemWatcher /// The file path, directory path or file extension to watch. /// The callback that will be invoked when a change occurs in the watched file or directory. void Watch(string pathOrExtension, FileSystemNotificationCallback callback); + void WatchDirectories(FileSystemNotificationCallback callback); } } diff --git a/src/OmniSharp.Host/FileWatching/ManualFileSystemWatcher.cs b/src/OmniSharp.Host/FileWatching/ManualFileSystemWatcher.cs index 0a485eb27e..ce7c22ec81 100644 --- a/src/OmniSharp.Host/FileWatching/ManualFileSystemWatcher.cs +++ b/src/OmniSharp.Host/FileWatching/ManualFileSystemWatcher.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; namespace OmniSharp.FileWatching { @@ -8,6 +9,7 @@ internal partial class ManualFileSystemWatcher : IFileSystemWatcher, IFileSystem { private readonly object _gate = new object(); private readonly Dictionary _callbacksMap; + private readonly Callbacks _folderCallbacks = new Callbacks(); public ManualFileSystemWatcher() { @@ -18,6 +20,11 @@ public void Notify(string filePath, FileChangeType changeType = FileChangeType.U { lock (_gate) { + if(changeType == FileChangeType.DirectoryDelete) + { + _folderCallbacks.Invoke(filePath, FileChangeType.DirectoryDelete); + } + if (_callbacksMap.TryGetValue(filePath, out var fileCallbacks)) { fileCallbacks.Invoke(filePath, changeType); @@ -38,6 +45,11 @@ public void Notify(string filePath, FileChangeType changeType = FileChangeType.U } } + public void WatchDirectories(FileSystemNotificationCallback callback) + { + _folderCallbacks.Add(callback); + } + public void Watch(string pathOrExtension, FileSystemNotificationCallback callback) { if (pathOrExtension == null) diff --git a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs index 4c878a069b..fe0e39bd08 100644 --- a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs +++ b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs @@ -41,10 +41,26 @@ public OmniSharpWorkspace(HostServicesAggregator aggregator, ILoggerFactory logg { BufferManager = new BufferManager(this, fileSystemWatcher); _logger = loggerFactory.CreateLogger(); + fileSystemWatcher.WatchDirectories(OnDirectoryRemoved); } public override bool CanOpenDocuments => true; + + private void OnDirectoryRemoved(string path, FileChangeType changeType) + { + if(changeType == FileChangeType.DirectoryDelete) + { + var docs = CurrentSolution.Projects.SelectMany(x => x.Documents) + .Where(x => x.FilePath.StartsWith(path + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)); + + foreach(var doc in docs) + { + OnDocumentRemoved(doc.Id); + } + } + } + public void AddWaitForProjectModelReadyHandler(Func handler) { _waitForProjectModelReadyHandlers.Add(handler); diff --git a/tests/OmniSharp.Cake.Tests/LineIndexHelperFacts.cs b/tests/OmniSharp.Cake.Tests/LineIndexHelperFacts.cs index 3dc6f031f4..4446050a52 100644 --- a/tests/OmniSharp.Cake.Tests/LineIndexHelperFacts.cs +++ b/tests/OmniSharp.Cake.Tests/LineIndexHelperFacts.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; using OmniSharp.Cake.Utilities; +using OmniSharp.FileWatching; using OmniSharp.Services; using OmniSharp.Utilities; using Xunit; @@ -59,7 +60,7 @@ private static OmniSharpWorkspace CreateSimpleWorkspace(string fileName, string var workspace = new OmniSharpWorkspace( new HostServicesAggregator( Enumerable.Empty(), new LoggerFactory()), - new LoggerFactory(), null); + new LoggerFactory(), new DummyFileSystemWatcher()); var projectInfo = ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Create(), "ProjectNameVal", "AssemblyNameVal", LanguageNames.CSharp); @@ -147,5 +148,16 @@ public async Task TranslateFromGenerated_Should_Translate_To_Negative_If_Outside Assert.Equal(expected, actualIndex); } + + private class DummyFileSystemWatcher : IFileSystemWatcher + { + public void Watch(string pathOrExtension, FileSystemNotificationCallback callback) + { + } + + public void WatchDirectories(FileSystemNotificationCallback callback) + { + } + } } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/FilesChangedFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/FilesChangedFacts.cs index ace7d9d098..623ef1c38e 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/FilesChangedFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/FilesChangedFacts.cs @@ -26,11 +26,7 @@ public async Task TestFileAddedToMSBuildWorkspaceOnCreation() { var watcher = host.GetExport(); - var path = Path.GetDirectoryName(host.Workspace.CurrentSolution.Projects.First().FilePath); - var filePath = Path.Combine(path, "FileName.cs"); - File.WriteAllText(filePath, "text"); - var handler = GetRequestHandler(host); - await handler.Handle(new[] { new FilesChangedRequest() { FileName = filePath, ChangeType = FileChangeType.Create } }); + var filePath = await AddFile(host); Assert.Contains(host.Workspace.CurrentSolution.Projects.First().Documents, d => d.FilePath == filePath && d.Name == "FileName.cs"); } @@ -45,22 +41,19 @@ public async Task TestFileMovedToPreviouslyEmptyDirectory() var watcher = host.GetExport(); var projectDirectory = Path.GetDirectoryName(host.Workspace.CurrentSolution.Projects.First().FilePath); - const string filename = "FileName.cs"; - var filePath = Path.Combine(projectDirectory, filename); - File.WriteAllText(filePath, "text"); - var handler = GetRequestHandler(host); - await handler.Handle(new[] { new FilesChangedRequest() { FileName = filePath, ChangeType = FileChangeType.Create } }); + + var filePath = await AddFile(host); Assert.Contains(host.Workspace.CurrentSolution.Projects.First().Documents, d => d.FilePath == filePath && d.Name == "FileName.cs"); var nestedDirectory = Path.Combine(projectDirectory, "Nested"); Directory.CreateDirectory(nestedDirectory); - var destinationPath = Path.Combine(nestedDirectory, filename); + var destinationPath = Path.Combine(nestedDirectory, Path.GetFileName(filePath)); File.Move(filePath, destinationPath); - await handler.Handle(new[] { new FilesChangedRequest() { FileName = filePath, ChangeType = FileChangeType.Delete } }); - await handler.Handle(new[] { new FilesChangedRequest() { FileName = destinationPath, ChangeType = FileChangeType.Create } }); + await GetRequestHandler(host).Handle(new[] { new FilesChangedRequest() { FileName = filePath, ChangeType = FileChangeType.Delete } }); + await GetRequestHandler(host).Handle(new[] { new FilesChangedRequest() { FileName = destinationPath, ChangeType = FileChangeType.Create } }); Assert.Contains(host.Workspace.CurrentSolution.Projects.First().Documents, d => d.FilePath == destinationPath && d.Name == "FileName.cs"); Assert.DoesNotContain(host.Workspace.CurrentSolution.Projects.First().Documents, d => d.FilePath == filePath && d.Name == "FileName.cs"); @@ -68,7 +61,7 @@ public async Task TestFileMovedToPreviouslyEmptyDirectory() } [Fact] - public void TestMultipleDirectoryWatchers() + public async Task TestMultipleDirectoryWatchers() { using (var host = CreateEmptyOmniSharpHost()) { @@ -80,7 +73,7 @@ public void TestMultipleDirectoryWatchers() watcher.Watch("", (path, changeType) => { secondWatcherCalled = true; }); var handler = GetRequestHandler(host); - handler.Handle(new[] { new FilesChangedRequest() { FileName = "FileName.cs", ChangeType = FileChangeType.Create } }); + await handler.Handle(new[] { new FilesChangedRequest() { FileName = "FileName.cs", ChangeType = FileChangeType.Create } }); Assert.True(firstWatcherCalled); Assert.True(secondWatcherCalled); @@ -88,7 +81,7 @@ public void TestMultipleDirectoryWatchers() } [Fact] - public void TestFileExtensionWatchers() + public async Task TestFileExtensionWatchers() { using (var host = CreateEmptyOmniSharpHost()) { @@ -98,10 +91,38 @@ public void TestFileExtensionWatchers() watcher.Watch(".cs", (path, changeType) => { extensionWatcherCalled = true; }); var handler = GetRequestHandler(host); - handler.Handle(new[] { new FilesChangedRequest() { FileName = "FileName.cs", ChangeType = FileChangeType.Create } }); + await handler.Handle(new[] { new FilesChangedRequest() { FileName = "FileName.cs", ChangeType = FileChangeType.Create } }); Assert.True(extensionWatcherCalled); } } + + [Fact] + // This is specifically added to workaround VScode broken file remove notifications on folder removals/moves/renames. + // It's by design at VsCode and will probably not get fixed any time soon if ever. + public async Task TestThatOnFolderRemovalFilesUnderFolderAreRemoved() + { + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectAndSolution")) + using (var host = CreateOmniSharpHost(testProject.Directory)) + { + var watcher = host.GetExport(); + + var filePath = await AddFile(host); + + await GetRequestHandler(host).Handle(new[] { new FilesChangedRequest() { FileName = Path.GetDirectoryName(filePath), ChangeType = FileChangeType.DirectoryDelete } }); + + Assert.DoesNotContain(host.Workspace.CurrentSolution.Projects.First().Documents, d => d.FilePath == filePath && d.Name == Path.GetFileName(filePath)); + } + } + + private async Task AddFile(OmniSharpTestHost host) + { + var projectDirectory = Path.GetDirectoryName(host.Workspace.CurrentSolution.Projects.First().FilePath); + const string filename = "FileName.cs"; + var filePath = Path.Combine(projectDirectory, filename); + File.WriteAllText(filePath, "text"); + await GetRequestHandler(host).Handle(new[] { new FilesChangedRequest() { FileName = filePath, ChangeType = FileChangeType.Create } }); + return filePath; + } } }