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

Add mapped edits helper #11146

Merged
merged 23 commits into from
Nov 12, 2024
Merged

Add mapped edits helper #11146

merged 23 commits into from
Nov 12, 2024

Conversation

ryzngard
Copy link
Contributor

@ryzngard ryzngard commented Nov 4, 2024

Razor side of implementing IRazorMappingService.

Suggested file by file. Bulk of the implementation is in RazorEditHelper. Commits don't really tell a story but a lot of the changes are mechanical to implement new service + endpoint. The process is generally as follows:

Call LSP endpoint to map edits from IRazorMappingService. LSP endpoints cass RazorEditHelper.MapCSharpEditsAsync. This uses RazorEditHelper.TextChangeBuilder to get the initial changes the normalizes them since there may be overlapping changes.

Tests are in in RazorMapToDocumentEditsEndpointTest.cs and should generally describe functionality fairly well. I'm still flushing out more scenarios.

@ryzngard ryzngard changed the title [DRAFt] Add mapped edits helper Add mapped edits helper Nov 5, 2024
@ryzngard ryzngard marked this pull request as ready for review November 5, 2024 02:26
@ryzngard ryzngard requested a review from a team as a code owner November 5, 2024 02:26
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.

I love the tests in general, but I wonder if there might be false positives from manually supplying the mappings. Is it possible to create a test that gets a real set of changes from Roslyn when renaming a using in a C# file that is the real razor compiler output, and use those changes and mappings?

I didn't have the closest look at the actual RazorEditHelper algorithm, so not approving yet, but left a few comments for how far I've gotten so far. Nothing worrying though :)

continue;
}

if (RazorSyntaxFacts.IsInUsingDirective(node))
Copy link
Contributor

@davidwengier davidwengier Nov 5, 2024

Choose a reason for hiding this comment

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

Not for this PR obviously, and perhaps never if this isn't a problem, but I'm 99% sure that the Razor compiler always outputs all using directives in a contiguous block in C#, regardless of where they come from (ie, which block of usings, _Imports files, etc.). It might be worth having the compiler expose the start and end line, in the C# document, of the using directives and then just compare the edit against those two line numbers, rather than repeatedly digging through the Razor tree here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ooo that's interesting. As long as that holds true it would simplify this a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This looks to be true, but brings about a point related to #6155 in that we're not correctly handling changes that should go into _imports files. Let me think more on it....

Copy link
Contributor

Choose a reason for hiding this comment

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

I would be fine with v1 not handling _Imports files, as I don't think the compiler maps them at all. Doing so would need a filename added to SourceMapping or SourceSpan, and updating everywhere to check that property, and I don't think that is in the realms of possibility right now. In future (cohosting) we can use the #line mappings to handle this.

Copy link
Member

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

All of my feedback is relatively minor. Happy to drop a green check when tests are passing

cancellationToken);
}

public async Task<ImmutableArray<RazorMappedEditoResult>> MapTextChangesAsync(
Copy link
Member

Choose a reason for hiding this comment

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

Amusingly, RazorMappedEditResult is misspelled in the EA. 😁

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My worst version of spanglish to date

Copy link
Member

Choose a reason for hiding this comment

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

Too funny. 🤣

@ryzngard
Copy link
Contributor Author

ryzngard commented Nov 7, 2024

Is it possible to create a test that gets a real set of changes from Roslyn when renaming a using in a C# file that is the real razor compiler output, and use those changes and mappings?

I'd like to add one of these. Other than an integration test is it possible to fully set up a test workspace for this and unit test it? I was having trouble figuring it out but it's also quite late so brain may not be top tier functioning

@davidwengier
Copy link
Contributor

Other than an integration test is it possible to fully set up a test workspace for this and unit test it?

Not sure we have anything outside of cohosting that does the whole thing, but we have the bones of it.

For getting a real generated C# document, with real mappings, yes. Anything that inherits from SingleServerDelegatingEndpointTestBase can call CreateLanguageServerAsync and pass in additionalRazorDocuments which will be compiled and added to the Roslyn workspace. eg, this test adds a component, and the test infra automatically adds the generated C#: https://github.com/dotnet/razor/blob/main/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/DefinitionEndpointDelegationTest.cs#L148-L177

I wonder if you could use that, and put a test in RenameEndpointDelegationTest, which has one Razor file with @using Goo, one with @namespace Goo, and do a rename on the namespace name, which should delegate to Roslyn. Would that then hit the edit mapping service?

If not, I guess just poke around at what the CreateLanguageServerAsync method does, and CSharpTestLspServerHelpers.CreateCSharpTestWorkspace, and make a few things public and string them together some how?

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.

Love the tests, and I don't think any of my comments are blocking. A real world test could be a pain so I don't think its a problem to follow up. Despite my comments about having the bones of it, an integration test might just be easier :)

I do wonder which tests fail, and in what way, if the AddComplexUsingsChanges method is removed, and we just always call AddNewUsingChanges and AddRemoveUsingsChanges

return;
}

AddComplexUsingsChanges(codeDocument, addedUsings, removedUsings, cancellationToken);
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 not sure I understand why we need separate logic to have additions and removals. Can't we just get the add changes, then then the remove changes, and then sort them to make sure they're in order?

.Select(u => u.ToString()["using ".Length..^1]);
.Select(u => u.Name)
.WhereNotNull()
.SelectAsArray(n => n.ToString());
Copy link
Contributor

Choose a reason for hiding this comment

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

This is definitely a lot easier than the code I had before, but it does worry me ever so slightly.

Right now if a Razor file has:

@using System;
@using X = System.DateTime;
@using static System.Math;

they will be output into the generated C# verbatim (DotNetInternals), and just accessing the Name property will pull them out as ["System", "System.DateTime", "System.Math"] (SharpLab)

I admit, anything but a simple using statement is unlikely, but we do know that some users have using aliases, as they've reported bugs, and I think this change would mean those would still break under rename. Not sure if we need to worry about this, or wait for feedback, but at the same time I don't think there is harm in just putting back the old code. Or does it not work with the new edit helper?

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I think this is your follow up, #11168. I'll leave the comment though, because that issue mentions the Razor tree being inconsistent, but I'm pretty sure which ever solution we come up with for that would also have to be consistent with this.

Perhaps the answer is just to use the string representation of the entire directive, sans @ in Razor and ; in C#

private readonly IDocumentMappingService _documentMappingService = new DocumentMappingService(loggerFactory);
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<RazorMappingService>();

public async Task<ImmutableArray<RazorMappedSpanResult>> MapSpansAsync(Document document, IEnumerable<TextSpan> spans, CancellationToken cancellationToken)
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if RazorMappingService should inherit from RazorSpanMappingService so we don't have to re-implement this method, and TryGetMappedSpans. Unless they're different?

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 thought about that, but also am just considering removing the RazorSpanMappingService. I'll consider this a follow up cleanup

{|map1:public int Counter { get; set; }|}
}
""",
newCSharpSource:
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay this is the pettiest of nits, but perhaps it would be easier to rearrange the parameters so its C#, new C#, Razor, new Razor. Would make it easier to tell what has changed between new and old I think

@ryzngard ryzngard requested a review from a team as a code owner November 7, 2024 22:33
Copy link
Member

Choose a reason for hiding this comment

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

This change LGTM

ryzngard and others added 2 commits November 11, 2024 12:16
…Mapping/UsingsNodeComparer.cs

Co-authored-by: David Wengier <[email protected]>
…Mapping/RazorEditHelper.cs

Co-authored-by: David Wengier <[email protected]>
@ryzngard
Copy link
Contributor Author

I'm planning on getting this in today with the follow up items I filed. If you see anything else that seems off please let me know! It's less complicated than my last attempt but I think there's probably more work to simplify even further.

@davidwengier especially has a good comment on #11146 (comment) where I want to follow up and make sure assumptions I had before are true. 3 of the current tests fail but that doesn't mean there can't be further simplifications.

@davidwengier
Copy link
Contributor

3 of the current tests fail but that doesn't mean there can't be further simplifications.

Would be curious to know in what fashion they fail too, and whether it's blocking. You're essentially implementing an algorithm that does both sort usings, group system usings first, and honours a users arbitrary grouping. An admirable goal, but perhaps something we don't need to hit.

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.

5 participants