Skip to content

Commit 8008e0c

Browse files
authored
Store client's version for open docs in LSP (#80064)
* Store client's version for open docs in LSP * Doc TrackedDocumentInfo
1 parent 0afebcf commit 8008e0c

File tree

9 files changed

+168
-47
lines changed

9 files changed

+168
-47
lines changed

src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ private async Task ProcessQueueAsync()
290290
var message = $"Error occurred processing queue: {ex.Message}.";
291291
if (lspServices is not null)
292292
{
293-
await _languageServer.ShutdownAsync("Error processing queue, shutting down").ConfigureAwait(false);
293+
await _languageServer.ShutdownAsync(message).ConfigureAwait(false);
294294
await _languageServer.ExitAsync().ConfigureAwait(false);
295295
}
296296

src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -359,9 +359,39 @@ private protected async Task<TestLspServer> CreateXmlTestLspServerAsync(
359359
return await TestLspServer.CreateAsync(workspace, lspOptions, TestOutputLspLogger);
360360
}
361361

362+
private void CheckForCompositionErrors(TestComposition composition)
363+
{
364+
// The test compositions tend to have a bunch of errors.
365+
// We only want to fail the test if we're seeing errors in relevant parts to the language server.
366+
// This isn't foolproof, but helps catch issues early.
367+
368+
var config = composition.GetCompositionConfiguration();
369+
var hasLanguageServerErrors = config.CompositionErrors.Flatten().Any(error => error.Parts.Any(IsRelevantPartError));
370+
371+
if (hasLanguageServerErrors)
372+
{
373+
try
374+
{
375+
config.ThrowOnErrors();
376+
}
377+
catch (CompositionFailedException ex)
378+
{
379+
// The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately
380+
this.TestOutputLspLogger.LogError($"Encountered errors in the MEF composition: {ex.Message}{Environment.NewLine}{ex.ErrorsAsString}");
381+
throw;
382+
}
383+
}
384+
385+
bool IsRelevantPartError(ComposedPart part)
386+
{
387+
return part.Definition.Type.FullName?.Contains("Microsoft.CodeAnalysis.LanguageServer") == true;
388+
}
389+
}
390+
362391
internal async Task<LspTestWorkspace> CreateWorkspaceAsync(
363392
InitializationOptions? options, string? workspaceKind, bool mutatingLspWorkspace, TestComposition? composition = null)
364393
{
394+
CheckForCompositionErrors(composition ?? Composition);
365395
var workspace = new LspTestWorkspace(
366396
composition?.ExportProviderFactory.CreateExportProvider() ?? await CreateExportProviderAsync(),
367397
workspaceKind,
@@ -494,7 +524,8 @@ private protected static LSP.Location GetLocationPlusOne(LSP.Location originalLo
494524

495525
private static LSP.DidChangeTextDocumentParams CreateDidChangeTextDocumentParams(
496526
DocumentUri documentUri,
497-
ImmutableArray<(LSP.Range Range, string Text)> changes)
527+
ImmutableArray<(LSP.Range Range, string Text)> changes,
528+
int version = 0)
498529
{
499530
var changeEvents = changes.Select(change => new LSP.TextDocumentContentChangeEvent
500531
{
@@ -506,20 +537,22 @@ private static LSP.DidChangeTextDocumentParams CreateDidChangeTextDocumentParams
506537
{
507538
TextDocument = new LSP.VersionedTextDocumentIdentifier
508539
{
509-
DocumentUri = documentUri
540+
DocumentUri = documentUri,
541+
Version = version
510542
},
511543
ContentChanges = changeEvents
512544
};
513545
}
514546

515-
private static LSP.DidOpenTextDocumentParams CreateDidOpenTextDocumentParams(DocumentUri uri, string source, string languageId = "")
547+
private static LSP.DidOpenTextDocumentParams CreateDidOpenTextDocumentParams(DocumentUri uri, string source, string languageId = "", int version = 0)
516548
=> new LSP.DidOpenTextDocumentParams
517549
{
518550
TextDocument = new LSP.TextDocumentItem
519551
{
520552
Text = source,
521553
DocumentUri = uri,
522-
LanguageId = languageId
554+
LanguageId = languageId,
555+
Version = version
523556
}
524557
};
525558

@@ -704,7 +737,7 @@ public Task ExecutePreSerializedRequestAsync(string methodName, JsonDocument ser
704737
return _clientRpc.InvokeWithParameterObjectAsync(methodName, serializedRequest);
705738
}
706739

707-
public async Task OpenDocumentAsync(DocumentUri documentUri, string? text = null, string languageId = "")
740+
public async Task OpenDocumentAsync(DocumentUri documentUri, string? text = null, string languageId = "", int version = 0)
708741
{
709742
if (text == null)
710743
{
@@ -714,13 +747,13 @@ public async Task OpenDocumentAsync(DocumentUri documentUri, string? text = null
714747
text = sourceText.ToString();
715748
}
716749

717-
var didOpenParams = CreateDidOpenTextDocumentParams(documentUri, text.ToString(), languageId);
750+
var didOpenParams = CreateDidOpenTextDocumentParams(documentUri, text.ToString(), languageId, version);
718751
await ExecuteRequestAsync<LSP.DidOpenTextDocumentParams, object>(LSP.Methods.TextDocumentDidOpenName, didOpenParams, CancellationToken.None);
719752
}
720753

721754
/// <summary>
722755
/// Opens a document in the workspace only, and waits for workspace operations.
723-
/// Use <see cref="OpenDocumentAsync(DocumentUri, string, string)"/> if the document should be opened in LSP"/>
756+
/// Use <see cref="OpenDocumentAsync(DocumentUri, string, string, int)"/> if the document should be opened in LSP"/>
724757
/// </summary>
725758
public async Task OpenDocumentInWorkspaceAsync(DocumentId documentId, bool openAllLinkedDocuments, SourceText? text = null)
726759
{
@@ -745,23 +778,34 @@ public async Task OpenDocumentInWorkspaceAsync(DocumentId documentId, bool openA
745778
await WaitForWorkspaceOperationsAsync(TestWorkspace);
746779
}
747780

748-
public Task ReplaceTextAsync(DocumentUri documentUri, params (LSP.Range Range, string Text)[] changes)
781+
public Task ReplaceTextAsync(DocumentUri documentUri, int version, params (LSP.Range Range, string Text)[] changes)
749782
{
750783
var didChangeParams = CreateDidChangeTextDocumentParams(
751784
documentUri,
752-
[.. changes]);
785+
[.. changes],
786+
version);
753787
return ExecuteRequestAsync<LSP.DidChangeTextDocumentParams, object>(LSP.Methods.TextDocumentDidChangeName, didChangeParams, CancellationToken.None);
754788
}
755789

756-
public Task InsertTextAsync(DocumentUri documentUri, params (int Line, int Column, string Text)[] changes)
790+
public Task ReplaceTextAsync(DocumentUri documentUri, params (LSP.Range Range, string Text)[] changes)
757791
{
758-
return ReplaceTextAsync(documentUri, [.. changes.Select(change => (new LSP.Range
792+
return ReplaceTextAsync(documentUri, version: 0, changes);
793+
}
794+
795+
public Task InsertTextAsync(DocumentUri documentUri, int version, params (int Line, int Column, string Text)[] changes)
796+
{
797+
return ReplaceTextAsync(documentUri, version, [.. changes.Select(change => (new LSP.Range
759798
{
760799
Start = new LSP.Position { Line = change.Line, Character = change.Column },
761800
End = new LSP.Position { Line = change.Line, Character = change.Column }
762801
}, change.Text))]);
763802
}
764803

804+
public Task InsertTextAsync(DocumentUri documentUri, params (int Line, int Column, string Text)[] changes)
805+
{
806+
return InsertTextAsync(documentUri, version: 0, changes);
807+
}
808+
765809
public Task DeleteTextAsync(DocumentUri documentUri, params (int StartLine, int StartColumn, int EndLine, int EndColumn)[] changes)
766810
{
767811
return ReplaceTextAsync(documentUri, [.. changes.Select(change => (new LSP.Range

src/LanguageServer/Protocol/Handler/DocumentChanges/DidChangeHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DidChangeTextDocumentPar
2828

2929
public Task<object?> HandleRequestAsync(DidChangeTextDocumentParams request, RequestContext context, CancellationToken cancellationToken)
3030
{
31-
var text = context.GetTrackedDocumentSourceText(request.TextDocument.DocumentUri);
31+
var text = context.GetTrackedDocumentInfo(request.TextDocument.DocumentUri).SourceText;
3232

3333
text = GetUpdatedSourceText(request.ContentChanges, text);
3434

35-
context.UpdateTrackedDocument(request.TextDocument.DocumentUri, text);
35+
context.UpdateTrackedDocument(request.TextDocument.DocumentUri, text, request.TextDocument.Version);
3636

3737
return SpecializedTasks.Default<object>();
3838
}

src/LanguageServer/Protocol/Handler/DocumentChanges/DidOpenHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@ public async Task HandleNotificationAsync(LSP.DidOpenTextDocumentParams request,
3535
// Create SourceText from binary representation of the document, retrieve encoding from the request and checksum algorithm from the project.
3636
var sourceText = SourceText.From(request.TextDocument.Text, System.Text.Encoding.UTF8, SourceHashAlgorithms.OpenDocumentChecksumAlgorithm);
3737

38-
await context.StartTrackingAsync(request.TextDocument.DocumentUri, sourceText, request.TextDocument.LanguageId, cancellationToken).ConfigureAwait(false);
38+
await context.StartTrackingAsync(request.TextDocument.DocumentUri, sourceText, request.TextDocument.LanguageId, request.TextDocument.Version, cancellationToken).ConfigureAwait(false);
3939
}
4040
}

src/LanguageServer/Protocol/Handler/IDocumentChangeTracker.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler;
1717
/// </summary>
1818
internal interface IDocumentChangeTracker
1919
{
20-
ValueTask StartTrackingAsync(DocumentUri documentUri, SourceText initialText, string languageId, CancellationToken cancellationToken);
21-
void UpdateTrackedDocument(DocumentUri documentUri, SourceText text);
20+
ValueTask StartTrackingAsync(DocumentUri documentUri, SourceText initialText, string languageId, int lspVersion, CancellationToken cancellationToken);
21+
void UpdateTrackedDocument(DocumentUri documentUri, SourceText text, int lspVersion);
2222
ValueTask StopTrackingAsync(DocumentUri documentUri, CancellationToken cancellationToken);
2323
}
2424

2525
internal sealed class NonMutatingDocumentChangeTracker : IDocumentChangeTracker
2626
{
27-
public ValueTask StartTrackingAsync(DocumentUri documentUri, SourceText initialText, string languageId, CancellationToken cancellationToken)
27+
public ValueTask StartTrackingAsync(DocumentUri documentUri, SourceText initialText, string languageId, int lspVersion, CancellationToken cancellationToken)
2828
{
2929
throw new InvalidOperationException("Mutating documents not allowed in a non-mutating request handler");
3030
}
@@ -34,7 +34,7 @@ public ValueTask StopTrackingAsync(DocumentUri documentUri, CancellationToken ca
3434
throw new InvalidOperationException("Mutating documents not allowed in a non-mutating request handler");
3535
}
3636

37-
public void UpdateTrackedDocument(DocumentUri documentUri, SourceText text)
37+
public void UpdateTrackedDocument(DocumentUri documentUri, SourceText text, int lspVersion)
3838
{
3939
throw new InvalidOperationException("Mutating documents not allowed in a non-mutating request handler");
4040
}

src/LanguageServer/Protocol/Handler/RequestContext.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ internal readonly struct RequestContext
4242
/// It contains text that is consistent with all prior LSP text sync notifications, but LSP text sync requests
4343
/// which are ordered after this one in the queue are not reflected here.
4444
/// </remarks>
45-
private readonly ImmutableDictionary<DocumentUri, (SourceText Text, string LanguageId)> _trackedDocuments;
45+
private readonly ImmutableDictionary<DocumentUri, TrackedDocumentInfo> _trackedDocuments;
4646

4747
private readonly ILspServices _lspServices;
4848

@@ -171,7 +171,7 @@ public RequestContext(
171171
WellKnownLspServerKinds serverKind,
172172
TextDocument? document,
173173
IDocumentChangeTracker documentChangeTracker,
174-
ImmutableDictionary<DocumentUri, (SourceText Text, string LanguageId)> trackedDocuments,
174+
ImmutableDictionary<DocumentUri, TrackedDocumentInfo> trackedDocuments,
175175
ImmutableArray<string> supportedLanguages,
176176
ILspServices lspServices,
177177
CancellationToken queueCancellationToken)
@@ -299,20 +299,20 @@ public static async Task<RequestContext> CreateAsync(
299299
/// Allows a mutating request to open a document and start it being tracked.
300300
/// Mutating requests are serialized by the execution queue in order to prevent concurrent access.
301301
/// </summary>
302-
public ValueTask StartTrackingAsync(DocumentUri uri, SourceText initialText, string languageId, CancellationToken cancellationToken)
303-
=> _documentChangeTracker.StartTrackingAsync(uri, initialText, languageId, cancellationToken);
302+
public ValueTask StartTrackingAsync(DocumentUri uri, SourceText initialText, string languageId, int lspVersion, CancellationToken cancellationToken)
303+
=> _documentChangeTracker.StartTrackingAsync(uri, initialText, languageId, lspVersion, cancellationToken);
304304

305305
/// <summary>
306306
/// Allows a mutating request to update the contents of a tracked document.
307307
/// Mutating requests are serialized by the execution queue in order to prevent concurrent access.
308308
/// </summary>
309-
public void UpdateTrackedDocument(DocumentUri uri, SourceText changedText)
310-
=> _documentChangeTracker.UpdateTrackedDocument(uri, changedText);
309+
public void UpdateTrackedDocument(DocumentUri uri, SourceText changedText, int lspVersion)
310+
=> _documentChangeTracker.UpdateTrackedDocument(uri, changedText, lspVersion);
311311

312-
public SourceText GetTrackedDocumentSourceText(DocumentUri documentUri)
312+
public TrackedDocumentInfo GetTrackedDocumentInfo(DocumentUri documentUri)
313313
{
314314
Contract.ThrowIfFalse(_trackedDocuments.ContainsKey(documentUri), $"Attempted to get text for {documentUri} which is not open.");
315-
return _trackedDocuments[documentUri].Text;
315+
return _trackedDocuments[documentUri];
316316
}
317317

318318
/// <summary>

0 commit comments

Comments
 (0)