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

[LSP] Add Razor options provider to Roslyn #53879

Merged
merged 16 commits into from
Jun 9, 2021

Conversation

allisonchou
Copy link
Contributor

@allisonchou allisonchou commented Jun 4, 2021

  • Roslyn isn't respecting Razor's tabs/spaces settings when generating override and partial method completion TextEdits in the resolve handler.
  • This PR adds a document options provider that allows Razor to communicate its options to Roslyn.
  • The Razor side of this also needs to be merged for this bug to be resolved: Respond to Roslyn adding Razor options service razor#3719

Fixes (once Razor side is also merged):
https://github.com/dotnet/aspnetcore/issues/32555

@allisonchou allisonchou requested review from a team as code owners June 4, 2021 08:46
@allisonchou allisonchou added the LSP issues related to the roslyn language server protocol implementation label Jun 4, 2021
Copy link
Member

@dibarbet dibarbet left a comment

Choose a reason for hiding this comment

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

Have one main piece of feedback before I look at the rest

return Task.FromResult(CompletionChange.Create(new TextChange(item.Span, item.DisplayText)));
}
public virtual Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
=> Task.FromResult(CompletionChange.Create(new TextChange(item.Span, item.DisplayText)));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just changed this to expression body to be more consistent with the style of the other methods in this class

@allisonchou
Copy link
Contributor Author

I totally overhauled this PR based on your super helpful feedback @davidwengier and @dibarbet. The check for Razor options is now performed in the formatter, and the specific tabs/spaces options logic is now done on the Razor side (via dotnet/razor#3719). If either of you could take another look whenever you have time, that would be appreciated. Thanks 😀

Copy link
Contributor

@davidwengier davidwengier left a comment

Choose a reason for hiding this comment

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

So much cleaner 😁

Copy link
Member

@dibarbet dibarbet left a comment

Choose a reason for hiding this comment

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

🎉 totally agree with David, I much prefer this iteration

var documentOptionSetProvider = document.Services.GetService<IDocumentOptionSetProvider>();
if (documentOptionSetProvider is not null)
{
options = await documentOptionSetProvider.GetOptionsForDocumentAsync(document, cancellationToken).ConfigureAwait(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm, one interesting implication is that if you pass in options to this method they'll be ignored because the document options will win. With that in mind the options on line 157 account for this; in an ideal world all call sites to this method wouldn't actually pass options in "within" Roslyn and would then fall back to the appropriate document options services. All that can be done in the future though, just my 2cents

Copy link
Member

Choose a reason for hiding this comment

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

Yeah this seems really weird here. Couldn't the LSP layer or something deal with coupling this at that layer versus something else?

Copy link
Contributor Author

@allisonchou allisonchou Jun 7, 2021

Choose a reason for hiding this comment

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

Currently Razor is the only implementer of IDocumentOptionSetProvider so in practice I don't think there's currently an issue, however I totally see both of your points about the check being weird here. I'm exploring alternate options, will hold off on merging in the meantime.

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

This approach feels really funky and it feels like we still don't have this wired up at the right layer....?

Comment on lines 150 to 152
// Languages such as Razor can specify their own options instead of using the document's options.
var documentOptionSetProvider = document.Services.GetService<IDocumentOptionSetProvider>();
if (documentOptionSetProvider is not null)
Copy link
Member

Choose a reason for hiding this comment

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

So why is this having to be checked here rather than in the core document options logic we have, like through https://github.com/dotnet/roslyn/blob/main/src/Workspaces/Core/Portable/Options/IDocumentOptionsProvider.cs?

@allisonchou
Copy link
Contributor Author

allisonchou commented Jun 8, 2021

@jasonmalinowski @NTaylorMullen Based on your feedback, I refactored out the Razor options logic from the formatter over to the indentation service. This required once again overhauling a large portion of the PR, namely due to having to change the return type of the added Razor external access method. If either of you (or anyone else reading this) have any concerns with the updated approach, please let me know!

=> Task.FromResult<IDocumentOptions?>(new DocumentOptions(document.Project.Solution.Workspace, document.Id, _indentationManagerService));
public async Task<IDocumentOptions?> GetOptionsForDocumentAsync(Document document, CancellationToken cancellationToken)
{
// Languages such as Razor can specify their own formatting options.
Copy link
Member

Choose a reason for hiding this comment

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

So it's a bit odd that we are having to modify the InferredIndentationDocumentOptionsProviderFactory here when we could have a new MEF export. The Razor ExternalAccess could directly export it's own IDocumentOptionsProviderFactory that imports the Razor service directly. This would mean you can keep almost the entire change in the Razor layer and not have to introduce the extra layer in the middle: just rename IDocumentOptionsService to IRazorDocumentOptionsService and you can get away with bit less wrapping.

Copy link
Member

Choose a reason for hiding this comment

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

(it's also otherwise odd we'd be having this at an EditorFeatures layer, since what happens for VS Code?)

@@ -179,5 +179,13 @@ public static ImmutableArray<AbstractFormattingRule> GetFormattingRules(this Doc

return rules.AddRange(Formatter.GetDefaultFormattingRules(document));
}

public static bool IsRazorDocument(this Document document)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Method was moved

Copy link
Contributor

Choose a reason for hiding this comment

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

You moved it here for ExternalAccess.Razor right? If you all are "ok" with this living here then I don't have an issue but I also wouldn't be opposed to duplicating this in the ExternalAccess.Razor layer

Copy link
Member

Choose a reason for hiding this comment

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

I think I would prefer leaving in external access if possible - while currently 'true' the presence of a span mapping service means it's razor, there's nothing saying that spans can't be mapped in other cases. So I think I'd prefer to not let it leak too far.

Also, not sure if checking for this might be more appropriate? - https://sourceroslyn.io/#Microsoft.CodeAnalysis.ExternalAccess.Razor/RazorDocumentPropertiesServiceWrapper.cs,23 because span mappers are used in legacy razor as well and I'm guessing we don't want to override options there

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created a new ExternalAccess class for Razor extensions - https://github.com/dotnet/roslyn/blob/b9eb126fc9c7539f6e7cd3ee31dd1da318ac2f1c/src/Tools/ExternalAccess/Razor/Extensions.cs

Hope this is a little cleaner!

@allisonchou
Copy link
Contributor Author

@jasonmalinowski I moved to a MEF-based approach in response to your feedback. One thing I was unsure about was whether there's a way to exclusively activate the Razor options factory in Razor scenarios, as right now it activates with every type of project/solution. To mitigate this, I added a IsRazorDocument check to the Razor options provider, but I'm not sure if there's a better approach.

namespace Microsoft.CodeAnalysis.ExternalAccess.Razor
{
[Shared]
[ExportMetadata("Extensions", new string[] { "cshtml", "razor", })]
Copy link
Member

@dibarbet dibarbet Jun 9, 2021

Choose a reason for hiding this comment

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

What does this bit do exactly? Only trigger when there is a cshtml or razor file in the project? And you still need the razor check down below since generated documetns don't fit here, is that right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great point, I checked with Taylor just now and it seems the line is actually unnecessary so I removed it - seems like it's required for Razor's dynamic file info provider but not here, it just tells Roslyn to ask Razor for dynamic file info when a .cshtml or .razor file is added.

@@ -179,5 +179,13 @@ public static ImmutableArray<AbstractFormattingRule> GetFormattingRules(this Doc

return rules.AddRange(Formatter.GetDefaultFormattingRules(document));
}

public static bool IsRazorDocument(this Document document)
Copy link
Member

Choose a reason for hiding this comment

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

I think I would prefer leaving in external access if possible - while currently 'true' the presence of a span mapping service means it's razor, there's nothing saying that spans can't be mapped in other cases. So I think I'd prefer to not let it leak too far.

Also, not sure if checking for this might be more appropriate? - https://sourceroslyn.io/#Microsoft.CodeAnalysis.ExternalAccess.Razor/RazorDocumentPropertiesServiceWrapper.cs,23 because span mappers are used in legacy razor as well and I'm guessing we don't want to override options there

Copy link
Contributor

@ryanbrandenburg ryanbrandenburg left a comment

Choose a reason for hiding this comment

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

One nit-pick, otherwise looks OK, though I lack context.

@allisonchou
Copy link
Contributor Author

Thanks everyone for the reviews! I think I've gotten to all the feedback, if there's anything else please let me know. If everything looks good, I plan to merge this by EOD so we can start getting the Razor side PR going as well.

@allisonchou allisonchou merged commit 0e94007 into dotnet:main Jun 9, 2021
@ghost ghost added this to the Next milestone Jun 9, 2021
@allisonchou allisonchou deleted the AddRazorOptionsService branch June 9, 2021 22:51
@RikkiGibson RikkiGibson modified the milestones: Next, 17.0.P2 Jun 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE LSP issues related to the roslyn language server protocol implementation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants