Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ protected override ValueTask<ImmutableArray<IDiagnosticSource>> GetOrderedDiagno
//
// Only consider open documents here (and only closed ones in the WorkspacePullDiagnosticHandler). Each
// handler treats those as separate worlds that they are responsible for.
var textDocument = context.TextDocument;
if (textDocument is null)
var identifier = GetTextDocumentIdentifier(diagnosticsParams);
if (identifier is null || context.Document is null)
{
context.TraceDebug("Ignoring diagnostics request because no text document was provided");
return new([]);
}
Copy link
Member

Choose a reason for hiding this comment

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

consider breaking this into two checks for better tra ing.


if (!context.IsTracking(textDocument.GetURI()))
Copy link
Member Author

@dibarbet dibarbet Jul 9, 2025

Choose a reason for hiding this comment

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

this was the issue - for non-unc paths it does a case sensitive comparison, so the URI created from the file path on disk did not match the opened URI (vscode normalizes it).

instead we use the request uri which per the LSP spec must be consistent with the open URI

if (!context.IsTracking(identifier.DocumentUri))
{
context.TraceWarning($"Ignoring diagnostics request for untracked document: {textDocument.GetURI()}");
context.TraceWarning($"Ignoring diagnostics request for untracked document: {identifier.DocumentUri}");
return new([]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,7 @@ internal abstract class AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvide

public ValueTask<ImmutableArray<IDiagnosticSource>> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken)
{
if (context.GetTrackedDocument<TextDocument>() is { } document)
{
return new([new DocumentDiagnosticSource(kind, document)]);
}

return new([]);
return new([new DocumentDiagnosticSource(kind, context.GetRequiredDocument())]);
Copy link
Member Author

@dibarbet dibarbet Jul 9, 2025

Choose a reason for hiding this comment

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

the AbstractDocumentPullDiagnosticHandler.cs code above already verifies that the document is tracked. No need to do it again.

}

[Export(typeof(IDiagnosticSourceProvider)), Shared]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ internal sealed class PublicDocumentNonLocalDiagnosticSourceProvider(
public ValueTask<ImmutableArray<IDiagnosticSource>> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken)
{
// Non-local document diagnostics are reported only when full solution analysis is enabled for analyzer execution.
if (context.GetTrackedDocument<TextDocument>() is { } textDocument &&
globalOptions.GetBackgroundAnalysisScope(textDocument.Project.Language) == BackgroundAnalysisScope.FullSolution)
if (globalOptions.GetBackgroundAnalysisScope(context.GetRequiredDocument().Project.Language) == BackgroundAnalysisScope.FullSolution)
{
// NOTE: Compiler does not report any non-local diagnostics, so we bail out for compiler analyzer.
return new([new NonLocalDocumentDiagnosticSource(textDocument, a => !a.IsCompilerAnalyzer())]);
return new([new NonLocalDocumentDiagnosticSource(context.GetRequiredDocument(), a => !a.IsCompilerAnalyzer())]);
}

return new([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ internal sealed class DocumentEditAndContinueDiagnosticSourceProvider() : IDiagn

public ValueTask<ImmutableArray<IDiagnosticSource>> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken)
{
if (context.GetTrackedDocument<Document>() is { } document)
{
return new([EditAndContinueDiagnosticSource.CreateOpenDocumentSource(document)]);
}

return new([]);
return new([EditAndContinueDiagnosticSource.CreateOpenDocumentSource(context.GetRequiredDocument())]);
}
}
23 changes: 0 additions & 23 deletions src/LanguageServer/Protocol/Handler/RequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,29 +320,6 @@ public SourceText GetTrackedDocumentSourceText(DocumentUri documentUri)
return _trackedDocuments[documentUri].Text;
}

public TDocument? GetTrackedDocument<TDocument>() where TDocument : TextDocument
{
// Note: context.Document may be null in the case where the client is asking about a document that we have
// since removed from the workspace. In this case, we don't really have anything to process.
// GetPreviousResults will be used to properly realize this and notify the client that the doc is gone.
//
// Only consider open documents here (and only closed ones in the WorkspacePullDiagnosticHandler). Each
// handler treats those as separate worlds that they are responsible for.
if (TextDocument is not TDocument document)
{
TraceDebug($"Ignoring diagnostics request because no {typeof(TDocument).Name} was provided");
return null;
}

if (!IsTracking(document.GetURI()))
{
TraceDebug($"Ignoring diagnostics request for untracked document: {document.GetURI()}");
return null;
}

return document;
}

/// <summary>
/// Allows a mutating request to close a document and stop it being tracked.
/// Mutating requests are serialized by the execution queue in order to prevent concurrent access.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,7 @@ internal sealed class DocumentTaskDiagnosticSourceProvider([Import] IGlobalOptio

public ValueTask<ImmutableArray<IDiagnosticSource>> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken)
{
if (context.GetTrackedDocument<Document>() is { } document)
{
return new([new TaskListDiagnosticSource(document, globalOptions)]);
}

return new([]);
return new([new TaskListDiagnosticSource(context.GetRequiredDocument(), globalOptions)]);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,37 @@ class A
Assert.Equal(LSP.DiagnosticSeverity.Information, results.Single().Diagnostics!.Single().Severity);
}

[ConditionalTheory(typeof(UnixLikeOnly)), CombinatorialData]
public async Task TestDocumentDiagnosticsWhenClientRequestsWithDifferentCasing(bool useVSDiagnostics, bool mutatingLspWorkspace)
{
var markup = @"class A {";

var workspaceXml =
$"""
<Workspace>
<Project Language="C#" CommonReferences="true" AssemblyName="CSProj1">
<Document FilePath="/Users/ConsoleApp1/Class1.cs">{markup}</Document>
</Project>
</Workspace>
""";

await using var testLspServer = await CreateTestWorkspaceFromXmlAsync(workspaceXml, mutatingLspWorkspace, BackgroundAnalysisScope.OpenFiles, useVSDiagnostics);

var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();

var loweredUri = ProtocolConversions.CreateAbsoluteDocumentUri(document.FilePath!.ToLowerInvariant());
await testLspServer.OpenDocumentAsync(loweredUri);

var results = await RunGetDocumentPullDiagnosticsAsync(
testLspServer, loweredUri, useVSDiagnostics);

// When looking up the document based on a URI, the workspace ignores casing differences and can open the document with different casing.
// When diagnostics checks if the document is open, it should also be able to tell that the document is open, even though the URI casing is different from the file path.

Assert.Equal("CS1513", results.Single().Diagnostics!.Single().Code);
Assert.NotNull(results.Single().Diagnostics!.Single().CodeDescription!.Href.ParsedUri);
}

#endregion

#region Workspace Diagnostics
Expand Down
Loading