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

Listen for file renames #987

Merged
merged 28 commits into from
Oct 27, 2017
Merged

Conversation

rchande
Copy link

@rchande rchande commented Oct 24, 2017

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.

@rchande
Copy link
Author

rchande commented Oct 24, 2017

Tag @DustinCampbell @filipw

@rchande
Copy link
Author

rchande commented Oct 24, 2017

Fixes dotnet/vscode-csharp#785

Copy link
Contributor

@DustinCampbell DustinCampbell left a 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;
Copy link
Contributor

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":
Copy link
Contributor

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.

Copy link
Author

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.

Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Member

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 😀

Copy link
Author

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) =>
Copy link
Contributor

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) =>
Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

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")
Copy link
Contributor

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;
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Member

@david-driscoll david-driscoll left a 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)
Copy link
Member

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))]
Copy link
Member

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?

Copy link
Contributor

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)
Copy link
Member

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();
Copy link
Member

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.

Ravi Chande added 5 commits October 25, 2017 08:19
Use json.net implicit enum serialization
Add an "unspecified" value to FileChangeType and remove nullables
@rchande
Copy link
Author

rchande commented Oct 25, 2017

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.

Copy link
Contributor

@DustinCampbell DustinCampbell left a 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;
Copy link
Contributor

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.

Copy link
Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bummer 😞

Copy link
Contributor

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>>();
Copy link
Contributor

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(...)?

Copy link
Author

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;
Copy link
Contributor

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
{

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank line

@DustinCampbell
Copy link
Contributor

DustinCampbell commented Oct 25, 2017

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.

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;
Copy link
Contributor

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).

@rchande
Copy link
Author

rchande commented Oct 25, 2017

@DustinCampbell Incorporated your latest feedback

{
if (_directoryCallBacks.TryGetValue(path, out var existingCallback))
{
_directoryCallBacks[path] = (Action<string, FileChangeType>)Delegate.Combine(callback, existingCallback);
Copy link
Contributor

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So much nicer. Fixing.

@DustinCampbell
Copy link
Contributor

@david-driscoll : Does this look better to you?

@rchande
Copy link
Author

rchande commented Oct 26, 2017

Found some issues while testing--let's hold off on merging this for a bit.

Copy link
Member

@david-driscoll david-driscoll left a 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; }
Copy link
Member

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)

@rchande
Copy link
Author

rchande commented Oct 27, 2017

This is ready to merge. Can you hit the button @DustinCampbell ?

@DustinCampbell DustinCampbell merged commit 22e0183 into OmniSharp:master Oct 27, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants