-
Notifications
You must be signed in to change notification settings - Fork 420
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
Added an option to configure a code actions folder #848
Conversation
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). |
@@ -3,4 +3,20 @@ | |||
<startup> | |||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" /> | |||
</startup> | |||
<runtime> |
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 was added so that a refactoring written against Roslyn 1.0.0 (which is often the case) can load too
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 need to be updated everytime we update Roslyn to a new version (or, at least, everytime the assembly version changes). Correct?
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.
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?
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.
There are others that will come up eventually, but this is good for now.
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 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)) |
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 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)
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.
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.
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.
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?
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 |
The refactorings you mentioned will very likely be added to Roslyn itself. |
yeah absolutely, then when they ship in the box, there is no need to opt into anything anymore 😃 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. |
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. |
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. |
An alternative solution to the same problem could be something like this:
The difference would be 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. |
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.
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(); |
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.
Why is this settable and instantiated here? Shouldn't it follow the same pattern as FormattingOptions below?
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 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); |
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'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) |
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.
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>(); |
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.
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) |
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.
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
.
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.
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.
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 didn't know there can be many of them - makes sense, thanks!
{ | ||
public string[] LocationPaths { get; set; } | ||
|
||
public IEnumerable<string> GetLocations(IOmniSharpEnvironment env) |
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.
Perhaps this should be called something like GetNormalizedLocationPaths(...)
or something like that to indicate what it does differently over just accessing the LocationPaths
property?
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 (LocationPaths == null) return Enumerable.Empty<string>(); | ||
|
||
var normalizePaths = new HashSet<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.
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()) |
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.
Optional: codeActionLocations?.Any() == true
normalizePaths.Add(Path.Combine(env.TargetDirectory, locationPath)); | ||
} | ||
|
||
return normalizePaths; |
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.
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)) |
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.
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?
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. |
cabd715
to
da1bdb6
Compare
yes makes sense to me. We still need relevant exports to be there (i.e. |
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.
just nits
|
||
public IReadOnlyList<Assembly> LoadAllFrom(string folderPath) | ||
{ | ||
if (string.IsNullOrWhiteSpace(folderPath)) return new Assembly[0]; |
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.
You could use Array.Empty<Assembly>()
and avoid allocating here if you like.
using OmniSharp.Services; | ||
using System.Linq; |
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? It looks like System.Linq
wound up on the bottom.
using System.Reflection; | ||
using OmniSharp.Options; | ||
using OmniSharp.Services; | ||
using System.Linq; |
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? It looks like System.Linq
wound up on the bottom.
@filipw: CI passed. I'm good with this change (only found a couple of tiny nits). Are there more changes you wanted to make? |
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.
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. 👍
Thanks guys, should be good to go. |
I won the "merge first contest". 😄 Kicking this again. |
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 theomnisharp.json
.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.