-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Enable support for an LSP client to open source generated files #75180
Conversation
// Source generated documents cannot go through OnDocumentOpened/Closed. | ||
// There is a separate OnSourceGeneratedDocumentOpened/Closed method, but there is no need | ||
// for us to call it in LSP - it deals with mapping TextBuffers to text containers. | ||
return; |
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.
@jasonmalinowski @CyrusNajmabadi for thoughts. I decided to not call it - otherwise I'd have to implement similar TryOnSourceGeneratedDocumentOpen/Closed like we have for regular documents. It didn't seem necessary as the open was only trying to associate things with text buffers.
Since these are readonly (and content managed by the server), there is nothing to update in the workspace either
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 the comment coudlo be beefed up.
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.
So I've forgotten, for regular documents, why are we opening/closing the documents? Is it just so other code can ask "what's open" from the workspace or are we still getting some other benefit?
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 honestly can't remembner anything that would require the regular documents to actually be opened in the workspace. Generally LSP features should be checking if LSP thinks the document is opened.
And there isn't any source text container stuff to do either, sicne we have our own LSP fork management.
src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs
Show resolved
Hide resolved
@@ -43,24 +43,31 @@ internal class LanguageInfoProvider : ILanguageInfoProvider | |||
{ ".mts", s_typeScriptLanguageInformation }, | |||
}; | |||
|
|||
public LanguageInformation GetLanguageInformation(string documentPath, string? lspLanguageId) | |||
public LanguageInformation GetLanguageInformation(Uri uri, string? lspLanguageId) |
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.
Is lspLanguageId
the language selected in the VS Code status bar for a particular document? If so, why would we not want to honor that over the file extension, in the case that the user has chosen something different?
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.
It is the languageId the LSP client gives us (which I believe in VSCode will change based on the language selected in the status bar).
I suppose generally we should use the languageId instead (if we're able to map it), otherwise fall back to the file extension.
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'm going to defer this for now though - there is some small risk of regression, especially in VS if they give us the wrong languageId. I'd rather take that in a separate PR
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.
Agreed that in the case of a well-behaved LSP client, we should trust the language ID. Also agreed we should keep this as is for now and verify we have well-behaved clients before changing it. 😄
src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratedFileGetTextHandler.cs
Outdated
Show resolved
Hide resolved
src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratedFileGetTextHandler.cs
Outdated
Show resolved
Hide resolved
4b76d35
to
5477d5b
Compare
5477d5b
to
58c8106
Compare
src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs
Outdated
Show resolved
Hide resolved
src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs
Outdated
Show resolved
Hide resolved
src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs
Outdated
Show resolved
Hide resolved
src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs
Outdated
Show resolved
Hide resolved
src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs
Outdated
Show resolved
Hide resolved
src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs
Outdated
Show resolved
Hide resolved
src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs
Show resolved
Hide resolved
src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs
Outdated
Show resolved
Hide resolved
src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs
Outdated
Show resolved
Hide resolved
var assemblyVersion = Uri.EscapeDataString(identity.Generator.AssemblyVersion.ToString()); | ||
var typeName = Uri.EscapeDataString(identity.Generator.TypeName); | ||
|
||
var uri = $"{Scheme}://{projectId}/{hintPathPortion}?{DocumentIdParam}={documentId}&{HintNameParam}={hintName}&{AssemblyNameParam}={assemblyName}&{AssemblyVersionParam}={assemblyVersion}&{TypeNameParam}={typeName}"; |
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.
so, the part i trust is the projectId/docId. teh rest i think should all be opaque data we effectively don't munge with (specifically referring to hintpathportion).
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.
The project ID/document ID is still the core part, yes. But including everything else means we're able to round-trip the entire SourceGeneratedDocumentIdentity. This allows for at least one edge case to be better handled: if the user opens a source generated file, and then something means that file isn't being generated any more, we can still respond to an LSP request for this URI if the file is still open on the client, as if it's otherwise a regular SourceGeneratedDocument rather than it being a generic miscellaneous files file.
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.
Overall this still jives with what I expect this to look like. I feel like my old PR had some issues around opening/closing documents repeatedly and miscellaneous files breaking, but I trust you've tested that well.
My two open questions:
- Regarding the DateTime.Now stuff -- what's that being used for?
- For the code we have that's doing document open/closed on mutating workspaces -- is that just so code can ask what documents are open or did we have something more happening there?
src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs
Outdated
Show resolved
Hide resolved
src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs
Outdated
Show resolved
Hide resolved
var assemblyVersion = Uri.EscapeDataString(identity.Generator.AssemblyVersion.ToString()); | ||
var typeName = Uri.EscapeDataString(identity.Generator.TypeName); | ||
|
||
var uri = $"{Scheme}://{projectId}/{hintPathPortion}?{DocumentIdParam}={documentId}&{HintNameParam}={hintName}&{AssemblyNameParam}={assemblyName}&{AssemblyVersionParam}={assemblyVersion}&{TypeNameParam}={typeName}"; |
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.
The project ID/document ID is still the core part, yes. But including everything else means we're able to round-trip the entire SourceGeneratedDocumentIdentity. This allows for at least one edge case to be better handled: if the user opens a source generated file, and then something means that file isn't being generated any more, we can still respond to an LSP request for this URI if the file is still open on the client, as if it's otherwise a regular SourceGeneratedDocument rather than it being a generic miscellaneous files file.
public static async ValueTask<Document?> GetDocumentAsync(this Solution solution, TextDocumentIdentifier documentIdentifier, CancellationToken cancellationToken) | ||
{ | ||
var textDocument = await solution.GetTextDocumentAsync(documentIdentifier, cancellationToken).ConfigureAwait(false); | ||
Contract.ThrowIfTrue(textDocument is not null && textDocument is not Document, $"{textDocument!.Id} is not a Document"); |
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.
The semantics here feel a bit funky -- so if the document doesn't exist we'll return null, but if it does exist but it's an additional file we'll throw? Seems like we might end up with a bunch of accidental exceptions that way?
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 my view, the accidental exceptions would be more catching unintentional bugs - someone was asking for a document - but they got an additional document (or other kind of document) instead - which likely means they should be calling a different API.
If we returned null, then the code might 'work' but not be doing what they want.
// Source generated documents cannot go through OnDocumentOpened/Closed. | ||
// There is a separate OnSourceGeneratedDocumentOpened/Closed method, but there is no need | ||
// for us to call it in LSP - it deals with mapping TextBuffers to text containers. | ||
return; |
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.
So I've forgotten, for regular documents, why are we opening/closing the documents? Is it just so other code can ask "what's open" from the workspace or are we still getting some other benefit?
return false; | ||
} | ||
|
||
var newState = existingState.WithText(text); |
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 forget, how expensive is this? If the state is different, can we actually use that later rather than doing it a second time?
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.
It doesn't seem to expensive (compares checksums, then copies over to a new Sg state. As far as I can tell any actual work is deferred - https://source.dot.net/#Microsoft.CodeAnalysis.Workspaces/Workspace/Solution/SourceGeneratedDocumentState.cs,c9182b28095a2b1c,references
if you'd prefer I can expose a version of WithFrozenSourceGeneratedDocuments
that takes in the sg states.
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.
Looked into this a little bit. Definitely possible to do with some small refactoring to WithFrozenSourceGeneratedDocuments
to allow us to pass an existing state. However I don't think it will make much of a difference for the following reasons
- If the state is the same, the only work we do is calculating the checksums, which we would do regardless
- If the state is different, there still isn't much work done - we create a
SourceGeneratedDocumentState
, but all the parsing and everything else is deferred in lazy's. - We create a max of one new state here if the text is different since this returns early as soon as we detect one difference.
Think it is fine to leave it like this, but let me know what you think
_trackedDocuments.Keys.Where(static uri => uri.Scheme == SourceGeneratedDocumentUri.Scheme) | ||
.Select(uri => (identity: SourceGeneratedDocumentUri.DeserializeIdentity(workspaceCurrentSolution, uri), _trackedDocuments[uri].Text)) | ||
.Where(tuple => tuple.identity.HasValue) | ||
.SelectAsArray(tuple => (tuple.identity!.Value, DateTime.Now, tuple.Text)); |
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.
@CyrusNajmabadi What is the time used for here, generally? Something complicated or in the VS for Windows case just updating the gold bar so the user knows when it was last updated?
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.
Think its just the gold bar. Right now I just picked a time because we need to pass it to WithFrozenSourceGeneratedDocuments
@@ -64,9 +64,9 @@ public Task<IReadOnlyList<SearchResultPreviewPanelBase>> GetPreviewPanelsAsync(S | |||
return null; | |||
|
|||
Uri? absoluteUri; | |||
if (document.IsSourceGeneratedDocument) | |||
if (document.SourceGeneratedDocumentIdentity is not null) |
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 we had to pass through the identity just so we can create a URI, why not just have the document object just hold onto the URI in the first place? Then we can delete this handling here and the try/catch in favor of a document.GetURI() when the object is created?
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.
the code creating the NavigableDocument is in features, which doesn't have any of the URI stuff from the LSP layer (see next comment)
{ | ||
absoluteUri = ProtocolConversions.CreateUriFromSourceGeneratedFilePath(filePath); | ||
absoluteUri = SourceGeneratedDocumentUri.Create(document.SourceGeneratedDocumentIdentity.Value); | ||
} | ||
else | ||
{ |
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.
And to be clear if we just pass a URI directly then we can delete this else block too. This was added as a part of #72442 which was a fine tactical fix but shouldn't be needed going forward, I'd imagine.
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.
Going to leave this as-is - the navigable document is created at the features layer which doesn't include any of the URI stuff we have at the protocol layer. So I believe the else is still needed here since we won't be using the GetUri extension on document.
I don't see a problem with moving some of the URI stuff down to features, but I think that is best left to a separate PR (a number of helpers would need to move along with it)
Going to merge this - addressed Cyrus' comments, and Jason won't be available for a bit. Can take additional feedback later. |
A new version of #68771
I was having a heck of a time with the conflicts, so I ended up just re-doing it (and copying changes over that I thought were relevant).
I believe I addressed the comments, however I still do have some questions.
Server side of dotnet/vscode-csharp#6426
Note - this does not yet implement support for refreshing the file on changes