-
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
Add LSP handler for copilot related documents #74918
Merged
CyrusNajmabadi
merged 13 commits into
dotnet:main
from
CyrusNajmabadi:relatedDocumentsHandler
Aug 28, 2024
Merged
Changes from 6 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
74e0537
Handler in progress
CyrusNajmabadi 18cea13
Flesh out
CyrusNajmabadi c05f9a5
Add tests
CyrusNajmabadi 79a4924
Merge remote-tracking branch 'upstream/main' into relatedDocumentsHan…
CyrusNajmabadi 686d17a
revert
CyrusNajmabadi 74d2fa5
Abs
CyrusNajmabadi a02589d
Merge remote-tracking branch 'upstream/main' into relatedDocumentsHan…
CyrusNajmabadi 348d5a8
Switch to early return
CyrusNajmabadi 1c27d2a
Switch to early return
CyrusNajmabadi 1ce5f40
tests
CyrusNajmabadi b86b464
tests
CyrusNajmabadi f0fed71
tests
CyrusNajmabadi aa5c2bc
tests
CyrusNajmabadi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
src/LanguageServer/Protocol/Handler/RelatedDocuments/RelatedDocumentsHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis.Host.Mef; | ||
using Microsoft.CodeAnalysis.RelatedDocuments; | ||
using Microsoft.CodeAnalysis.Serialization; | ||
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Microsoft.CommonLanguageServerProtocol.Framework; | ||
using Roslyn.LanguageServer.Protocol; | ||
using Roslyn.Utilities; | ||
|
||
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.RelatedDocuments; | ||
|
||
[ExportCSharpVisualBasicLspServiceFactory(typeof(RelatedDocumentsHandler)), Shared] | ||
[method: ImportingConstructor] | ||
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
internal sealed class RelatedDocumentsHandlerFactory() : ILspServiceFactory | ||
{ | ||
public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) | ||
=> new RelatedDocumentsHandler(); | ||
} | ||
|
||
[Method(VSInternalMethods.CopilotRelatedDocumentsName)] | ||
internal sealed class RelatedDocumentsHandler | ||
: ILspServiceRequestHandler<VSInternalRelatedDocumentParams, VSInternalRelatedDocumentReport[]?>, | ||
ITextDocumentIdentifierHandler<VSInternalRelatedDocumentParams, TextDocumentIdentifier> | ||
{ | ||
/// <summary> | ||
/// Cache where we store the data produced by prior requests so that they can be returned if nothing of significance | ||
/// changed. The version key is produced by combining the checksums for project options <see | ||
/// cref="ProjectState.GetParseOptionsChecksum"/> and <see cref="DocumentStateChecksums.Text"/> | ||
/// </summary> | ||
private readonly VersionedPullCache<(Checksum parseOptionsChecksum, Checksum textChecksum)?> _versionedCache = new(nameof(RelatedDocumentsHandler)); | ||
|
||
public bool MutatesSolutionState => false; | ||
public bool RequiresLSPSolution => true; | ||
|
||
private static async Task<(Checksum parseOptionsChecksum, Checksum textChecksum)> ComputeChecksumsAsync(Document document, CancellationToken cancellationToken) | ||
{ | ||
var project = document.Project; | ||
var parseOptionsChecksum = project.State.GetParseOptionsChecksum(); | ||
|
||
var documentChecksumState = await document.State.GetStateChecksumsAsync(cancellationToken).ConfigureAwait(false); | ||
var textChecksum = documentChecksumState.Text; | ||
|
||
return (parseOptionsChecksum, textChecksum); | ||
} | ||
|
||
public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalRelatedDocumentParams requestParams) | ||
=> requestParams.TextDocument; | ||
|
||
/// <summary> | ||
/// Retrieve the previous results we reported. Used so we can avoid resending data for unchanged files. | ||
/// </summary> | ||
private static ImmutableArray<PreviousPullResult>? GetPreviousResults(VSInternalRelatedDocumentParams requestParams) | ||
=> requestParams.PreviousResultId != null && requestParams.TextDocument != null | ||
? [new PreviousPullResult(requestParams.PreviousResultId, requestParams.TextDocument)] | ||
// The client didn't provide us with a previous result to look for, so we can't lookup anything. | ||
: null; | ||
|
||
public async Task<VSInternalRelatedDocumentReport[]?> HandleRequestAsync( | ||
VSInternalRelatedDocumentParams requestParams, RequestContext context, CancellationToken cancellationToken) | ||
{ | ||
context.TraceInformation($"{this.GetType()} started getting related documents"); | ||
|
||
// The progress object we will stream reports to. | ||
using var progress = BufferedProgress.Create(requestParams.PartialResultToken); | ||
|
||
context.TraceInformation($"PreviousResultId={requestParams.PreviousResultId}"); | ||
|
||
var solution = context.Solution; | ||
var document = context.Document; | ||
Contract.ThrowIfNull(solution); | ||
Contract.ThrowIfNull(document); | ||
|
||
context.TraceInformation($"Processing: {document.FilePath}"); | ||
|
||
var relatedDocumentsService = document.GetLanguageService<IRelatedDocumentsService>(); | ||
if (relatedDocumentsService == null) | ||
{ | ||
context.TraceInformation($"Ignoring document '{document.FilePath}' because it does not support related documents"); | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
else | ||
{ | ||
var documentToPreviousParams = new Dictionary<Document, PreviousPullResult>(); | ||
if (requestParams.PreviousResultId != null) | ||
documentToPreviousParams.Add(document, new PreviousPullResult(requestParams.PreviousResultId, requestParams.TextDocument)); | ||
|
||
var newResultId = await _versionedCache.GetNewResultIdAsync( | ||
documentToPreviousParams, | ||
document, | ||
computeVersionAsync: async () => await ComputeChecksumsAsync(document, cancellationToken).ConfigureAwait(false), | ||
cancellationToken).ConfigureAwait(false); | ||
if (newResultId != null) | ||
{ | ||
context.TraceInformation($"Version was changed for document: {document.FilePath}"); | ||
|
||
var linePosition = requestParams.Position is null | ||
? new LinePosition(0, 0) | ||
: ProtocolConversions.PositionToLinePosition(requestParams.Position); | ||
|
||
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); | ||
var position = text.Lines.GetPosition(linePosition); | ||
|
||
await relatedDocumentsService.GetRelatedDocumentIdsAsync( | ||
document, | ||
position, | ||
(relatedDocumentIds, cancellationToken) => | ||
{ | ||
// As the related docs services reports document ids to us, stream those immediately through our | ||
// progress reporter. | ||
progress.Report(new VSInternalRelatedDocumentReport | ||
{ | ||
ResultId = newResultId, | ||
FilePaths = relatedDocumentIds.Select(id => solution.GetRequiredDocument(id).FilePath).WhereNotNull().ToArray(), | ||
}); | ||
|
||
return ValueTaskFactory.CompletedTask; | ||
}, | ||
cancellationToken).ConfigureAwait(false); | ||
} | ||
else | ||
{ | ||
context.TraceInformation($"Version was unchanged for document: {document.FilePath}"); | ||
|
||
// Nothing changed between the last request and this one. Report a (null-file-paths, same-result-id) | ||
// response to the client as that means they should just preserve the current related file paths they | ||
// have for this file. | ||
progress.Report(new VSInternalRelatedDocumentReport { ResultId = requestParams.PreviousResultId }); | ||
} | ||
} | ||
|
||
// If we had a progress object, then we will have been reporting to that. Otherwise, take what we've been | ||
// collecting and return that. | ||
context.TraceInformation($"{this.GetType()} finished getting related documents"); | ||
return progress.GetFlattenedValues(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
src/LanguageServer/Protocol/Protocol/Internal/VSInternalRelatedDocumentParams.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
namespace Roslyn.LanguageServer.Protocol | ||
{ | ||
using System; | ||
using System.Text.Json.Serialization; | ||
|
||
/// <summary> | ||
/// Parameter for copilot/_related_documents. | ||
/// </summary> | ||
internal sealed class VSInternalRelatedDocumentParams : VSInternalStreamingParams, IPartialResultParams<VSInternalRelatedDocumentReport[]> | ||
{ | ||
/// <summary> | ||
/// Gets or sets the value which indicates the position within the document. | ||
/// </summary> | ||
[JsonPropertyName("position")] | ||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
public Position? Position { get; set; } | ||
|
||
/// <inheritdoc/> | ||
[JsonPropertyName(Methods.PartialResultTokenName)] | ||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
public IProgress<VSInternalRelatedDocumentReport[]>? PartialResultToken { get; set; } | ||
} | ||
|
||
internal sealed class VSInternalRelatedDocumentReport | ||
{ | ||
/// <summary> | ||
/// Gets or sets the server-generated version number for the related documents result. This is treated as a | ||
/// black box by the client: it is stored on the client for each textDocument and sent back to the server when | ||
/// requesting related documents. The server can use this result ID to avoid resending results | ||
/// that had previously been sent. | ||
/// </summary> | ||
[JsonPropertyName("_vs_resultId")] | ||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
public string? ResultId { get; set; } | ||
|
||
[JsonPropertyName("_vs_file_paths")] | ||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
public string[]? FilePaths { get; set; } | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
137 changes: 137 additions & 0 deletions
137
src/LanguageServer/ProtocolUnitTests/RelatedDocuments/RelatedDocumentsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis.LanguageServer.Handler; | ||
using Roslyn.LanguageServer.Protocol; | ||
using Roslyn.Test.Utilities; | ||
using Roslyn.Utilities; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
||
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RelatedDocuments; | ||
|
||
public sealed class RelatedDocumentsTests(ITestOutputHelper testOutputHelper) | ||
: AbstractLanguageServerProtocolTests(testOutputHelper) | ||
{ | ||
private static async Task<VSInternalRelatedDocumentReport[]> RunGetRelatedDocumentsAsync( | ||
TestLspServer testLspServer, | ||
Uri uri, | ||
string? previousResultId = null, | ||
bool useProgress = false) | ||
{ | ||
BufferedProgress<VSInternalRelatedDocumentReport[]>? progress = useProgress ? BufferedProgress.Create<VSInternalRelatedDocumentReport[]>(null) : null; | ||
var spans = await testLspServer.ExecuteRequestAsync<VSInternalRelatedDocumentParams, VSInternalRelatedDocumentReport[]>( | ||
VSInternalMethods.CopilotRelatedDocumentsName, | ||
new VSInternalRelatedDocumentParams | ||
{ | ||
TextDocument = new TextDocumentIdentifier { Uri = uri }, | ||
PreviousResultId = previousResultId, | ||
PartialResultToken = progress, | ||
}, | ||
CancellationToken.None).ConfigureAwait(false); | ||
|
||
if (useProgress) | ||
{ | ||
Assert.Null(spans); | ||
spans = progress!.Value.GetFlattenedValues(); | ||
} | ||
|
||
AssertEx.NotNull(spans); | ||
return spans; | ||
} | ||
|
||
[Theory, CombinatorialData] | ||
public async Task ReferenceNoDocuments(bool mutatingLspWorkspace, bool useProgress) | ||
{ | ||
var markup1 = """ | ||
class X | ||
{ | ||
} | ||
"""; | ||
|
||
var markup2 = """ | ||
class Y | ||
{ | ||
} | ||
"""; | ||
|
||
await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace); | ||
|
||
var project = testLspServer.TestWorkspace.CurrentSolution.Projects.Single(); | ||
var results = await RunGetRelatedDocumentsAsync( | ||
testLspServer, | ||
project.Documents.First().GetURI(), | ||
useProgress: useProgress); | ||
|
||
Assert.Equal(0, results.Length); | ||
} | ||
|
||
[Theory, CombinatorialData] | ||
public async Task ReferenceSingleOtherDocument(bool mutatingLspWorkspace, bool useProgress) | ||
{ | ||
var markup1 = """ | ||
class X | ||
{ | ||
Y y; | ||
} | ||
"""; | ||
|
||
var markup2 = """ | ||
class Y | ||
{ | ||
} | ||
"""; | ||
|
||
await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace); | ||
|
||
var project = testLspServer.TestWorkspace.CurrentSolution.Projects.Single(); | ||
var results = await RunGetRelatedDocumentsAsync( | ||
testLspServer, | ||
project.Documents.First().GetURI(), | ||
useProgress: useProgress); | ||
|
||
Assert.Equal(1, results.Length); | ||
Assert.Equal(project.Documents.Last().FilePath, results[0].FilePaths.Single()); | ||
} | ||
|
||
[Theory, CombinatorialData] | ||
public async Task ReferenceMultipleOtherDocument(bool mutatingLspWorkspace, bool useProgress) | ||
{ | ||
var markup1 = """ | ||
class X | ||
{ | ||
Y y; | ||
Z z; | ||
} | ||
"""; | ||
|
||
var markup2 = """ | ||
class Y | ||
{ | ||
} | ||
"""; | ||
|
||
var markup3 = """ | ||
class Z | ||
{ | ||
} | ||
"""; | ||
|
||
await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2, markup3], mutatingLspWorkspace); | ||
|
||
var project = testLspServer.TestWorkspace.CurrentSolution.Projects.Single(); | ||
var results = await RunGetRelatedDocumentsAsync( | ||
testLspServer, | ||
project.Documents.First().GetURI(), | ||
useProgress: useProgress); | ||
|
||
Assert.Equal(1, results.Length); | ||
Assert.Equal(2, results[0].FilePaths!.Length); | ||
AssertEx.SetEqual([.. project.Documents.Skip(1).Select(d => d.FilePath)], results[0].FilePaths); | ||
} | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 this endpoint invoked as part of a specific user action, or is the client polling us?
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.
client polling i believe.