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

Added an option to configure a code actions folder #848

Merged
merged 10 commits into from
May 17, 2017

Conversation

filipw
Copy link
Member

@filipw filipw commented May 5, 2017

Use case: I'd like to load my own or 3rd party code actions into Omnisharp.

In this PR I added a possibility to add the following CodeActions node to the omnisharp.json.

{
    "FormattingOptions": {
        "IndentationSize": 2
    },
    "CodeActions": {
        "LocationPaths": [
            "C:/codeactions"
        ]
    }
}

This will be picked up by RoslynFeaturesHostServicesProvider as a source location for loading assemblies containing refactorings and code fixes.
Because omnisharp.json is hierarchical (global/local) we can specify code actions on a global and local level if needed.

No I could download something like https://marketplace.visualstudio.com/items?itemName=josefpihrt.Roslynator2017 and extract its DLLs into C:/codeactions and profit. Relative and absolute paths are supported.

@filipw
Copy link
Member Author

filipw commented May 5, 2017

The reason why I thought it might be a good idea is it could give a chance for user to opt-in into features not yet available as internal part of Roslyn, but already built in the community or self-built. (i.e. dotnet/roslyn#8925 which has been requested already in Omnisharp repos and so on).

See: http://recordit.co/dEAKIBAvbl

@@ -3,4 +3,20 @@
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
</startup>
<runtime>
Copy link
Member Author

Choose a reason for hiding this comment

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

this was added so that a refactoring written against Roslyn 1.0.0 (which is often the case) can load too

Copy link
Contributor

Choose a reason for hiding this comment

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

This will need to be updated everytime we update Roslyn to a new version (or, at least, everytime the assembly version changes). Correct?

Copy link
Member Author

Choose a reason for hiding this comment

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

In the current format, yes, correct. While this is technically not necessary, most of the code actions out there (i.e. packaged as VSIX-es) reference Roslyn assemblies from the host (VS). So it has to be low version if you want to avoid having a dependency on something like let's say Update 3 of Visual Studio to be installed by the user.
I believe even the official Roslyn SDK templates default to Roslyn 1.0.0.

Otherwise, we'd only be able to load into the current process code actions built specifically against the version of Roslyn OmniSharp is built against.

I only added the three most common ones, maybe there are other DLLs that would be worthy of redirecting?

Copy link
Contributor

Choose a reason for hiding this comment

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

There are others that will come up eventually, but this is good for now.

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.

I think this is something we can support for the moment, with the knowledge that it is really just a power user feature, use at their own risk.

var normalizePaths = new HashSet<string>();
foreach (var locationPath in LocationPaths)
{
if (Path.IsPathRooted(locationPath))
Copy link
Member

Choose a reason for hiding this comment

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

If the paths are relative shouldn't they be rooted in the directory where the configuration file lives? However this may be super painful to pull off (would require a custom options provider)

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah so at the moment, relative always means relative to the folder in which OmniSharp is running.
I think this is reasonable for most cases, but you are right, when you are editing the global file like c:\users\filip\.omnisharp\omnisharp.json it might appear a bit counter intuitive.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, you're right that will be counter intuitive for the global config file case. Maybe a doc comment on LocationPaths to help describe the expected values?

@DustinCampbell
Copy link
Contributor

Is this change all that interesting if we add a proper diagnostics engine that loads analyzers referenced by a project?

@filipw
Copy link
Member Author

filipw commented May 5, 2017

Is this change all that interesting if we add a proper diagnostics engine that loads analyzers referenced by a project?

however stand alone refactorings without analyzer (i.e. initalize a field from constructor, LINQ into foreach and so on) are installed as VSIX - they are not referenced by projects. So this PR attempts to cover only that use case.

Setting a code actions folder in a global omnisharp.json would be to me roughly equivalent to installing a VSIX into my Visual Studio.

@DustinCampbell
Copy link
Contributor

The refactorings you mentioned will very likely be added to Roslyn itself.

@filipw
Copy link
Member Author

filipw commented May 5, 2017

yeah absolutely, then when they ship in the box, there is no need to opt into anything anymore 😃
But it's exactly the same case as with installing these types of add ons into Visual Studio - they can help fill some feature gap.

but until then, if you wish, you could tap into anything from here https://github.com/JosefPihrt/Roslynator/tree/master/source/Refactorings/Refactorings or here https://github.com/code-cracker/code-cracker/tree/master/src/CSharp/CodeCracker/Refactoring or few other places that people have been building.

This also has the reverse effect - if I'm an author of a refactoring, I'm loving the fact that I can build it, and it works in VS and I there is a way that I can also load it through OmniSharp into VS Code and other places. In fact, for example, at work, I have some domain specific refactorings for internal purposes only, that developers in our organization install into their VS - I think it would be great to be able to load them into OmniSharp.

I think ultimately the discussion should be what are the potential extensibility point in OmniSharp, and I think this is an excellent candidate one, especially as, in a way, it aligns with Visual Studio so experience-wise should be intuitive.

@DustinCampbell
Copy link
Contributor

Note: I'm not against this change. I'm just pushing on whether it's the right change, so we don't have to support it later if we decide that we should have done something else.

@david-driscoll
Copy link
Member

Would anyone complain if we called this a beta / alpha feature? Perhaps annotate the code (and docs when we get there) to indicate that.

Ideally I'd love some sort of nuget based process going forward, but this is a nice segway into that.

@filipw
Copy link
Member Author

filipw commented May 5, 2017

An alternative solution to the same problem could be something like this:

  • someone creates a hypothetical ThirdPartyCodeActionProvider implementation of OmniSharp's ICodeActionProvider which subclasses our RoslynCodeActionProvider and introduces the type of functionality contained in this current PR (ability to load code actions from some external paths)
  • package is published into Nuget
  • user installs this Nuget package into, say,`%USERPROFILE%.omnisharp\plugins
  • at startup we scan that plugins folder and pick up the plugin and swap the default RoslynCodeActionProvider with ThirdPartyCodeActionProvider and everything else remains unchanged.

The difference would be omnisharp.json cannot participate anymore, so the plugin would need to have its own config file, but I think that's fine.

This could also have a different format, and the plugin could just bundle the code actions straight up and offer them as direct dependency - the downside here is that you'd need such dedicated loader for everything, rather than a having a more generic solution.

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.

Sorry for the delay in giving this a careful review.

@@ -4,6 +4,8 @@ namespace OmniSharp.Options
{
public class OmniSharpOptions
{
public CodeActionOptions CodeActions { get; set; } = new CodeActionOptions();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this settable and instantiated here? Shouldn't it follow the same pattern as FormattingOptions below?

Copy link
Member 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 observation. Let's change it to public CodeActionOptions CodeActions { get; } = new CodeActionOptions(); I think this should still bind fine to the ASP.NET Configuration and be more elegant. The other property (FormattingOptions) should use the same approach, it would be cleaner. Currently it can be set through the constructor too, but that constructor is redundant - I don't believe we use it anywhere, only the default constructor is in use

@@ -7,6 +7,8 @@ namespace OmniSharp.Services
public interface IAssemblyLoader
{
Assembly Load(AssemblyName name);

IEnumerable<Assembly> LoadAll(string folderPath);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer this be called LoadAllFrom to call out that it uses the LoadFrom context rather than the Load context.

@@ -30,5 +33,43 @@ public Assembly Load(AssemblyName name)
_logger.LogTrace($"Assembly loaded: {name}");
return result;
}

public IEnumerable<Assembly> LoadAll(string folderPath)
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like we could return a better interface than IEnumerable<Assembly> since the instance is a List<Assembly>. Maybe IReadOnlyList<Assembly> so that callers could index into it if they want.


public IEnumerable<Assembly> LoadAll(string folderPath)
{
if (folderPath == null) return Enumerable.Empty<Assembly>();
Copy link
Contributor

Choose a reason for hiding this comment

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

string.IsNullOrWhitespace?

@@ -12,7 +14,7 @@ public class RoslynFeaturesHostServicesProvider : IHostServicesProvider
public ImmutableArray<Assembly> Assemblies { get; }

[ImportingConstructor]
public RoslynFeaturesHostServicesProvider(IAssemblyLoader loader)
public RoslynFeaturesHostServicesProvider(IAssemblyLoader loader, OmniSharpOptions options, IOmniSharpEnvironment env)
Copy link
Contributor

Choose a reason for hiding this comment

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

To separate concerns, this should probably be added to another IHostServicesProvider rather than tacked onto the RoslynFeaturesHostServicesProvider, since this is specifically about loading the Roslyn features assemblies. Note that OmniSharpWorkspace aggregates multiple IHostServicesProviders.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, I just realized that RoslynFeaturesHostServicesProvider is not marked as [Shared]. It probably doesn't matter in practice (since OmniSharpWorkspace is a singleton), but I just noticed.

Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't know there can be many of them - makes sense, thanks!

{
public string[] LocationPaths { get; set; }

public IEnumerable<string> GetLocations(IOmniSharpEnvironment env)
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps this should be called something like GetNormalizedLocationPaths(...) or something like that to indicate what it does differently over just accessing the LocationPaths property?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

{
if (LocationPaths == null) return Enumerable.Empty<string>();

var normalizePaths = new HashSet<string>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want this to be case-sensitive? Or, should you add StringComparer.OrdinalIgnoreCase? (We do that in other places)

@@ -21,6 +23,15 @@ public RoslynFeaturesHostServicesProvider(IAssemblyLoader loader)

builder.AddRange(loader.Load(Features, CSharpFeatures));

var codeActionLocations = options.CodeActions.GetLocations(env);
if (codeActionLocations != null && codeActionLocations.Any())
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional: codeActionLocations?.Any() == true

normalizePaths.Add(Path.Combine(env.TargetDirectory, locationPath));
}

return normalizePaths;
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that the paths will now be returned in a different order than they were specified, which could result in subtle bugs. (e.g. Imagine two assemblies that reference the same dependency but different versions. Their load order could be important.)

var normalizePaths = new HashSet<string>();
foreach (var locationPath in LocationPaths)
{
if (Path.IsPathRooted(locationPath))
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, you're right that will be counter intuitive for the global config file case. Maybe a doc comment on LocationPaths to help describe the expected values?

@DustinCampbell
Copy link
Contributor

I'm wondering if the folder should have a more general name. Maybe "RoslynExtensions" instead of "CodeActions". After all, there's nothing restricting this to just Code Actions. It would also load, say, CompletionProviders.

@filipw filipw force-pushed the features/code-actions-folder branch from cabd715 to da1bdb6 Compare May 15, 2017 15:10
@filipw
Copy link
Member Author

filipw commented May 15, 2017

I'm wondering if the folder should have a more general name. Maybe "RoslynExtensions" instead of "CodeActions". After all, there's nothing restricting this to just Code Actions. It would also load, say, CompletionProviders.

yes makes sense to me. We still need relevant exports to be there (i.e. ICodeActionProvider) to understand which types should be discovered, but as far as loading assemblies, indeed, it can bring in all kinds of extensions/add-ons.

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.

just nits


public IReadOnlyList<Assembly> LoadAllFrom(string folderPath)
{
if (string.IsNullOrWhiteSpace(folderPath)) return new Assembly[0];
Copy link
Contributor

Choose a reason for hiding this comment

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

You could use Array.Empty<Assembly>() and avoid allocating here if you like.

using OmniSharp.Services;
using System.Linq;
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? It looks like System.Linq wound up on the bottom.

using System.Reflection;
using OmniSharp.Options;
using OmniSharp.Services;
using System.Linq;
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? It looks like System.Linq wound up on the bottom.

@DustinCampbell
Copy link
Contributor

@filipw: CI passed. I'm good with this change (only found a couple of tiny nits). Are there more changes you wanted to make?

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.

Will wait for @filipw to come online after work (It's 5:30 now?)

Can fix the nits, or just let em pass and fix them later if we need to. 👍

@filipw
Copy link
Member Author

filipw commented May 17, 2017

Thanks guys, should be good to go.

@DustinCampbell
Copy link
Contributor

I won the "merge first contest". 😄 Kicking this again.

@DustinCampbell DustinCampbell merged commit 4e59671 into OmniSharp:dev May 17, 2017
@filipw filipw deleted the features/code-actions-folder branch May 18, 2017 16:32
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.

3 participants