Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix folder deletion with VSCode #1821

Merged
3 changes: 2 additions & 1 deletion src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public enum FileChangeType
Unspecified = 0,
Change,
Create,
Delete
Delete,
DirectoryDelete
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public interface IFileSystemWatcher
/// <param name="pathOrExtension">The file path, directory path or file extension to watch.</param>
/// <param name="callback">The callback that will be invoked when a change occurs in the watched file or directory.</param>
void Watch(string pathOrExtension, FileSystemNotificationCallback callback);
void WatchDirectories(FileSystemNotificationCallback callback);
}
}
12 changes: 12 additions & 0 deletions src/OmniSharp.Host/FileWatching/ManualFileSystemWatcher.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace OmniSharp.FileWatching
{
internal partial class ManualFileSystemWatcher : IFileSystemWatcher, IFileSystemNotifier
{
private readonly object _gate = new object();
private readonly Dictionary<string, Callbacks> _callbacksMap;
private readonly Callbacks _folderCallbacks = new Callbacks();

public ManualFileSystemWatcher()
{
Expand All @@ -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);
Expand All @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions src/OmniSharp.Roslyn/OmniSharpWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,26 @@ public OmniSharpWorkspace(HostServicesAggregator aggregator, ILoggerFactory logg
{
BufferManager = new BufferManager(this, fileSystemWatcher);
_logger = loggerFactory.CreateLogger<OmniSharpWorkspace>();
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<string, Task> handler)
{
_waitForProjectModelReadyHandlers.Add(handler);
Expand Down
14 changes: 13 additions & 1 deletion tests/OmniSharp.Cake.Tests/LineIndexHelperFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,7 +60,7 @@ private static OmniSharpWorkspace CreateSimpleWorkspace(string fileName, string
var workspace = new OmniSharpWorkspace(
new HostServicesAggregator(
Enumerable.Empty<IHostServicesProvider>(), new LoggerFactory()),
new LoggerFactory(), null);
new LoggerFactory(), new DummyFileSystemWatcher());

var projectInfo = ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Create(),
"ProjectNameVal", "AssemblyNameVal", LanguageNames.CSharp);
Expand Down Expand Up @@ -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)
{
}
}
}
}
55 changes: 38 additions & 17 deletions tests/OmniSharp.Roslyn.CSharp.Tests/FilesChangedFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,7 @@ public async Task TestFileAddedToMSBuildWorkspaceOnCreation()
{
var watcher = host.GetExport<IFileSystemWatcher>();

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");
}
Expand All @@ -45,30 +41,27 @@ public async Task TestFileMovedToPreviouslyEmptyDirectory()
var watcher = host.GetExport<IFileSystemWatcher>();

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");
}
}

[Fact]
public void TestMultipleDirectoryWatchers()
public async Task TestMultipleDirectoryWatchers()
{
using (var host = CreateEmptyOmniSharpHost())
{
Expand All @@ -80,15 +73,15 @@ 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);
}
}

[Fact]
public void TestFileExtensionWatchers()
public async Task TestFileExtensionWatchers()
{
using (var host = CreateEmptyOmniSharpHost())
{
Expand All @@ -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<IFileSystemWatcher>();

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<string> 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;
}
}
}