-
Notifications
You must be signed in to change notification settings - Fork 418
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
Listen for file renames #987
Conversation
…t was made to the file.
* Pass through FileChangeTypes when available * Allow clients to watch a directory (in addition to individual files)
Fixes dotnet/vscode-csharp#785 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks very good! Just a couple of things.
@@ -1,12 +1,20 @@ | |||
using System; | |||
using OmniSharp.Models.FilesChanged; | |||
using System; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sort usings should have System
on top.
{ | ||
switch (reader.Value as string) | ||
{ | ||
case "Change": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should probably be case-insensitive string matches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this and switched to the automatic serialization offered by json.net, which is case insensitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great!
/// </summary> | ||
/// <param name="path">The path to the file</param> | ||
/// <param name="verb">The type of change. Hosts are not required to pass a change type</param> | ||
void TriggerChange(string path, FileChangeType? verb); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, the terms type
and verb
should match. So, if the type is FileChangeType
, the parameter here should be type
. Or, the type should be FileChangeVerb
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it somehow feels that an enum with default value would be more intuitive than nullable enum? at least that's what MSDN would tell us 😀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed names and added a default value.
@@ -166,13 +166,13 @@ private void UpdateProject(string projectDirectory) | |||
_projectStates.Update(projectDirectory, contexts, AddProject, RemoveProject); | |||
|
|||
var projectFilePath = contexts.First().ProjectFile.ProjectFilePath; | |||
_fileSystemWatcher.Watch(projectFilePath, file => | |||
_fileSystemWatcher.Watch(projectFilePath, (file, verb) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment above about type
vs. verb
. If you go with type
, be sure to update this as well.
{ | ||
_logger.LogInformation($"Watcher: {file} updated."); | ||
Update(allowRestore: true); | ||
}); | ||
|
||
_fileSystemWatcher.Watch(Path.ChangeExtension(projectFilePath, "lock.json"), file => | ||
_fileSystemWatcher.Watch(Path.ChangeExtension(projectFilePath, "lock.json"), (file, verb) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
private void OnDirectoryFileChanged(string path, FileChangeType? changeType) | ||
{ | ||
// Hosts may not have passed through a file change type | ||
if (changeType == null && !File.Exists(path) || changeType.GetValueOrDefault() == FileChangeType.Delete) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need the GetValueOrDefault()
here.
} | ||
} | ||
|
||
if (changeType == null && File.Exists(path) || changeType.GetValueOrDefault() == FileChangeType.Create) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or here
if (changeType == null && File.Exists(path) || changeType.GetValueOrDefault() == FileChangeType.Create) | ||
{ | ||
// Only add .cs files to the workspace | ||
if (Path.GetExtension(path) == ".cs") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be a case-insensitive comparison.
using System.Threading.Tasks; | ||
using TestUtility; | ||
using Xunit; | ||
using Xunit.Abstractions; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usings should be sorted with System
on top.
handler.Handle(new[] { new FilesChangedRequest() { FileName = "FileName.cs", FileChangeType = FileChangeType.Create } }); | ||
|
||
Assert.Equal("FileName.cs", filePath); | ||
Assert.Equal(FileChangeType.Create, ct); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth checking to see if the file is now included in the workspace? I believe you can get to it with host.Workspace
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The presence of the custom JsonConveter
has me confused, other than that 👍
public override bool CanConvert(Type objectType) => objectType == typeof(FileChangeType); | ||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | ||
{ | ||
switch (reader.Value as string) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason this isn't using Enum.TryParse<FileChangeType>("abc", true, out var item)
?
Either way it should probably be invariant in case other editors send through the wrong casing.
[OmniSharpEndpoint(OmniSharpEndpoints.FilesChanged, typeof(IEnumerable<FilesChangedRequest>), typeof(FilesChangedResponse))] | ||
public class FilesChangedRequest : Request | ||
{ | ||
[JsonConverter(typeof(FileChangeTypeConverter))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't this just be StringEnumConverter
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's right.
private void OnDirectoryFileChanged(string path, FileChangeType? changeType) | ||
{ | ||
// Hosts may not have passed through a file change type | ||
if (changeType == null && !File.Exists(path) || changeType.GetValueOrDefault() == FileChangeType.Delete) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nit it's more obvious that the value is Nullable<> if we use !changeType.HasValue
.
return null; | ||
} | ||
} | ||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we keep this we should override CanWrite
and return false, would hate to have this throw unexpectedly.
Use json.net implicit enum serialization Add an "unspecified" value to FileChangeType and remove nullables
Incorporated feedback. Want to sign off @DustinCampbell @david-driscoll ? @DustinCampbell regarding the test, it looks like all of the existing test helpers create projects that use the "dotnet" project system. I think it might be worth going through and updating the tests to use the "msbuild" project system, or to run against both project systems. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking much better! Could you take a look at my comment about delegate registration with the ManualFileSystemWatcher
using System.Composition; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Threading.Tasks; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sort usings with System
namespaces on top.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I swear my option is resetting underneath me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bummer 😞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you've discovered a VS bug via dogfooding?
|
||
namespace OmniSharp.FileWatching | ||
{ | ||
public class ManualFileSystemWatcher : IFileSystemWatcher | ||
{ | ||
private readonly Dictionary<string, Action<string>> _callbacks = new Dictionary<string, Action<string>>(); | ||
private readonly Dictionary<string, Action<string, FileChangeType>> _callbacks = new Dictionary<string, Action<string, FileChangeType>>(); | ||
private readonly Dictionary<string, Action<string, FileChangeType>> _directoryCallBacks = new Dictionary<string, Action<string, FileChangeType>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One concern I have here is that only a single callback can be called for a particular path. And, the last callback registered will win. I don't think this will be a problem so far, but it may be in the future -- especially since we're watching directories. I can easily imagine some other project system (like script or cake) wanting to listen to the same directories that the MSBuild project system does. Should we combine callbacks using Delegate.Combine(...)
and Delegate.Remove(...)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point--will add this
using OmniSharp.Models.FilesChanged; | ||
using System.Collections.Generic; | ||
using System.Composition; | ||
using System.Threading.Tasks; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sort usings with System
namespaces on top please.
|
||
namespace OmniSharp.Roslyn.CSharp.Tests | ||
{ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blank line
The appropriate way to do this is not to create dummy projects in tests. Instead, you can use one of the test projects like we do in the OmniSharp.MSBuild tests like so: https://github.com/OmniSharp/omnisharp-roslyn/blob/master/tests/OmniSharp.MSBuild.Tests/WorkspaceInformationTests.cs#L18-L32. |
using OmniSharp.Mef; | ||
using System.Collections.Generic; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sort System
namespaces first please. You've got the option set incorrectly in VS (this is also the preference in Roslyn).
@DustinCampbell Incorporated your latest feedback |
{ | ||
if (_directoryCallBacks.TryGetValue(path, out var existingCallback)) | ||
{ | ||
_directoryCallBacks[path] = (Action<string, FileChangeType>)Delegate.Combine(callback, existingCallback); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK leaving this as is, but it's also possible to just write:
_directoryCallBacks[path] = callback + existingCallback;
Believe it or not, +
handles combination of delegates in C#. 😄
The More You Know
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So much nicer. Fixing.
@david-driscoll : Does this look better to you? |
Found some issues while testing--let's hold off on merging this for a bit. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes look good, only one comment about the serialization, but I don't really have a preference.
[OmniSharpEndpoint(OmniSharpEndpoints.FilesChanged, typeof(IEnumerable<FilesChangedRequest>), typeof(FilesChangedResponse))] | ||
public class FilesChangedRequest : Request | ||
{ | ||
public FileChangeType ChangeType { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will serialize as an int (I think, I didn't see any where we configure Newtonsoft.Json
)
This is ready to merge. Can you hit the button @DustinCampbell ? |
Use the improved file watching in dotnet/vscode-csharp#1805 to better handle file renames.
When adding files to the workspace, we now set up file watching for each directory for which we add a document. If a file deletion comes in from such a directory, we'll remove any applicable documents from the workspace. If a .cs file is created, we kick off a task for the buffer change notification service to use its logic to add the file to applicable projects.
These changes allow users to rename files from the VS Code file tree without getting errors about duplicate type declarations. For projects with file globbing, the workspace will automatically wire up the new file and the user won't see any errors about missing types.