diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/AbstractVSTypeScriptRequestHandler.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/AbstractVSTypeScriptRequestHandler.cs index bb128f92430dd..46b559222fb7c 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/AbstractVSTypeScriptRequestHandler.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/Api/AbstractVSTypeScriptRequestHandler.cs @@ -31,7 +31,7 @@ internal abstract class AbstractVSTypeScriptRequestHandler @@ -159,7 +160,7 @@ private protected static int CompareLocations(LSP.Location? l1, LSP.Location? l2 if (l2 is null) return 1; - var compareDocument = l1.Uri.AbsoluteUri.CompareTo(l2.Uri.AbsoluteUri); + var compareDocument = l1.Uri.UriString.CompareTo(l2.Uri.UriString); var compareRange = CompareRange(l1.Range, l2.Range); return compareDocument != 0 ? compareDocument : compareRange; } @@ -206,7 +207,7 @@ internal static LSP.SymbolInformation CreateSymbolInformation(LSP.SymbolKind kin return info; } - private protected static LSP.TextDocumentIdentifier CreateTextDocumentIdentifier(Uri uri, ProjectId? projectContext = null) + private protected static LSP.TextDocumentIdentifier CreateTextDocumentIdentifier(DocumentUri uri, ProjectId? projectContext = null) { var documentIdentifier = new LSP.VSTextDocumentIdentifier { Uri = uri }; @@ -474,7 +475,7 @@ protected static async Task RemoveGeneratorAsync(AnalyzerReference reference, Ed return locations; - static LSP.Location ConvertTextSpanWithTextToLocation(TextSpan span, SourceText text, Uri documentUri) + static LSP.Location ConvertTextSpanWithTextToLocation(TextSpan span, SourceText text, DocumentUri documentUri) { var location = new LSP.Location { @@ -500,7 +501,7 @@ private static string GetDocumentFilePathFromName(string documentName) => "C:\\" + documentName; private static LSP.DidChangeTextDocumentParams CreateDidChangeTextDocumentParams( - Uri documentUri, + DocumentUri documentUri, ImmutableArray<(LSP.Range Range, string Text)> changes) { var changeEvents = changes.Select(change => new LSP.TextDocumentContentChangeEvent @@ -519,7 +520,7 @@ private static LSP.DidChangeTextDocumentParams CreateDidChangeTextDocumentParams }; } - private static LSP.DidOpenTextDocumentParams CreateDidOpenTextDocumentParams(Uri uri, string source, string languageId = "") + private static LSP.DidOpenTextDocumentParams CreateDidOpenTextDocumentParams(DocumentUri uri, string source, string languageId = "") => new LSP.DidOpenTextDocumentParams { TextDocument = new LSP.TextDocumentItem @@ -530,7 +531,7 @@ private static LSP.DidOpenTextDocumentParams CreateDidOpenTextDocumentParams(Uri } }; - private static LSP.DidCloseTextDocumentParams CreateDidCloseTextDocumentParams(Uri uri) + private static LSP.DidCloseTextDocumentParams CreateDidCloseTextDocumentParams(DocumentUri uri) => new LSP.DidCloseTextDocumentParams() { TextDocument = new LSP.TextDocumentIdentifier @@ -650,14 +651,14 @@ private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Str return languageServer; } - public async Task GetDocumentAsync(Uri uri) + public async Task GetDocumentAsync(DocumentUri uri) { var document = await GetCurrentSolution().GetDocumentAsync(new LSP.TextDocumentIdentifier { Uri = uri }, CancellationToken.None).ConfigureAwait(false); Contract.ThrowIfNull(document, $"Unable to find document with {uri} in solution"); return document; } - public async Task GetDocumentTextAsync(Uri uri) + public async Task GetDocumentTextAsync(DocumentUri uri) { var document = await GetDocumentAsync(uri).ConfigureAwait(false); return await document.GetTextAsync(CancellationToken.None).ConfigureAwait(false); @@ -692,7 +693,7 @@ public Task ExecutePreSerializedRequestAsync(string methodName, JsonDocument ser return _clientRpc.InvokeWithParameterObjectAsync(methodName, serializedRequest); } - public async Task OpenDocumentAsync(Uri documentUri, string? text = null, string languageId = "") + public async Task OpenDocumentAsync(DocumentUri documentUri, string? text = null, string languageId = "") { if (text == null) { @@ -706,7 +707,7 @@ public async Task OpenDocumentAsync(Uri documentUri, string? text = null, string await ExecuteRequestAsync(LSP.Methods.TextDocumentDidOpenName, didOpenParams, CancellationToken.None); } - public Task ReplaceTextAsync(Uri documentUri, params (LSP.Range Range, string Text)[] changes) + public Task ReplaceTextAsync(DocumentUri documentUri, params (LSP.Range Range, string Text)[] changes) { var didChangeParams = CreateDidChangeTextDocumentParams( documentUri, @@ -714,7 +715,7 @@ public Task ReplaceTextAsync(Uri documentUri, params (LSP.Range Range, string Te return ExecuteRequestAsync(LSP.Methods.TextDocumentDidChangeName, didChangeParams, CancellationToken.None); } - public Task InsertTextAsync(Uri documentUri, params (int Line, int Column, string Text)[] changes) + public Task InsertTextAsync(DocumentUri documentUri, params (int Line, int Column, string Text)[] changes) { return ReplaceTextAsync(documentUri, [.. changes.Select(change => (new LSP.Range { @@ -723,7 +724,7 @@ public Task InsertTextAsync(Uri documentUri, params (int Line, int Column, strin }, change.Text))]); } - public Task DeleteTextAsync(Uri documentUri, params (int StartLine, int StartColumn, int EndLine, int EndColumn)[] changes) + public Task DeleteTextAsync(DocumentUri documentUri, params (int StartLine, int StartColumn, int EndLine, int EndColumn)[] changes) { return ReplaceTextAsync(documentUri, [.. changes.Select(change => (new LSP.Range { @@ -732,7 +733,7 @@ public Task DeleteTextAsync(Uri documentUri, params (int StartLine, int StartCol }, string.Empty))]); } - public Task CloseDocumentAsync(Uri documentUri) + public Task CloseDocumentAsync(DocumentUri documentUri) { var didCloseParams = CreateDidCloseTextDocumentParams(documentUri); return ExecuteRequestAsync(LSP.Methods.TextDocumentDidCloseName, didCloseParams, CancellationToken.None); @@ -790,7 +791,7 @@ internal async Task WaitForDiagnosticsAsync() internal T GetRequiredLspService() where T : class, ILspService => LanguageServer.GetTestAccessor().GetRequiredLspService(); - internal ImmutableArray GetTrackedTexts() => [.. GetManager().GetTrackedLspText().Values.Select(v => v.Text)]; + internal ImmutableArray GetTrackedTexts() => [.. GetManager().GetTrackedLspText().Values.Select(v => v.SourceText)]; internal Task RunCodeAnalysisAsync(ProjectId? projectId) => _codeAnalysisService.RunAnalysisAsync(GetCurrentSolution(), projectId, onAfterProjectAnalyzed: _ => { }, CancellationToken.None); diff --git a/src/Features/Lsif/Generator/Generator.cs b/src/Features/Lsif/Generator/Generator.cs index 354e412cf2d71..5d25333aba60b 100644 --- a/src/Features/Lsif/Generator/Generator.cs +++ b/src/Features/Lsif/Generator/Generator.cs @@ -212,7 +212,7 @@ public async Task GenerateForProjectAsync( var contentBase64Encoded = await GetBase64EncodedContentAsync(document, cancellationToken); - var documentVertex = new Graph.LsifDocument(document.GetURI(), GetLanguageKind(semanticModel.Language), contentBase64Encoded, idFactory); + var documentVertex = new Graph.LsifDocument(document.GetURI().GetRequiredParsedUri(), GetLanguageKind(semanticModel.Language), contentBase64Encoded, idFactory); lsifJsonWriter.Write(documentVertex); lsifJsonWriter.Write(new Event(Event.EventKind.Begin, documentVertex.GetId(), idFactory)); diff --git a/src/Features/Lsif/GeneratorTest/ProjectStructureTests.vb b/src/Features/Lsif/GeneratorTest/ProjectStructureTests.vb index 3b7f729aaf3f5..07854f60ea5c3 100644 --- a/src/Features/Lsif/GeneratorTest/ProjectStructureTests.vb +++ b/src/Features/Lsif/GeneratorTest/ProjectStructureTests.vb @@ -76,7 +76,7 @@ Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests Await TestLsifOutput.GenerateForWorkspaceAsync(workspace, New LineModeLsifJsonWriter(stringWriter)) Dim generatedDocument = Assert.Single(Await workspace.CurrentSolution.Projects.Single().GetSourceGeneratedDocumentsAsync()) - Dim uri = SourceGeneratedDocumentUri.Create(generatedDocument.Identity) + Dim uri = SourceGeneratedDocumentUri.Create(generatedDocument.Identity).GetRequiredParsedUri() Dim outputText = stringWriter.ToString() Assert.Contains(uri.AbsoluteUri, outputText) End Function diff --git a/src/LanguageServer/ExternalAccess/CompilerDeveloperSDK/Handler/AbstractCompilerDeveloperSdkLspServiceDocumentRequestHandler.cs b/src/LanguageServer/ExternalAccess/CompilerDeveloperSDK/Handler/AbstractCompilerDeveloperSdkLspServiceDocumentRequestHandler.cs index 1d7899ee3ce37..8a730ba669fb0 100644 --- a/src/LanguageServer/ExternalAccess/CompilerDeveloperSDK/Handler/AbstractCompilerDeveloperSdkLspServiceDocumentRequestHandler.cs +++ b/src/LanguageServer/ExternalAccess/CompilerDeveloperSDK/Handler/AbstractCompilerDeveloperSdkLspServiceDocumentRequestHandler.cs @@ -22,7 +22,7 @@ internal abstract class AbstractCompilerDeveloperSdkLspServiceDocumentRequestHan bool ISolutionRequiredHandler.RequiresLSPSolution => RequiresLSPSolution; TextDocumentIdentifier ITextDocumentIdentifierHandler.GetTextDocumentIdentifier(TRequest request) - => new() { Uri = GetTextDocumentIdentifier(request) }; + => new() { Uri = new(GetTextDocumentIdentifier(request)) }; Task IRequestHandler.HandleRequestAsync(TRequest request, LspRequestContext context, CancellationToken cancellationToken) => HandleRequestAsync(request, new RequestContext(context), cancellationToken); } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/LspFileChangeWatcherTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/LspFileChangeWatcherTests.cs index 900cf1a438c89..ba46122a93977 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/LspFileChangeWatcherTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/LspFileChangeWatcherTests.cs @@ -62,7 +62,7 @@ public async Task CreatingDirectoryWatchRequestsDirectoryWatch() var watcher = GetSingleFileWatcher(dynamicCapabilitiesRpcTarget); - Assert.Equal(tempDirectory.Path, watcher.GlobPattern.Second.BaseUri.Second.LocalPath); + Assert.Equal(tempDirectory.Path, watcher.GlobPattern.Second.BaseUri.Second.GetRequiredParsedUri().LocalPath); Assert.Equal("**/*", watcher.GlobPattern.Second.Pattern); // Get rid of the registration and it should be gone again @@ -93,7 +93,7 @@ public async Task CreatingFileWatchRequestsFileWatch() var watcher = GetSingleFileWatcher(dynamicCapabilitiesRpcTarget); - Assert.Equal("Z:\\", watcher.GlobPattern.Second.BaseUri.Second.LocalPath); + Assert.Equal("Z:\\", watcher.GlobPattern.Second.BaseUri.Second.GetRequiredParsedUri().LocalPath); Assert.Equal("SingleFile.txt", watcher.GlobPattern.Second.Pattern); // Get rid of the registration and it should be gone again diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ServerInitializationTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ServerInitializationTests.cs index c66971d44d5c0..2264a03f0cf11 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ServerInitializationTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ServerInitializationTests.cs @@ -18,7 +18,7 @@ public ServerInitializationTests(ITestOutputHelper testOutputHelper) : base(test public async Task TestServerHandlesTextSyncRequestsAsync() { await using var server = await CreateLanguageServerAsync(); - var document = new VersionedTextDocumentIdentifier { Uri = ProtocolConversions.CreateAbsoluteUri("C:\\\ue25b\ud86d\udeac.cs") }; + var document = new VersionedTextDocumentIdentifier { Uri = ProtocolConversions.CreateAbsoluteDocumentUri("C:\\\ue25b\ud86d\udeac.cs") }; var response = await server.ExecuteRequestAsync(Methods.TextDocumentDidOpenName, new DidOpenTextDocumentParams { TextDocument = new TextDocumentItem diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs index c21b47a23e803..e8f469f8ad7aa 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs @@ -104,7 +104,7 @@ private void WatchedFilesHandler_OnNotificationRaised(object? sender, DidChangeW { foreach (var changedFile in e.Changes) { - var filePath = changedFile.Uri.LocalPath; + var filePath = changedFile.Uri.GetRequiredParsedUri().LocalPath; // Unfortunately the LSP protocol doesn't give us any hint of which of the file watches we might have sent to the client // was the one that registered for this change, so we have to check paths to see if this one we should respond to. @@ -152,7 +152,7 @@ public IWatchedFile EnqueueWatchingFile(string filePath) // TODO: figure out how I just can do an absolute path watch GlobPattern = new RelativePattern { - BaseUri = ProtocolConversions.CreateAbsoluteUri(Path.GetDirectoryName(filePath)!), + BaseUri = ProtocolConversions.CreateAbsoluteDocumentUri(Path.GetDirectoryName(filePath)!), Pattern = Path.GetFileName(filePath) } }; diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileChangedHandler.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileChangedHandler.cs index 48c1c3fa8595f..0cbacfb133105 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileChangedHandler.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileChangedHandler.cs @@ -5,6 +5,7 @@ using System.Composition; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.Razor; @@ -27,7 +28,8 @@ public RazorDynamicFileChangedHandler(RazorDynamicFileInfoProvider razorDynamicF public Task HandleNotificationAsync(RazorDynamicFileChangedParams request, RequestContext requestContext, CancellationToken cancellationToken) { - var filePath = ProtocolConversions.GetDocumentFilePathFromUri(request.RazorDocument.Uri); + var parsedUri = request.RazorDocument.Uri.GetRequiredParsedUri(); + var filePath = ProtocolConversions.GetDocumentFilePathFromUri(parsedUri); _razorDynamicFileInfoProvider.Update(filePath); return Task.CompletedTask; } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.TextChangesTextLoader.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.TextChangesTextLoader.cs index 0f62d58b1d221..57647dff70919 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.TextChangesTextLoader.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.TextChangesTextLoader.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.Razor; @@ -18,14 +19,14 @@ private sealed class TextChangesTextLoader( byte[] checksum, SourceHashAlgorithm checksumAlgorithm, int? codePage, - Uri razorUri) : TextLoader + DocumentUri razorUri) : TextLoader { private readonly TextDocument? _document = document; private readonly IEnumerable _updates = updates; private readonly byte[] _checksum = checksum; private readonly SourceHashAlgorithm _checksumAlgorithm = checksumAlgorithm; private readonly int? _codePage = codePage; - private readonly Uri _razorUri = razorUri; + private readonly DocumentUri _razorUri = razorUri; private readonly Lazy _emptySourceText = new Lazy(() => { diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.cs index b064fb31132c3..cbc34022c4cf2 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.Razor; @@ -55,7 +56,7 @@ public void Update(string filePath) { _razorWorkspaceListenerInitializer.Value.NotifyDynamicFile(projectId); - var razorUri = ProtocolConversions.CreateAbsoluteUri(filePath); + var razorUri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); var requestParams = new RazorProvideDynamicFileParams { RazorDocument = new() @@ -77,7 +78,8 @@ public void Update(string filePath) // Since we only sent one file over, we should get either zero or one URI back var responseUri = response.CSharpDocument.Uri; - var dynamicFileInfoFilePath = ProtocolConversions.GetDocumentFilePathFromUri(responseUri); + var parsedUri = responseUri.GetRequiredParsedUri(); + var dynamicFileInfoFilePath = ProtocolConversions.GetDocumentFilePathFromUri(parsedUri); if (response.Updates is not null) { @@ -113,7 +115,7 @@ public Task RemoveDynamicFileInfoAsync(ProjectId projectId, string? projectFileP { CSharpDocument = new() { - Uri = ProtocolConversions.CreateAbsoluteUri(filePath) + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath) } }; diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationHandler.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationHandler.cs index a04adfd9c4bda..52e3845ae942e 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationHandler.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationHandler.cs @@ -4,6 +4,7 @@ using System.Composition; using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; @@ -38,9 +39,9 @@ public Task HandleRequestAsync(WorkspaceDebugConfig return Task.FromResult(projects); } - private static bool IsProjectInWorkspace(Uri workspacePath, Project project) + private static bool IsProjectInWorkspace(DocumentUri workspacePath, Project project) { - return PathUtilities.IsSameDirectoryOrChildOf(project.FilePath!, workspacePath.LocalPath); + return PathUtilities.IsSameDirectoryOrChildOf(project.FilePath!, workspacePath.GetRequiredParsedUri().LocalPath); } private ProjectDebugConfiguration GetProjectDebugConfiguration(Project project) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationParams.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationParams.cs index ac806d4fe897b..33bb843dc842c 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationParams.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationParams.cs @@ -8,4 +8,4 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; internal record WorkspaceDebugConfigurationParams( - [property: JsonPropertyName("workspacePath"), JsonConverter(typeof(DocumentUriConverter))] Uri WorkspacePath); + [property: JsonPropertyName("workspacePath"), JsonConverter(typeof(DocumentUriConverter))] DocumentUri WorkspacePath); diff --git a/src/LanguageServer/Protocol/Extensions/Extensions.cs b/src/LanguageServer/Protocol/Extensions/Extensions.cs index adc965d671f6f..bdb2cad3fd85b 100644 --- a/src/LanguageServer/Protocol/Extensions/Extensions.cs +++ b/src/LanguageServer/Protocol/Extensions/Extensions.cs @@ -23,19 +23,19 @@ namespace Microsoft.CodeAnalysis.LanguageServer { internal static partial class Extensions { - public static Uri GetURI(this TextDocument document) + public static DocumentUri GetURI(this TextDocument document) { Contract.ThrowIfNull(document.FilePath); return document is SourceGeneratedDocument sourceGeneratedDocument ? SourceGeneratedDocumentUri.Create(sourceGeneratedDocument.Identity) - : ProtocolConversions.CreateAbsoluteUri(document.FilePath); + : ProtocolConversions.CreateAbsoluteDocumentUri(document.FilePath); } /// /// Generate the Uri of a document by replace the name in file path using the document's name. /// Used to generate the correct Uri when rename a document, because calling doesn't update the file path. /// - public static Uri GetUriForRenamedDocument(this TextDocument document) + public static DocumentUri GetUriForRenamedDocument(this TextDocument document) { Contract.ThrowIfNull(document.FilePath); Contract.ThrowIfNull(document.Name); @@ -44,10 +44,10 @@ public static Uri GetUriForRenamedDocument(this TextDocument document) Contract.ThrowIfNull(directoryName); var path = Path.Combine(directoryName, document.Name); - return ProtocolConversions.CreateAbsoluteUri(path); + return ProtocolConversions.CreateAbsoluteDocumentUri(path); } - public static Uri CreateUriForDocumentWithoutFilePath(this TextDocument document) + public static DocumentUri CreateUriForDocumentWithoutFilePath(this TextDocument document) { Contract.ThrowIfNull(document.Name); Contract.ThrowIfNull(document.Project.FilePath); @@ -55,14 +55,20 @@ public static Uri CreateUriForDocumentWithoutFilePath(this TextDocument document var projectDirectoryName = Path.GetDirectoryName(document.Project.FilePath); Contract.ThrowIfNull(projectDirectoryName); var path = Path.Combine([projectDirectoryName, .. document.Folders, document.Name]); - return ProtocolConversions.CreateAbsoluteUri(path); + return ProtocolConversions.CreateAbsoluteDocumentUri(path); + } + + public static Uri GetRequiredParsedUri(this DocumentUri documentUri) + { + Contract.ThrowIfNull(documentUri.ParsedUri, $"URI {documentUri} could not be parsed"); + return documentUri.ParsedUri; } /// /// Get all regular and additional s for the given . /// This will not return source generated documents. /// - public static ImmutableArray GetTextDocuments(this Solution solution, Uri documentUri) + public static ImmutableArray GetTextDocuments(this Solution solution, DocumentUri documentUri) { var documentIds = GetDocumentIds(solution, documentUri); @@ -73,16 +79,23 @@ public static ImmutableArray GetTextDocuments(this Solution soluti return documents; } - public static ImmutableArray GetDocumentIds(this Solution solution, Uri documentUri) + public static ImmutableArray GetDocumentIds(this Solution solution, DocumentUri documentUri) { + if (documentUri.ParsedUri is null) + { + // If we were given an unparse-able URI, just search for documents with the full URI string. + // For example the miscellaneous workspace stores these kinds of documents with the full URI string. + return solution.GetDocumentIdsWithFilePath(documentUri.UriString); + } + // If this is not our special scheme for generated documents, then we can just look for documents with that file path. - if (documentUri.Scheme != SourceGeneratedDocumentUri.Scheme) - return solution.GetDocumentIdsWithFilePath(ProtocolConversions.GetDocumentFilePathFromUri(documentUri)); + if (documentUri.ParsedUri.Scheme != SourceGeneratedDocumentUri.Scheme) + return solution.GetDocumentIdsWithFilePath(ProtocolConversions.GetDocumentFilePathFromUri(documentUri.ParsedUri)); // We can get a null documentId if we were unable to find the project associated with the // generated document - this can happen if say a project is unloaded. There may be LSP requests // already in-flight which may ask for a generated document from that project. So we return null - var documentId = SourceGeneratedDocumentUri.DeserializeIdentity(solution, documentUri)?.DocumentId; + var documentId = SourceGeneratedDocumentUri.DeserializeIdentity(solution, documentUri.ParsedUri)?.DocumentId; return documentId is not null ? [documentId] : []; } @@ -103,7 +116,7 @@ public static ImmutableArray GetDocumentIds(this Solution solution, public static async ValueTask GetTextDocumentAsync(this Solution solution, TextDocumentIdentifier documentIdentifier, CancellationToken cancellationToken) { // If it's the URI scheme for source generated files, delegate to our other helper, otherwise we can handle anything else here. - if (documentIdentifier.Uri.Scheme == SourceGeneratedDocumentUri.Scheme) + if (documentIdentifier.Uri.ParsedUri?.Scheme == SourceGeneratedDocumentUri.Scheme) { // In the case of a URI scheme for source generated files, we generate a different URI for each project, thus this URI cannot be linked into multiple projects; // this means we can safely call .SingleOrDefault() and not worry about calling FindDocumentInProjectContext. @@ -164,7 +177,13 @@ public static T FindDocumentInProjectContext(this ImmutableArray documents public static Project? GetProject(this Solution solution, TextDocumentIdentifier projectIdentifier) { - var projects = solution.Projects.Where(project => project.FilePath == projectIdentifier.Uri.LocalPath).ToImmutableArray(); + // We need to parse the URI (scheme, file path) to be able to lookup the URI in the solution. + if (projectIdentifier.Uri.ParsedUri is null) + { + return null; + } + + var projects = solution.Projects.Where(project => project.FilePath == projectIdentifier.Uri.ParsedUri.LocalPath).ToImmutableArray(); return !projects.Any() ? null : FindItemInProjectContext(projects, projectIdentifier, projectIdGetter: (item) => item.Id, defaultGetter: () => projects[0]); diff --git a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.Diagnostics.cs b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.Diagnostics.cs index 9d084b8076d00..2f09f98c9235f 100644 --- a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.Diagnostics.cs +++ b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.Diagnostics.cs @@ -82,7 +82,7 @@ internal static partial class ProtocolConversions Location = new LSP.Location { Range = GetRange(l), - Uri = ProtocolConversions.CreateAbsoluteUri(l.UnmappedFileSpan.Path) + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(l.UnmappedFileSpan.Path) }, Message = diagnostic.Message }).ToArray(); diff --git a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index d4a1eda80a6cb..3080e76318c3c 100644 --- a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; @@ -23,6 +24,7 @@ using Microsoft.CodeAnalysis.SpellCheck; using Microsoft.CodeAnalysis.Tags; using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; using Roslyn.Text.Adornments; using Roslyn.Utilities; using Logger = Microsoft.CodeAnalysis.Internal.Log.Logger; @@ -106,6 +108,7 @@ public static JsonSerializerOptions AddLspSerializerOptions(this JsonSerializerO { LSP.VSInternalExtensionUtilities.AddVSInternalExtensionConverters(options); options.Converters.Add(new LSP.NaturalObjectConverter()); + options.Converters.Add(new DocumentUriConverter()); return options; } @@ -188,6 +191,7 @@ public static string GetDocumentFilePathFromUri(Uri uri) /// /// Converts an absolute local file path or an absolute URL string to . + /// For use with callers that require specifically a /// /// /// The can't be represented as . @@ -211,7 +215,20 @@ public static Uri CreateAbsoluteUri(string absolutePath) } } - internal static Uri CreateRelativePatternBaseUri(string path) + /// + /// Converts an absolute local file path or an absolute URL string to . + /// For use with callers (generally LSP) that require + /// + /// + /// The can't be represented as . + /// For example, UNC paths with invalid characters in server name. + /// + public static DocumentUri CreateAbsoluteDocumentUri(string absolutePath) + { + return new(CreateAbsoluteUri(absolutePath)); + } + + internal static DocumentUri CreateRelativePatternBaseUri(string path) { // According to VSCode LSP RelativePattern spec, // found at https://github.com/microsoft/vscode/blob/9e1974682eb84eebb073d4ae775bad1738c281f6/src/vscode-dts/vscode.d.ts#L2226 @@ -224,7 +241,7 @@ internal static Uri CreateRelativePatternBaseUri(string path) Debug.Assert(!path.Split(System.IO.Path.DirectorySeparatorChar).Any(p => p == "." || p == "..")); - return CreateAbsoluteUri(path); + return CreateAbsoluteDocumentUri(path); } // Implements workaround for https://github.com/dotnet/runtime/issues/89538: @@ -376,7 +393,7 @@ public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text) public static async Task ChangedDocumentsToTextDocumentEditsAsync(IEnumerable changedDocuments, Func getNewDocumentFunc, Func getOldDocumentFunc, IDocumentTextDifferencingService? textDiffService, CancellationToken cancellationToken) where T : TextDocument { - using var _ = ArrayBuilder<(Uri Uri, LSP.TextEdit TextEdit)>.GetInstance(out var uriToTextEdits); + using var _ = ArrayBuilder<(DocumentUri Uri, LSP.TextEdit TextEdit)>.GetInstance(out var uriToTextEdits); foreach (var docId in changedDocuments) { @@ -419,7 +436,7 @@ public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text) var textChange = textChanges[i]; if (!mappedSpan.IsDefault) { - uriToTextEdits.Add((CreateAbsoluteUri(mappedSpan.FilePath), new LSP.TextEdit + uriToTextEdits.Add((CreateAbsoluteDocumentUri(mappedSpan.FilePath), new LSP.TextEdit { Range = MappedSpanResultToRange(mappedSpan), NewText = textChange.NewText ?? string.Empty @@ -464,17 +481,17 @@ public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text) if (mappedSpan.IsDefault) return await ConvertTextSpanToLocationAsync(document, textSpan, isStale, cancellationToken).ConfigureAwait(false); - Uri? uri = null; + DocumentUri? uri = null; try { if (PathUtilities.IsAbsolute(mappedSpan.FilePath)) - uri = CreateAbsoluteUri(mappedSpan.FilePath); + uri = CreateAbsoluteDocumentUri(mappedSpan.FilePath); } catch (UriFormatException) { } - if (uri == null) + if (uri is null) { context?.TraceInformation($"Could not convert '{mappedSpan.FilePath}' to uri"); return null; @@ -508,7 +525,7 @@ public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text) return ConvertTextSpanWithTextToLocation(span, text, uri); } - static LSP.Location ConvertTextSpanWithTextToLocation(TextSpan span, SourceText text, Uri documentUri) + static LSP.Location ConvertTextSpanWithTextToLocation(TextSpan span, SourceText text, DocumentUri documentUri) { var location = new LSP.Location { @@ -521,7 +538,9 @@ static LSP.Location ConvertTextSpanWithTextToLocation(TextSpan span, SourceText } public static LSP.CodeDescription? HelpLinkToCodeDescription(Uri? uri) - => (uri != null) ? new LSP.CodeDescription { Href = uri } : null; + { + return (uri != null) ? new LSP.CodeDescription { Href = new(uri) } : null; + } public static LSP.SymbolKind NavigateToKindToSymbolKind(string kind) { diff --git a/src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs b/src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs index c42bc1002eae8..45382429ca582 100644 --- a/src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs +++ b/src/LanguageServer/Protocol/Extensions/SourceGeneratedDocumentUri.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; using System.Linq; +using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer; @@ -30,7 +31,7 @@ internal static class SourceGeneratedDocumentUri private const string AssemblyPathParam = "assemblyPath"; private const string TypeNameParam = "typeName"; - public static Uri Create(SourceGeneratedDocumentIdentity identity) + public static DocumentUri Create(SourceGeneratedDocumentIdentity identity) { var hintPath = Uri.EscapeDataString(identity.HintName); var projectId = identity.DocumentId.ProjectId.Id.ToString(GuidFormat); @@ -46,7 +47,7 @@ public static Uri Create(SourceGeneratedDocumentIdentity identity) if (identity.Generator.AssemblyPath != null) uri += $"&{AssemblyPathParam}={Uri.EscapeDataString(identity.Generator.AssemblyPath)}"; - return ProtocolConversions.CreateAbsoluteUri(uri); + return ProtocolConversions.CreateAbsoluteDocumentUri(uri); } public static SourceGeneratedDocumentIdentity? DeserializeIdentity(Solution solution, Uri documentUri) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/FormatNewFileHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/FormatNewFileHandler.cs index fb8bdf4533e68..5a91057b1de6b 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/FormatNewFileHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/FormatNewFileHandler.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.RemoveUnnecessaryImports; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.ExternalAccess.Razor; @@ -45,8 +46,10 @@ public FormatNewFileHandler(IGlobalOptionService globalOptions) return null; } + var parsedUri = request.Document.Uri.GetRequiredParsedUri(); + // Create a document in-memory to represent the file Razor wants to add - var filePath = ProtocolConversions.GetDocumentFilePathFromUri(request.Document.Uri); + var filePath = ProtocolConversions.GetDocumentFilePathFromUri(parsedUri); var source = SourceText.From(request.Contents); var fileLoader = new SourceTextLoader(source, filePath); var documentId = DocumentId.CreateNewId(project.Id); diff --git a/src/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs b/src/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs index 5eec735c049bd..d97bfe16d3905 100644 --- a/src/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs +++ b/src/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs @@ -20,7 +20,7 @@ internal abstract class AbstractRefreshQueue : ILspService, IDisposable { - private AsyncBatchingWorkQueue? _refreshQueue; + private AsyncBatchingWorkQueue? _refreshQueue; private readonly LspWorkspaceManager _lspWorkspaceManager; private readonly IClientLanguageServerManager _notificationManager; @@ -63,11 +63,10 @@ public void Initialize(ClientCapabilities clientCapabilities) // sending too many notifications at once. This ensures we batch up workspace notifications, // but also means we send soon enough after a compilation-computation to not make the user wait // an enormous amount of time. - _refreshQueue = new AsyncBatchingWorkQueue( + _refreshQueue = new AsyncBatchingWorkQueue( delay: TimeSpan.FromMilliseconds(2000), processBatchAsync: (documentUris, cancellationToken) => FilterLspTrackedDocumentsAsync(_lspWorkspaceManager, _notificationManager, documentUris, cancellationToken), - equalityComparer: EqualityComparer.Default, asyncListener: _asyncListener, _disposalTokenSource.Token); _isQueueCreated = true; @@ -94,7 +93,7 @@ protected virtual void OnLspSolutionChanged(object? sender, WorkspaceChangeEvent } } - protected void EnqueueRefreshNotification(Uri? documentUri) + protected void EnqueueRefreshNotification(DocumentUri? documentUri) { if (_isQueueCreated) { @@ -106,7 +105,7 @@ protected void EnqueueRefreshNotification(Uri? documentUri) private ValueTask FilterLspTrackedDocumentsAsync( LspWorkspaceManager lspWorkspaceManager, IClientLanguageServerManager notificationManager, - ImmutableSegmentedList documentUris, + ImmutableSegmentedList documentUris, CancellationToken cancellationToken) { var trackedDocuments = lspWorkspaceManager.GetTrackedLspText(); diff --git a/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHelper.cs b/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHelper.cs index 88b7247a1cd16..e2316519a2e73 100644 --- a/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHelper.cs +++ b/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHelper.cs @@ -225,7 +225,7 @@ async Task AddTextDocumentAdditionsAsync( var newTextDoc = getNewDocument(docId); Contract.ThrowIfNull(newTextDoc); - Uri? uri = null; + DocumentUri? uri = null; if (newTextDoc.FilePath != null) { uri = newTextDoc.GetURI(); diff --git a/src/LanguageServer/Protocol/Handler/Definitions/AbstractGoToDefinitionHandler.cs b/src/LanguageServer/Protocol/Handler/Definitions/AbstractGoToDefinitionHandler.cs index a0c753ff3ff9d..41d4f1328fcc7 100644 --- a/src/LanguageServer/Protocol/Handler/Definitions/AbstractGoToDefinitionHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Definitions/AbstractGoToDefinitionHandler.cs @@ -88,7 +88,7 @@ await definition.Document.GetRequiredDocumentAsync(document.Project.Solution, ca var linePosSpan = declarationFile.IdentifierLocation.GetLineSpan().Span; locations.Add(new LSP.Location { - Uri = ProtocolConversions.CreateAbsoluteUri(declarationFile.FilePath), + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(declarationFile.FilePath), Range = ProtocolConversions.LinePositionToRange(linePosSpan), }); } diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs index ac49b0a9da7c6..8ec99dcb33984 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs @@ -29,7 +29,7 @@ public static AbstractProjectDiagnosticSource CreateForCodeAnalysisDiagnostics(P public Project GetProject() => Project; public TextDocumentIdentifier? GetDocumentIdentifier() => !string.IsNullOrEmpty(Project.FilePath) - ? new VSTextDocumentIdentifier { ProjectContext = ProtocolConversions.ProjectToProjectContext(Project), Uri = ProtocolConversions.CreateAbsoluteUri(Project.FilePath) } + ? new VSTextDocumentIdentifier { ProjectContext = ProtocolConversions.ProjectToProjectContext(Project), Uri = ProtocolConversions.CreateAbsoluteDocumentUri(Project.FilePath) } : null; public string ToDisplayString() => Project.Name; diff --git a/src/LanguageServer/Protocol/Handler/DocumentChanges/DidOpenHandler.cs b/src/LanguageServer/Protocol/Handler/DocumentChanges/DidOpenHandler.cs index b75b6c9793e95..0da9e13562664 100644 --- a/src/LanguageServer/Protocol/Handler/DocumentChanges/DidOpenHandler.cs +++ b/src/LanguageServer/Protocol/Handler/DocumentChanges/DidOpenHandler.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.DocumentChanges { [ExportCSharpVisualBasicStatelessLspService(typeof(DidOpenHandler)), Shared] [Method(LSP.Methods.TextDocumentDidOpenName)] - internal class DidOpenHandler : ILspServiceNotificationHandler, ITextDocumentIdentifierHandler + internal class DidOpenHandler : ILspServiceNotificationHandler, ITextDocumentIdentifierHandler { [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] @@ -26,7 +26,7 @@ public DidOpenHandler() public bool MutatesSolutionState => true; public bool RequiresLSPSolution => false; - public Uri GetTextDocumentIdentifier(LSP.DidOpenTextDocumentParams request) => request.TextDocument.Uri; + public LSP.TextDocumentItem GetTextDocumentIdentifier(LSP.DidOpenTextDocumentParams request) => request.TextDocument; public async Task HandleNotificationAsync(LSP.DidOpenTextDocumentParams request, RequestContext context, CancellationToken cancellationToken) { diff --git a/src/LanguageServer/Protocol/Handler/IDocumentChangeTracker.cs b/src/LanguageServer/Protocol/Handler/IDocumentChangeTracker.cs index ff2410d5277d7..86520e74f6bde 100644 --- a/src/LanguageServer/Protocol/Handler/IDocumentChangeTracker.cs +++ b/src/LanguageServer/Protocol/Handler/IDocumentChangeTracker.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageServer.Handler.DocumentChanges; using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler; @@ -16,24 +17,24 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler; /// internal interface IDocumentChangeTracker { - ValueTask StartTrackingAsync(Uri documentUri, SourceText initialText, string languageId, CancellationToken cancellationToken); - void UpdateTrackedDocument(Uri documentUri, SourceText text); - ValueTask StopTrackingAsync(Uri documentUri, CancellationToken cancellationToken); + ValueTask StartTrackingAsync(DocumentUri documentUri, SourceText initialText, string languageId, CancellationToken cancellationToken); + void UpdateTrackedDocument(DocumentUri documentUri, SourceText text); + ValueTask StopTrackingAsync(DocumentUri documentUri, CancellationToken cancellationToken); } internal class NonMutatingDocumentChangeTracker : IDocumentChangeTracker { - public ValueTask StartTrackingAsync(Uri documentUri, SourceText initialText, string languageId, CancellationToken cancellationToken) + public ValueTask StartTrackingAsync(DocumentUri documentUri, SourceText initialText, string languageId, CancellationToken cancellationToken) { throw new InvalidOperationException("Mutating documents not allowed in a non-mutating request handler"); } - public ValueTask StopTrackingAsync(Uri documentUri, CancellationToken cancellationToken) + public ValueTask StopTrackingAsync(DocumentUri documentUri, CancellationToken cancellationToken) { throw new InvalidOperationException("Mutating documents not allowed in a non-mutating request handler"); } - public void UpdateTrackedDocument(Uri documentUri, SourceText text) + public void UpdateTrackedDocument(DocumentUri documentUri, SourceText text) { throw new InvalidOperationException("Mutating documents not allowed in a non-mutating request handler"); } diff --git a/src/LanguageServer/Protocol/Handler/MapCode/MapCodeHandler.cs b/src/LanguageServer/Protocol/Handler/MapCode/MapCodeHandler.cs index a404c848ff4ec..c0ba90e0d56c2 100644 --- a/src/LanguageServer/Protocol/Handler/MapCode/MapCodeHandler.cs +++ b/src/LanguageServer/Protocol/Handler/MapCode/MapCodeHandler.cs @@ -42,12 +42,12 @@ public MapCodeHandler() throw new NotImplementedException("mapCode Request failed: additional workspace 'Update' is currently not supported"); } - using var _ = PooledDictionary.GetInstance(out var uriToEditsMap); + using var _ = PooledDictionary.GetInstance(out var uriToEditsMap); foreach (var codeMapping in request.Mappings) { var mappingResult = await MapCodeAsync(codeMapping).ConfigureAwait(false); - if (mappingResult is not (Uri uri, LSP.TextEdit[] textEdits)) + if (mappingResult is not (DocumentUri uri, LSP.TextEdit[] textEdits)) { // Failed the entire request if any of the sub-requests failed return null; @@ -73,11 +73,11 @@ public MapCodeHandler() { return new WorkspaceEdit { - Changes = uriToEditsMap.ToDictionary(kvp => ProtocolConversions.GetDocumentFilePathFromUri(kvp.Key), kvp => kvp.Value) + Changes = uriToEditsMap.ToDictionary(kvp => ProtocolConversions.GetDocumentFilePathFromUri(kvp.Key.GetRequiredParsedUri()), kvp => kvp.Value) }; } - async Task<(Uri, LSP.TextEdit[])?> MapCodeAsync(LSP.VSInternalMapCodeMapping codeMapping) + async Task<(DocumentUri, LSP.TextEdit[])?> MapCodeAsync(LSP.VSInternalMapCodeMapping codeMapping) { var textDocument = codeMapping.TextDocument ?? throw new ArgumentException($"mapCode sub-request failed: MapCodeMapping.TextDocument not expected to be null."); diff --git a/src/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs b/src/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs index 4196abef13bbe..b030a18049b78 100644 --- a/src/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs +++ b/src/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs @@ -217,7 +217,7 @@ public override async ValueTask OnReferencesFoundAsync(IAsyncEnumerable - private readonly ImmutableDictionary _trackedDocuments; + private readonly ImmutableDictionary _trackedDocuments; private readonly ILspServices _lspServices; @@ -176,7 +176,7 @@ public RequestContext( WellKnownLspServerKinds serverKind, TextDocument? document, IDocumentChangeTracker documentChangeTracker, - ImmutableDictionary trackedDocuments, + ImmutableDictionary trackedDocuments, ImmutableArray supportedLanguages, ILspServices lspServices, CancellationToken queueCancellationToken) @@ -304,17 +304,17 @@ public static async Task CreateAsync( /// Allows a mutating request to open a document and start it being tracked. /// Mutating requests are serialized by the execution queue in order to prevent concurrent access. /// - public ValueTask StartTrackingAsync(Uri uri, SourceText initialText, string languageId, CancellationToken cancellationToken) + public ValueTask StartTrackingAsync(DocumentUri uri, SourceText initialText, string languageId, CancellationToken cancellationToken) => _documentChangeTracker.StartTrackingAsync(uri, initialText, languageId, cancellationToken); /// /// Allows a mutating request to update the contents of a tracked document. /// Mutating requests are serialized by the execution queue in order to prevent concurrent access. /// - public void UpdateTrackedDocument(Uri uri, SourceText changedText) + public void UpdateTrackedDocument(DocumentUri uri, SourceText changedText) => _documentChangeTracker.UpdateTrackedDocument(uri, changedText); - public SourceText GetTrackedDocumentSourceText(Uri documentUri) + public SourceText GetTrackedDocumentSourceText(DocumentUri documentUri) { Contract.ThrowIfFalse(_trackedDocuments.ContainsKey(documentUri), $"Attempted to get text for {documentUri} which is not open."); return _trackedDocuments[documentUri].Text; @@ -347,10 +347,10 @@ public SourceText GetTrackedDocumentSourceText(Uri documentUri) /// 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. /// - public ValueTask StopTrackingAsync(Uri uri, CancellationToken cancellationToken) + public ValueTask StopTrackingAsync(DocumentUri uri, CancellationToken cancellationToken) => _documentChangeTracker.StopTrackingAsync(uri, cancellationToken); - public bool IsTracking(Uri documentUri) + public bool IsTracking(DocumentUri documentUri) => _trackedDocuments.ContainsKey(documentUri); public void ClearSolutionContext() diff --git a/src/LanguageServer/Protocol/Handler/RequestContextFactory.cs b/src/LanguageServer/Protocol/Handler/RequestContextFactory.cs index d23deddb0bc00..9e68282dced72 100644 --- a/src/LanguageServer/Protocol/Handler/RequestContextFactory.cs +++ b/src/LanguageServer/Protocol/Handler/RequestContextFactory.cs @@ -41,12 +41,12 @@ public override Task CreateRequestContextAsync(IQ { textDocumentIdentifier = nullHandler.GetTextDocumentIdentifier(requestParam); } - else if (textDocumentIdentifierHandler is ITextDocumentIdentifierHandler uHandler) + else if (textDocumentIdentifierHandler is ITextDocumentIdentifierHandler uHandler) { - var uri = uHandler.GetTextDocumentIdentifier(requestParam); + var textDocumentItem = uHandler.GetTextDocumentIdentifier(requestParam); textDocumentIdentifier = new TextDocumentIdentifier { - Uri = uri, + Uri = textDocumentItem.Uri, }; } else if (textDocumentIdentifierHandler is null) diff --git a/src/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRefreshQueue.cs b/src/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRefreshQueue.cs index af4f7fe83d7a5..8f20c255d78f4 100644 --- a/src/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRefreshQueue.cs +++ b/src/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensRefreshQueue.cs @@ -62,7 +62,7 @@ public async Task TryEnqueueRefreshComputationAsync(Project project, Cancellatio protected override void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e) { - Uri? documentUri = null; + DocumentUri? documentUri = null; if (e.DocumentId is not null) { diff --git a/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratorRefreshQueue.cs b/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratorRefreshQueue.cs index f1e315fc18004..9ade93237811a 100644 --- a/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratorRefreshQueue.cs +++ b/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratorRefreshQueue.cs @@ -122,7 +122,7 @@ await newProject.GetDependentVersionAsync(_disposalTokenSource.Token).ConfigureA private ValueTask RefreshSourceGeneratedDocumentsAsync( CancellationToken cancellationToken) { - var hasOpenSourceGeneratedDocuments = _lspWorkspaceManager.GetTrackedLspText().Keys.Any(uri => uri.Scheme == SourceGeneratedDocumentUri.Scheme); + var hasOpenSourceGeneratedDocuments = _lspWorkspaceManager.GetTrackedLspText().Keys.Any(uri => uri.ParsedUri?.Scheme == SourceGeneratedDocumentUri.Scheme); if (!hasOpenSourceGeneratedDocuments) { // There are no opened source generated documents - we don't need to bother asking the client to refresh anything. diff --git a/src/LanguageServer/Protocol/ILanguageInfoProvider.cs b/src/LanguageServer/Protocol/ILanguageInfoProvider.cs index 315e6ed6e22fb..5186176c672f2 100644 --- a/src/LanguageServer/Protocol/ILanguageInfoProvider.cs +++ b/src/LanguageServer/Protocol/ILanguageInfoProvider.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.Features.Workspaces; +using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer { @@ -20,6 +21,6 @@ internal interface ILanguageInfoProvider : ILspService /// It is totally possible to not find language based on the file path (e.g. a newly created file that hasn't been saved to disk). /// In that case, we use the language Id that the LSP client gave us. /// - bool TryGetLanguageInformation(Uri uri, string? lspLanguageId, [NotNullWhen(true)] out LanguageInformation? languageInformation); + bool TryGetLanguageInformation(DocumentUri uri, string? lspLanguageId, [NotNullWhen(true)] out LanguageInformation? languageInformation); } } diff --git a/src/LanguageServer/Protocol/LanguageInfoProvider.cs b/src/LanguageServer/Protocol/LanguageInfoProvider.cs index 56e9c506aa809..5290dff6006af 100644 --- a/src/LanguageServer/Protocol/LanguageInfoProvider.cs +++ b/src/LanguageServer/Protocol/LanguageInfoProvider.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using Microsoft.CodeAnalysis.Features.Workspaces; +using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer { @@ -44,13 +45,13 @@ internal class LanguageInfoProvider : ILanguageInfoProvider { ".mts", s_typeScriptLanguageInformation }, }; - public bool TryGetLanguageInformation(Uri uri, string? lspLanguageId, [NotNullWhen(true)] out LanguageInformation? languageInformation) + public bool TryGetLanguageInformation(DocumentUri requestUri, string? lspLanguageId, [NotNullWhen(true)] out LanguageInformation? languageInformation) { // First try to get language information from the URI path. // We can do this for File uris and absolute uris. We use local path to get the value without any query parameters. - if (uri.IsFile || uri.IsAbsoluteUri) + if (requestUri.ParsedUri is not null && (requestUri.ParsedUri.IsFile || requestUri.ParsedUri.IsAbsoluteUri)) { - var localPath = uri.LocalPath; + var localPath = requestUri.ParsedUri.LocalPath; var extension = Path.GetExtension(localPath); if (s_extensionToLanguageInformation.TryGetValue(extension, out languageInformation)) { diff --git a/src/LanguageServer/Protocol/Protocol/CodeDescription.cs b/src/LanguageServer/Protocol/Protocol/CodeDescription.cs index 4ff525a766ad9..88654c7fda7b5 100644 --- a/src/LanguageServer/Protocol/Protocol/CodeDescription.cs +++ b/src/LanguageServer/Protocol/Protocol/CodeDescription.cs @@ -21,7 +21,7 @@ internal class CodeDescription : IEquatable /// [JsonPropertyName("href")] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Href + public DocumentUri Href { get; set; @@ -70,7 +70,7 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - return this.Href == null ? 53 : this.Href.GetHashCode(); + return this.Href is null ? 53 : this.Href.GetHashCode(); } } } diff --git a/src/LanguageServer/Protocol/Protocol/ConfigurationItem.cs b/src/LanguageServer/Protocol/Protocol/ConfigurationItem.cs index f97fcd9a3652c..b30297969cded 100644 --- a/src/LanguageServer/Protocol/Protocol/ConfigurationItem.cs +++ b/src/LanguageServer/Protocol/Protocol/ConfigurationItem.cs @@ -21,7 +21,7 @@ internal class ConfigurationItem [JsonPropertyName("scopeUri")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonConverter(typeof(DocumentUriConverter))] - public Uri? ScopeUri + public DocumentUri? ScopeUri { get; set; diff --git a/src/LanguageServer/Protocol/Protocol/Converters/DocumentUriConverter.cs b/src/LanguageServer/Protocol/Protocol/Converters/DocumentUriConverter.cs index 4a91a4cacbde4..f5807c7bab1f0 100644 --- a/src/LanguageServer/Protocol/Protocol/Converters/DocumentUriConverter.cs +++ b/src/LanguageServer/Protocol/Protocol/Converters/DocumentUriConverter.cs @@ -9,13 +9,16 @@ namespace Roslyn.LanguageServer.Protocol; /// -/// TODO: document. +/// Converts the LSP spec URI string into our custom wrapper for URI strings. +/// We do not convert directly to as it is unable to handle +/// certain valid RFC spec URIs. We do not want serialization / deserialization to fail if we cannot parse the URI. +/// See https://github.com/dotnet/runtime/issues/64707 /// -internal class DocumentUriConverter : JsonConverter +internal class DocumentUriConverter : JsonConverter { - public override Uri Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DocumentUri Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => new(reader.GetString()); - public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) - => writer.WriteStringValue(value.AbsoluteUri); + public override void Write(Utf8JsonWriter writer, DocumentUri value, JsonSerializerOptions options) + => writer.WriteStringValue(value.UriString); } diff --git a/src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs b/src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs index a3879a6b50449..39b86df161931 100644 --- a/src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs +++ b/src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs @@ -65,6 +65,7 @@ public SumTypeInfoCache(Type sumTypeType) if (parameterTypeInfo.IsPrimitive || parameterTypeInfo == typeof(string) || + parameterTypeInfo == typeof(DocumentUri) || parameterTypeInfo == typeof(Uri) || typeof(IStringEnum).IsAssignableFrom(parameterTypeInfo)) { @@ -261,7 +262,12 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions var sumValue = value.Value; // behavior from DocumentUriConverter - if (sumValue is Uri) + if (sumValue is DocumentUri documentUri) + { + writer.WriteStringValue(documentUri.UriString); + return; + } + else if (sumValue is Uri) { writer.WriteStringValue(sumValue.ToString()); return; @@ -297,6 +303,7 @@ private static bool IsTokenCompatibleWithType(ref Utf8JsonReader reader, SumConv case JsonTokenType.String: isCompatible = unionTypeInfo.Type == typeof(string) || unionTypeInfo.Type == typeof(Uri) || + unionTypeInfo.Type == typeof(DocumentUri) || typeof(IStringEnum).IsAssignableFrom(unionTypeInfo.Type); break; } diff --git a/src/LanguageServer/Protocol/Protocol/CreateFile.cs b/src/LanguageServer/Protocol/Protocol/CreateFile.cs index 2ca8e1a7e3a8c..20ca04744c668 100644 --- a/src/LanguageServer/Protocol/Protocol/CreateFile.cs +++ b/src/LanguageServer/Protocol/Protocol/CreateFile.cs @@ -30,7 +30,7 @@ internal class CreateFile : IAnnotatedChange [JsonPropertyName("uri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri + public DocumentUri Uri { get; set; diff --git a/src/LanguageServer/Protocol/Protocol/DeleteFile.cs b/src/LanguageServer/Protocol/Protocol/DeleteFile.cs index 72cae2338387d..98e766d9b8cdb 100644 --- a/src/LanguageServer/Protocol/Protocol/DeleteFile.cs +++ b/src/LanguageServer/Protocol/Protocol/DeleteFile.cs @@ -30,7 +30,7 @@ internal class DeleteFile : IAnnotatedChange [JsonPropertyName("uri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri + public DocumentUri Uri { get; set; diff --git a/src/LanguageServer/Protocol/Protocol/DocumentLink.cs b/src/LanguageServer/Protocol/Protocol/DocumentLink.cs index f060bec505004..d44479a463fad 100644 --- a/src/LanguageServer/Protocol/Protocol/DocumentLink.cs +++ b/src/LanguageServer/Protocol/Protocol/DocumentLink.cs @@ -32,7 +32,7 @@ public Range Range [JsonPropertyName("target")] [JsonConverter(typeof(DocumentUriConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public Uri? Target + public DocumentUri? Target { get; set; diff --git a/src/LanguageServer/Protocol/Protocol/DocumentUri.cs b/src/LanguageServer/Protocol/Protocol/DocumentUri.cs new file mode 100644 index 0000000000000..cf38fe388330d --- /dev/null +++ b/src/LanguageServer/Protocol/Protocol/DocumentUri.cs @@ -0,0 +1,139 @@ +// 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.Diagnostics.CodeAnalysis; + +namespace Roslyn.LanguageServer.Protocol; + +/// +/// Datatype used to hold URI strings for LSP message serialization. For details on how URIs are communicated in LSP, +/// see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#uri +/// +/// +/// While .NET has a type represent URIs (System.Uri), we do not want to use this type directly in +/// serialization and deserialization. System.Uri does full parsing and validation on the URI upfront, so +/// any issues in the uri format will cause deserialization to fail and bypass any of our error recovery. +/// +/// Compounding this problem, System.Uri will fail to parse various RFC spec valid URIs. +/// In order to gracefully handle these issues, we defer the parsing of the URI until someone +/// actually asks for it (and can handle the failure). +/// +internal sealed class DocumentUri : IEquatable +{ + private readonly Lazy _parsedUriLazy; + + public DocumentUri(string uriString) + { + UriString = uriString; + _parsedUriLazy = new(() => ParseUri(uriString)); + } + + public DocumentUri(Uri parsedUri) + { + UriString = parsedUri.AbsoluteUri; + _parsedUriLazy = new(() => parsedUri); + } + + public string UriString { get; } + + /// + /// Gets the parsed System.Uri for the URI string. + /// + /// + /// Null if the URI string is not parse-able with System.Uri. + /// + /// + /// Invalid RFC spec URI strings are not parse-able as so will return null here. + /// However, System.Uri can also fail to parse certain valid RFC spec URI strings. + /// + /// For example, any URI containing a 'sub-delims' character in the host name + /// is a valid RFC spec URI, but will fail with System.Uri + /// + public Uri? ParsedUri => _parsedUriLazy.Value; + + private static Uri? ParseUri(string uriString) + { + try + { + return new Uri(uriString); + } + catch (UriFormatException) + { + // This is not a URI that System.Uri can handle. + return null; + } + } + + public override string ToString() => UriString; + + public override bool Equals([NotNullWhen(true)] object? obj) => obj is DocumentUri other && this.Equals(other); + + public bool Equals(DocumentUri otherUri) + { + // 99% of the time the equivalent URIs will have equivalent URI strings, as the client is expected to be consistent in how it sends the URIs to the server, + // either always encoded or always unencoded. + // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#uri + if (this.UriString == otherUri.UriString) + { + return true; + } + + // If either of the URIs cannot be parsed + if (otherUri.ParsedUri is null || this.ParsedUri is null) + { + // Bail if we cannot parse either of the URIs. We already determined the URI strings are not equal + // and we need to be able to parse the URIs to do deeper equivalency checks. + return false; + } + + // Next we compare the parsed URIs to handle various casing and encoding scenarios (for example - different schemes may handle casing differently). + + // Uri.Equals will not always consider a percent encoded URI equal to an unencoded URI, even if they point to the same resource. + // As above, the client is supposed to be consistent in which kind of URI they send. + // + // However, there are rare cases where we are comparing an unencoded URI to an encoded URI and should consider them + // equivalent if they point to the same file path. + // For example - say the client generally sends us the unencoded URI. When we serialize URIs back to the client, we always serialize the AbsoluteUri property (see FromUri). + // The AbsoluteUri property is *always* percent encoded - if this URI gets sent back to us as part of a data object on a request (e.g. codelens/resolve), the client will leave + // the URI untouched, even if they generally send unencoded URIs. In such cases we need to consider the encoded and unencoded URI equivalent. + // + // To handle that, we first compare the AbsoluteUri properties on both, which are always percent encoded. + if (this.ParsedUri.IsAbsoluteUri && otherUri.ParsedUri.IsAbsoluteUri && this.ParsedUri.AbsoluteUri == otherUri.ParsedUri.AbsoluteUri) + { + return true; + } + else + { + return Uri.Equals(this.ParsedUri, otherUri.ParsedUri); + } + } + + public override int GetHashCode() + { + if (this.ParsedUri is null) + { + // We can't do anything better than the uri string hash code if we cannot parse the URI. + return this.UriString.GetHashCode(); + } + + if (this.ParsedUri.IsAbsoluteUri) + { + // Since the Uri type does not consider an encoded Uri equal to an unencoded Uri, we need to handle this ourselves. + // The AbsoluteUri property is always encoded, so we can use this to compare the URIs (see Equals above). + // + // However, depending on the kind of URI, case sensitivity in AbsoluteUri should be ignored. + // Uri.GetHashCode normally handles this internally, but the parameters it uses to determine which comparison to use are not exposed. + // + // Instead, we will always create the hash code ignoring case, and will rely on the Equals implementation + // to handle collisions (between two Uris with different casing). This should be very rare in practice. + // Collisions can happen for non UNC URIs (e.g. `git:/blah` vs `git:/Blah`). + return StringComparer.OrdinalIgnoreCase.GetHashCode(this.ParsedUri.AbsoluteUri); + } + else + { + return this.ParsedUri.GetHashCode(); + } + } +} diff --git a/src/LanguageServer/Protocol/Protocol/FileOperations/FileCreate.cs b/src/LanguageServer/Protocol/Protocol/FileOperations/FileCreate.cs index ddd7192f4af3e..732f9dd564f94 100644 --- a/src/LanguageServer/Protocol/Protocol/FileOperations/FileCreate.cs +++ b/src/LanguageServer/Protocol/Protocol/FileOperations/FileCreate.cs @@ -22,5 +22,5 @@ internal class FileCreate [JsonPropertyName("uri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri { get; set; } + public DocumentUri Uri { get; set; } } diff --git a/src/LanguageServer/Protocol/Protocol/FileOperations/FileDelete.cs b/src/LanguageServer/Protocol/Protocol/FileOperations/FileDelete.cs index f5ff575a3cc27..1edbd3adecc40 100644 --- a/src/LanguageServer/Protocol/Protocol/FileOperations/FileDelete.cs +++ b/src/LanguageServer/Protocol/Protocol/FileOperations/FileDelete.cs @@ -22,5 +22,5 @@ internal class FileDelete [JsonPropertyName("uri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri { get; set; } + public DocumentUri Uri { get; set; } } diff --git a/src/LanguageServer/Protocol/Protocol/FileOperations/FileEvent.cs b/src/LanguageServer/Protocol/Protocol/FileOperations/FileEvent.cs index 6a45333d8a5ee..26daaeb252db3 100644 --- a/src/LanguageServer/Protocol/Protocol/FileOperations/FileEvent.cs +++ b/src/LanguageServer/Protocol/Protocol/FileOperations/FileEvent.cs @@ -20,7 +20,7 @@ internal class FileEvent /// [JsonPropertyName("uri")] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri { get; set; } + public DocumentUri Uri { get; set; } /// /// Gets or sets the file change type. diff --git a/src/LanguageServer/Protocol/Protocol/FileOperations/FileRename.cs b/src/LanguageServer/Protocol/Protocol/FileOperations/FileRename.cs index fada354f69415..3aea71a4f1bc8 100644 --- a/src/LanguageServer/Protocol/Protocol/FileOperations/FileRename.cs +++ b/src/LanguageServer/Protocol/Protocol/FileOperations/FileRename.cs @@ -22,7 +22,7 @@ internal class FileRename [JsonPropertyName("oldUri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri OldUri { get; set; } + public DocumentUri OldUri { get; set; } /// /// A file:// URI for the new location of the file/folder being renamed. @@ -30,5 +30,5 @@ internal class FileRename [JsonPropertyName("newUri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri NewUri { get; set; } + public DocumentUri NewUri { get; set; } } diff --git a/src/LanguageServer/Protocol/Protocol/FileOperations/RelativePattern.cs b/src/LanguageServer/Protocol/Protocol/FileOperations/RelativePattern.cs index 6126ef4a7c3b7..2a72df32aa6d6 100644 --- a/src/LanguageServer/Protocol/Protocol/FileOperations/RelativePattern.cs +++ b/src/LanguageServer/Protocol/Protocol/FileOperations/RelativePattern.cs @@ -21,7 +21,7 @@ internal class RelativePattern /// [JsonPropertyName("baseUri")] [JsonRequired] - public SumType BaseUri { get; init; } + public SumType BaseUri { get; init; } /// /// The actual glob pattern. See Glob Pattern for more detail. diff --git a/src/LanguageServer/Protocol/Protocol/InitializeParams.cs b/src/LanguageServer/Protocol/Protocol/InitializeParams.cs index 1d6aa360525e3..74b5d8c9a96dd 100644 --- a/src/LanguageServer/Protocol/Protocol/InitializeParams.cs +++ b/src/LanguageServer/Protocol/Protocol/InitializeParams.cs @@ -67,7 +67,7 @@ public string? RootPath [JsonPropertyName("rootUri")] [Obsolete("Deprecated in favor of WorkspaceFolders")] [JsonConverter(typeof(DocumentUriConverter))] - public Uri? RootUri + public DocumentUri? RootUri { get; set; diff --git a/src/LanguageServer/Protocol/Protocol/Location.cs b/src/LanguageServer/Protocol/Protocol/Location.cs index 123dfea04440a..d2b9adf4fc707 100644 --- a/src/LanguageServer/Protocol/Protocol/Location.cs +++ b/src/LanguageServer/Protocol/Protocol/Location.cs @@ -20,7 +20,7 @@ internal class Location : IEquatable /// [JsonPropertyName("uri")] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri + public DocumentUri Uri { get; set; @@ -45,7 +45,7 @@ public override bool Equals(object obj) /// public bool Equals(Location? other) { - return other != null && this.Uri != null && other.Uri != null && + return other != null && this.Uri.Equals(other.Uri) && EqualityComparer.Default.Equals(this.Range, other.Range); } @@ -54,7 +54,7 @@ public bool Equals(Location? other) public override int GetHashCode() { var hashCode = 1486144663; - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(this.Uri); + hashCode = (hashCode * -1521134295) + this.Uri.GetHashCode(); hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(this.Range); return hashCode; } diff --git a/src/LanguageServer/Protocol/Protocol/LocationLink.cs b/src/LanguageServer/Protocol/Protocol/LocationLink.cs index 8f2fff57d4246..818f93d54cc48 100644 --- a/src/LanguageServer/Protocol/Protocol/LocationLink.cs +++ b/src/LanguageServer/Protocol/Protocol/LocationLink.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; - using Roslyn.Utilities; namespace Roslyn.LanguageServer.Protocol; @@ -36,7 +35,7 @@ internal class LocationLink : IEquatable [JsonPropertyName("targetUri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri TargetUri { get; init; } + public DocumentUri TargetUri { get; init; } /// /// The full target range of the linked location in the target document, which includes @@ -63,7 +62,7 @@ internal class LocationLink : IEquatable public bool Equals(LocationLink? other) => other != null && EqualityComparer.Default.Equals(this.OriginSelectionRange, other.OriginSelectionRange) - && this.TargetUri != null && other.TargetUri != null && this.TargetUri.Equals(other.TargetUri) + && EqualityComparer.Default.Equals(this.TargetUri, other.TargetUri) && EqualityComparer.Default.Equals(this.TargetRange, other.TargetRange) && EqualityComparer.Default.Equals(this.TargetSelectionRange, other.TargetSelectionRange); @@ -73,7 +72,7 @@ public override int GetHashCode() => HashCode.Combine(OriginSelectionRange, TargetUri, TargetRange, TargetSelectionRange); #else Hash.Combine(OriginSelectionRange, - Hash.Combine(TargetUri, + Hash.Combine(TargetUri.GetHashCode(), Hash.Combine(TargetRange, TargetSelectionRange.GetHashCode()))); #endif } diff --git a/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyItem.cs b/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyItem.cs index 384c225efa4cd..2e99f501865aa 100644 --- a/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyItem.cs +++ b/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyItem.cs @@ -50,7 +50,7 @@ internal class CallHierarchyItem [JsonPropertyName("uri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri { get; init; } + public DocumentUri Uri { get; init; } /// /// The range enclosing this symbol not including leading/trailing whitespace diff --git a/src/LanguageServer/Protocol/Protocol/Navigation/TypeHierarchyItem.cs b/src/LanguageServer/Protocol/Protocol/Navigation/TypeHierarchyItem.cs index 860eba4cc1d1a..7cc7b274849fd 100644 --- a/src/LanguageServer/Protocol/Protocol/Navigation/TypeHierarchyItem.cs +++ b/src/LanguageServer/Protocol/Protocol/Navigation/TypeHierarchyItem.cs @@ -50,7 +50,7 @@ internal class TypeHierarchyItem [JsonPropertyName("uri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri { get; init; } + public DocumentUri Uri { get; init; } /// /// The range enclosing this symbol not including leading/trailing whitespace diff --git a/src/LanguageServer/Protocol/Protocol/Notebook/NotebookCell.cs b/src/LanguageServer/Protocol/Protocol/Notebook/NotebookCell.cs index a738797ac815b..dd5e7f28abdc5 100644 --- a/src/LanguageServer/Protocol/Protocol/Notebook/NotebookCell.cs +++ b/src/LanguageServer/Protocol/Protocol/Notebook/NotebookCell.cs @@ -33,7 +33,7 @@ internal class NotebookCell [JsonPropertyName("document")] [JsonConverter(typeof(DocumentUriConverter))] [JsonRequired] - public Uri Document { get; init; } + public DocumentUri Document { get; init; } /// /// Additional metadata stored with the cell. diff --git a/src/LanguageServer/Protocol/Protocol/PreviousResultId.cs b/src/LanguageServer/Protocol/Protocol/PreviousResultId.cs index 610de39552d7c..28dd4ea10b834 100644 --- a/src/LanguageServer/Protocol/Protocol/PreviousResultId.cs +++ b/src/LanguageServer/Protocol/Protocol/PreviousResultId.cs @@ -21,7 +21,7 @@ internal class PreviousResultId /// [JsonPropertyName("uri")] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri + public DocumentUri Uri { get; set; diff --git a/src/LanguageServer/Protocol/Protocol/PublishDiagnosticParams.cs b/src/LanguageServer/Protocol/Protocol/PublishDiagnosticParams.cs index 4656113a54618..32d553bc844de 100644 --- a/src/LanguageServer/Protocol/Protocol/PublishDiagnosticParams.cs +++ b/src/LanguageServer/Protocol/Protocol/PublishDiagnosticParams.cs @@ -19,7 +19,7 @@ internal class PublishDiagnosticParams /// [JsonPropertyName("uri")] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri + public DocumentUri Uri { get; set; diff --git a/src/LanguageServer/Protocol/Protocol/RenameFile.cs b/src/LanguageServer/Protocol/Protocol/RenameFile.cs index 4b50223b1495c..0bf93abc553d8 100644 --- a/src/LanguageServer/Protocol/Protocol/RenameFile.cs +++ b/src/LanguageServer/Protocol/Protocol/RenameFile.cs @@ -30,7 +30,7 @@ internal class RenameFile : IAnnotatedChange [JsonPropertyName("oldUri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri OldUri + public DocumentUri OldUri { get; set; @@ -42,7 +42,7 @@ public Uri OldUri [JsonPropertyName("newUri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri NewUri + public DocumentUri NewUri { get; set; diff --git a/src/LanguageServer/Protocol/Protocol/TextDocumentIdentifier.cs b/src/LanguageServer/Protocol/Protocol/TextDocumentIdentifier.cs index 9ec8f5a0c8804..9c6fdebb755e7 100644 --- a/src/LanguageServer/Protocol/Protocol/TextDocumentIdentifier.cs +++ b/src/LanguageServer/Protocol/Protocol/TextDocumentIdentifier.cs @@ -19,7 +19,7 @@ internal class TextDocumentIdentifier : IEquatable /// [JsonPropertyName("uri")] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri + public DocumentUri Uri { get; set; @@ -69,13 +69,13 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - return this.Uri == null ? 89 : this.Uri.GetHashCode(); + return this.Uri is null ? 89 : this.Uri.GetHashCode(); } /// public override string ToString() { - return this.Uri == null ? string.Empty : this.Uri.AbsolutePath; + return this.Uri.ToString(); } } } diff --git a/src/LanguageServer/Protocol/Protocol/TextDocumentItem.cs b/src/LanguageServer/Protocol/Protocol/TextDocumentItem.cs index 9a627e3ff7622..e9ce7c82dc905 100644 --- a/src/LanguageServer/Protocol/Protocol/TextDocumentItem.cs +++ b/src/LanguageServer/Protocol/Protocol/TextDocumentItem.cs @@ -19,7 +19,7 @@ internal class TextDocumentItem /// [JsonPropertyName("uri")] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri + public DocumentUri Uri { get; set; diff --git a/src/LanguageServer/Protocol/Protocol/WorkspaceFolder.cs b/src/LanguageServer/Protocol/Protocol/WorkspaceFolder.cs index baa62b41407f1..b91a229b55994 100644 --- a/src/LanguageServer/Protocol/Protocol/WorkspaceFolder.cs +++ b/src/LanguageServer/Protocol/Protocol/WorkspaceFolder.cs @@ -17,7 +17,7 @@ internal class WorkspaceFolder [JsonPropertyName("uri")] [JsonConverter(typeof(DocumentUriConverter))] [JsonRequired] - public Uri Uri { get; init; } + public DocumentUri Uri { get; init; } /// /// The name of the workspace folder used in the UI. diff --git a/src/LanguageServer/Protocol/Protocol/WorkspaceFullDocumentDiagnosticReport.cs b/src/LanguageServer/Protocol/Protocol/WorkspaceFullDocumentDiagnosticReport.cs index a034368bf8bda..26a2976aa190a 100644 --- a/src/LanguageServer/Protocol/Protocol/WorkspaceFullDocumentDiagnosticReport.cs +++ b/src/LanguageServer/Protocol/Protocol/WorkspaceFullDocumentDiagnosticReport.cs @@ -23,7 +23,7 @@ internal class WorkspaceFullDocumentDiagnosticReport : FullDocumentDiagnosticRep [JsonPropertyName("uri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri + public DocumentUri Uri { get; set; diff --git a/src/LanguageServer/Protocol/Protocol/WorkspaceSymbolLocation.cs b/src/LanguageServer/Protocol/Protocol/WorkspaceSymbolLocation.cs index 6719e157caac2..ed0ab6803d7cc 100644 --- a/src/LanguageServer/Protocol/Protocol/WorkspaceSymbolLocation.cs +++ b/src/LanguageServer/Protocol/Protocol/WorkspaceSymbolLocation.cs @@ -15,6 +15,6 @@ internal class WorkspaceSymbolLocation [JsonPropertyName("uri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri { get; init; } + public DocumentUri Uri { get; init; } } diff --git a/src/LanguageServer/Protocol/Protocol/WorkspaceUnchangedDocumentDiagnosticReport.cs b/src/LanguageServer/Protocol/Protocol/WorkspaceUnchangedDocumentDiagnosticReport.cs index 84316ebaacbfa..2680f13e33da7 100644 --- a/src/LanguageServer/Protocol/Protocol/WorkspaceUnchangedDocumentDiagnosticReport.cs +++ b/src/LanguageServer/Protocol/Protocol/WorkspaceUnchangedDocumentDiagnosticReport.cs @@ -23,7 +23,7 @@ internal class WorkspaceUnchangedDocumentDiagnosticReport : UnchangedDocumentDia [JsonPropertyName("uri")] [JsonRequired] [JsonConverter(typeof(DocumentUriConverter))] - public Uri Uri + public DocumentUri Uri { get; set; diff --git a/src/LanguageServer/Protocol/RoslynLanguageServer.cs b/src/LanguageServer/Protocol/RoslynLanguageServer.cs index a68788ad03c87..8a4e9b2a45923 100644 --- a/src/LanguageServer/Protocol/RoslynLanguageServer.cs +++ b/src/LanguageServer/Protocol/RoslynLanguageServer.cs @@ -191,13 +191,13 @@ public override bool TryGetLanguageForRequest(string methodName, object? seriali // { "textDocument": { "uri": "" ... } ... } // // We can easily identify the URI for the request by looking for this structure - Uri? uri = null; + DocumentUri? uri = null; if (parameters.TryGetProperty("textDocument", out var textDocumentToken) || parameters.TryGetProperty("_vs_textDocument", out textDocumentToken)) { - var uriToken = textDocumentToken.GetProperty("uri"); - uri = JsonSerializer.Deserialize(uriToken, ProtocolConversions.LspJsonSerializerOptions); - Contract.ThrowIfNull(uri, "Failed to deserialize uri property"); + var textDocumentIdentifier = JsonSerializer.Deserialize(textDocumentToken, ProtocolConversions.LspJsonSerializerOptions); + Contract.ThrowIfNull(textDocumentIdentifier, "Failed to deserialize text document identifier property"); + uri = textDocumentIdentifier.Uri; } else if (parameters.TryGetProperty("data", out var dataToken)) { @@ -211,7 +211,7 @@ public override bool TryGetLanguageForRequest(string methodName, object? seriali uri = data.TextDocument.Uri; } - if (uri == null) + if (uri is null) { // This request is not for a textDocument and is not a resolve request. Logger.LogInformation("Request did not contain a textDocument, using default language handler"); diff --git a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspace.cs b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspace.cs index 289bda83594e4..15b1d0ba5f955 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspace.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspace.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.CommonLanguageServerProtocol.Framework; +using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer @@ -33,12 +34,16 @@ internal sealed class LspMiscellaneousFilesWorkspace(ILspServices lspServices, I /// /// Takes in a file URI and text and creates a misc project and document for the file. /// - /// Calls to this method and are made + /// Calls to this method and are made /// from LSP text sync request handling which do not run concurrently. /// - public Document? AddMiscellaneousDocument(Uri uri, SourceText documentText, string languageId, ILspLogger logger) + public Document? AddMiscellaneousDocument(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) { - var documentFilePath = ProtocolConversions.GetDocumentFilePathFromUri(uri); + var documentFilePath = uri.UriString; + if (uri.ParsedUri is not null) + { + documentFilePath = ProtocolConversions.GetDocumentFilePathFromUri(uri.ParsedUri); + } var container = new StaticSourceTextContainer(documentText); if (metadataAsSourceFileService.TryAddDocumentToWorkspace(documentFilePath, container, out var documentId)) @@ -70,13 +75,12 @@ internal sealed class LspMiscellaneousFilesWorkspace(ILspServices lspServices, I /// /// Removes a document with the matching file path from this workspace. /// - /// Calls to this method and are made + /// Calls to this method and are made /// from LSP text sync request handling which do not run concurrently. /// - public void TryRemoveMiscellaneousDocument(Uri uri, bool removeFromMetadataWorkspace) + public void TryRemoveMiscellaneousDocument(DocumentUri uri, bool removeFromMetadataWorkspace) { - var documentFilePath = ProtocolConversions.GetDocumentFilePathFromUri(uri); - if (removeFromMetadataWorkspace && metadataAsSourceFileService.TryRemoveDocumentFromWorkspace(documentFilePath)) + if (removeFromMetadataWorkspace && uri.ParsedUri is not null && metadataAsSourceFileService.TryRemoveDocumentFromWorkspace(ProtocolConversions.GetDocumentFilePathFromUri(uri.ParsedUri))) { return; } diff --git a/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs b/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs index f9e31132397a7..8b95326437e8a 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs @@ -45,56 +45,6 @@ namespace Microsoft.CodeAnalysis.LanguageServer; /// internal sealed class LspWorkspaceManager : IDocumentChangeTracker, ILspService { - private class LspUriComparer : IEqualityComparer - { - public static readonly LspUriComparer Instance = new(); - public bool Equals(Uri? x, Uri? y) - { - // Compare the absolute URIs to handle the case where one URI is encoded and the other is not. - // By default, Uri.Equals will not consider the encoded version of a URI equal to the unencoded version. - // - // The client is expected to be consistent in how it sends the URIs (either encoded or unencoded). - // So we normally can safely store the URIs as they send us in our map and expect subsequent requests to be encoded in the same way and match. - // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#uri - // - // However when we serialize URIs to the client, we serialize the AbsoluteUri property which is always % encoded (no matter the original representation). - // For some requests, the client sends us exactly back what we sent (e.g. the data in a codelens/resolve request). - // This means that for these requests, the URI we will get from the client is the encoded version (that we sent). - // If the client sent us an unencoded URI originally, Uri.Equals will not consider it equal to the encoded version and we will fail to find the document - // - // So in order to resolve the encoded URI to the correct text, we can compare the AbsoluteUri properties (which are always encoded). - if (x is not null && y is not null && x.IsAbsoluteUri && y.IsAbsoluteUri && x.AbsoluteUri == y.AbsoluteUri) - { - return true; - } - else - { - return Uri.Equals(x, y); - } - } - - public int GetHashCode(Uri obj) - { - if (obj.IsAbsoluteUri) - { - // Since the Uri type does not consider an encoded Uri equal to an unencoded Uri, we need to handle this ourselves. - // The AbsoluteUri property is always encoded, so we can use this to compare the URIs (see Equals above). - // - // However, depending on the kind of URI, case sensitivity in AbsoluteUri should be ignored. - // Uri.GetHashCode normally handles this internally, but the parameters it uses to determine which comparison to use are not exposed. - // - // Instead, we will always create the hash code ignoring case, and will rely on the Equals implementation - // to handle collisions (between two Uris with different casing). This should be very rare in practice. - // Collisions can happen for non UNC URIs (e.g. `git:/blah` vs `git:/Blah`). - return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.AbsoluteUri); - } - else - { - return obj.GetHashCode(); - } - } - } - /// /// A cache from workspace to the last solution we returned for LSP. /// The forkedFromVersion is not null when the solution was created from a fork of the workspace with LSP @@ -112,7 +62,7 @@ public int GetHashCode(Uri obj) /// the URI. /// Access to this is guaranteed to be serial by the /// - private ImmutableDictionary _trackedDocuments = ImmutableDictionary.Empty.WithComparers(LspUriComparer.Instance); + private ImmutableDictionary _trackedDocuments = ImmutableDictionary.Empty; private readonly ILspLogger _logger; private readonly LspMiscellaneousFilesWorkspace? _lspMiscellaneousFilesWorkspace; @@ -139,7 +89,7 @@ public LspWorkspaceManager( #region Implementation of IDocumentChangeTracker - private static async ValueTask ApplyChangeToMutatingWorkspaceAsync(Workspace workspace, Uri uri, Func change) + private static async ValueTask ApplyChangeToMutatingWorkspaceAsync(Workspace workspace, DocumentUri uri, Func change) { if (workspace is not ILspWorkspace { SupportsMutation: true } mutatingWorkspace) return; @@ -153,10 +103,16 @@ private static async ValueTask ApplyChangeToMutatingWorkspaceAsync(Workspace wor /// /// is true which means this runs serially in the /// - public async ValueTask StartTrackingAsync(Uri uri, SourceText documentText, string languageId, CancellationToken cancellationToken) + public async ValueTask StartTrackingAsync(DocumentUri uri, SourceText documentText, string languageId, CancellationToken cancellationToken) { // First, store the LSP view of the text as the uri is now owned by the LSP client. Contract.ThrowIfTrue(_trackedDocuments.ContainsKey(uri), $"didOpen received for {uri} which is already open."); + + if (uri.ParsedUri is null) + { + _logger.LogError($"Unable to parse URI {uri}"); + } + _trackedDocuments = _trackedDocuments.Add(uri, (documentText, languageId)); // If LSP changed, we need to compare against the workspace again to get the updated solution. @@ -171,7 +127,7 @@ public async ValueTask StartTrackingAsync(Uri uri, SourceText documentText, stri return; - async ValueTask TryOpenDocumentsInMutatingWorkspaceAsync(Uri uri) + async ValueTask TryOpenDocumentsInMutatingWorkspaceAsync(DocumentUri uri) { var registeredWorkspaces = _lspWorkspaceRegistrationService.GetAllRegistrations(); foreach (var workspace in registeredWorkspaces) @@ -187,7 +143,7 @@ await ApplyChangeToMutatingWorkspaceAsync(workspace, uri, (_, documentId) => /// /// is true which means this runs serially in the /// - public async ValueTask StopTrackingAsync(Uri uri, CancellationToken cancellationToken) + public async ValueTask StopTrackingAsync(DocumentUri uri, CancellationToken cancellationToken) { // First, stop tracking this URI and source text as it is no longer owned by LSP. Contract.ThrowIfFalse(_trackedDocuments.ContainsKey(uri), $"didClose received for {uri} which is not open."); @@ -206,7 +162,7 @@ public async ValueTask StopTrackingAsync(Uri uri, CancellationToken cancellation return; - async ValueTask TryCloseDocumentsInMutatingWorkspaceAsync(Uri uri) + async ValueTask TryCloseDocumentsInMutatingWorkspaceAsync(DocumentUri uri) { var registeredWorkspaces = _lspWorkspaceRegistrationService.GetAllRegistrations(); foreach (var workspace in registeredWorkspaces) @@ -231,7 +187,7 @@ await ApplyChangeToMutatingWorkspaceAsync(workspace, uri, async (_, documentId) /// /// is true which means this runs serially in the /// - public void UpdateTrackedDocument(Uri uri, SourceText newSourceText) + public void UpdateTrackedDocument(DocumentUri uri, SourceText newSourceText) { // Store the updated LSP view of the source text. Contract.ThrowIfFalse(_trackedDocuments.ContainsKey(uri), $"didChange received for {uri} which is not open."); @@ -244,7 +200,7 @@ public void UpdateTrackedDocument(Uri uri, SourceText newSourceText) LspTextChanged?.Invoke(this, EventArgs.Empty); } - public ImmutableDictionary GetTrackedLspText() => _trackedDocuments; + public ImmutableDictionary GetTrackedLspText() => _trackedDocuments; #endregion @@ -396,8 +352,9 @@ .. registeredWorkspaces.Where(workspace => workspace.Kind == WorkspaceKind.Misce var documentsInWorkspace = GetDocumentsForUris([.. _trackedDocuments.Keys], workspaceCurrentSolution); var sourceGeneratedDocuments = - _trackedDocuments.Keys.Where(static uri => uri.Scheme == SourceGeneratedDocumentUri.Scheme) - .Select(uri => (identity: SourceGeneratedDocumentUri.DeserializeIdentity(workspaceCurrentSolution, uri), _trackedDocuments[uri].Text)) + _trackedDocuments.Keys.Where(static trackedDocument => trackedDocument.ParsedUri?.Scheme == SourceGeneratedDocumentUri.Scheme) + // We know we have a non null URI with a source generated scheme. + .Select(uri => (identity: SourceGeneratedDocumentUri.DeserializeIdentity(workspaceCurrentSolution, uri.ParsedUri!), _trackedDocuments[uri].Text)) .Where(tuple => tuple.identity.HasValue) .SelectAsArray(tuple => (tuple.identity!.Value, DateTime.Now, tuple.Text)); @@ -478,11 +435,11 @@ await workspace.TryOnDocumentOpenedAsync( /// This looks at the source generator state explicitly to avoid actually running source generators /// private static bool DoesAllSourceGeneratedTextMatchWorkspaceSolution( - ImmutableArray<(SourceGeneratedDocumentIdentity Identity, DateTime Generated, SourceText Text)> sourceGenereatedDocuments, + ImmutableArray<(SourceGeneratedDocumentIdentity Identity, DateTime Generated, SourceText Text)> sourceGeneratedDocuments, Solution workspaceSolution) { var compilationState = workspaceSolution.CompilationState; - foreach (var (identity, _, text) in sourceGenereatedDocuments) + foreach (var (identity, _, text) in sourceGeneratedDocuments) { var existingState = compilationState.TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(identity.DocumentId); if (existingState is null) @@ -504,7 +461,7 @@ private static bool DoesAllSourceGeneratedTextMatchWorkspaceSolution( /// /// Given a set of documents from the workspace current solution, verify that the LSP text is the same as the document contents. /// - private async Task DoesAllTextMatchWorkspaceSolutionAsync(ImmutableDictionary> documentsInWorkspace, CancellationToken cancellationToken) + private async Task DoesAllTextMatchWorkspaceSolutionAsync(ImmutableDictionary> documentsInWorkspace, CancellationToken cancellationToken) { foreach (var (uriInWorkspace, documentsForUri) in documentsInWorkspace) { @@ -536,7 +493,7 @@ private static async ValueTask AreChecksumsEqualAsync(TextDocument documen /// /// Returns a Roslyn language name for the given URI. /// - internal bool TryGetLanguageForUri(Uri uri, [NotNullWhen(true)] out string? language) + internal bool TryGetLanguageForUri(DocumentUri uri, [NotNullWhen(true)] out string? language) { string? languageId = null; if (_trackedDocuments.TryGetValue(uri, out var trackedDocument)) @@ -557,9 +514,9 @@ internal bool TryGetLanguageForUri(Uri uri, [NotNullWhen(true)] out string? lang /// /// Using the workspace's current solutions, find the matching documents in for each URI. /// - private static ImmutableDictionary> GetDocumentsForUris(ImmutableArray trackedDocuments, Solution workspaceCurrentSolution) + private static ImmutableDictionary> GetDocumentsForUris(ImmutableArray trackedDocuments, Solution workspaceCurrentSolution) { - using var _ = PooledDictionary>.GetInstance(out var documentsInSolution); + using var _ = PooledDictionary>.GetInstance(out var documentsInSolution); foreach (var trackedDoc in trackedDocuments) { var documents = workspaceCurrentSolution.GetTextDocuments(trackedDoc); diff --git a/src/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionResolveTests.cs b/src/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionResolveTests.cs index 115998c9c86ca..217b0dfac9ade 100644 --- a/src/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionResolveTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionResolveTests.cs @@ -193,7 +193,7 @@ class {|caret:ABC|} groupName: "Roslyn2", applicableRange: new LSP.Range { Start = new Position { Line = 0, Character = 6 }, End = new Position { Line = 0, Character = 9 } }, diagnostics: null, - edit: GenerateRenameFileEdit(new List<(Uri, Uri)> { (documentUriBefore, documentUriAfter) })); + edit: GenerateRenameFileEdit(new List<(DocumentUri, DocumentUri)> { (documentUriBefore, documentUriAfter) })); AssertJsonEquals(expectedCodeAction, actualResolvedAction); } @@ -343,7 +343,7 @@ class BCD var actualResolvedAction = await RunGetCodeActionResolveAsync(testLspServer, unresolvedCodeAction); var project = testWorkspace.CurrentSolution.Projects.Single(); - var newDocumentUri = ProtocolConversions.CreateAbsoluteUri(Path.Combine(Path.GetDirectoryName(project.FilePath), "ABC.cs")); + var newDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(Path.Combine(Path.GetDirectoryName(project.FilePath), "ABC.cs")); var existingDocumentUri = testWorkspace.CurrentSolution.GetRequiredDocument(testWorkspace.Documents.Single().Id).GetURI(); var workspaceEdit = new WorkspaceEdit() { @@ -470,7 +470,7 @@ class {|caret:BCD|} var existingDocumentUri = existingDocument.GetURI(); Assert.Contains(Path.Combine("dir1", "dir2", "dir3"), existingDocument.FilePath); - var newDocumentUri = ProtocolConversions.CreateAbsoluteUri( + var newDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri( Path.Combine(Path.GetDirectoryName(existingDocument.FilePath), "BCD.cs")); var workspaceEdit = new WorkspaceEdit() { @@ -583,7 +583,7 @@ private static WorkspaceEdit GenerateWorkspaceEdit( } }; - private static WorkspaceEdit GenerateRenameFileEdit(IList<(Uri oldUri, Uri newUri)> renameLocations) + private static WorkspaceEdit GenerateRenameFileEdit(IList<(DocumentUri oldUri, DocumentUri newUri)> renameLocations) => new() { DocumentChanges = renameLocations.Select( diff --git a/src/LanguageServer/ProtocolUnitTests/Definitions/GoToDefinitionTests.cs b/src/LanguageServer/ProtocolUnitTests/Definitions/GoToDefinitionTests.cs index 7eb8c621e703c..91bdbc4d8de54 100644 --- a/src/LanguageServer/ProtocolUnitTests/Definitions/GoToDefinitionTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Definitions/GoToDefinitionTests.cs @@ -38,7 +38,7 @@ void M() var results = await RunGotoDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); // Verify that as originally serialized, the URI had a file scheme. - Assert.True(results.Single().Uri.OriginalString.StartsWith("file")); + Assert.True(results.Single().Uri.UriString.StartsWith("file")); AssertLocationsEqual(testLspServer.GetLocations("definition"), results); } @@ -88,7 +88,7 @@ void M() var position = new LSP.Position { Line = 5, Character = 18 }; var results = await RunGotoDefinitionAsync(testLspServer, new LSP.Location { - Uri = ProtocolConversions.CreateAbsoluteUri($"C:\\{TestSpanMapper.GeneratedFileName}"), + Uri = ProtocolConversions.CreateAbsoluteDocumentUri($"C:\\{TestSpanMapper.GeneratedFileName}"), Range = new LSP.Range { Start = position, End = position } }); AssertLocationsEqual([TestSpanMapper.MappedFileLocation], results); @@ -253,7 +253,7 @@ class B var results = await RunGotoDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); var result = Assert.Single(results); - Assert.Equal(SourceGeneratedDocumentUri.Scheme, result.Uri.Scheme); + Assert.Equal(SourceGeneratedDocumentUri.Scheme, result.Uri.GetRequiredParsedUri().Scheme); } [Theory, CombinatorialData] @@ -272,7 +272,7 @@ void M() await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var results = await RunGotoDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); - Assert.True(results.Single().Uri.OriginalString.EndsWith("String.cs")); + Assert.True(results.Single().Uri.UriString.EndsWith("String.cs")); } private static async Task RunGotoDefinitionAsync(TestLspServer testLspServer, LSP.Location caret) diff --git a/src/LanguageServer/ProtocolUnitTests/Definitions/GoToTypeDefinitionTests.cs b/src/LanguageServer/ProtocolUnitTests/Definitions/GoToTypeDefinitionTests.cs index a18b449d3cd47..52b7c5cd9e783 100644 --- a/src/LanguageServer/ProtocolUnitTests/Definitions/GoToTypeDefinitionTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Definitions/GoToTypeDefinitionTests.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Roslyn.Test.Utilities.TestGenerators; using Xunit; @@ -216,7 +217,7 @@ class B var results = await RunGotoTypeDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); var result = Assert.Single(results); - Assert.Equal(SourceGeneratedDocumentUri.Scheme, result.Uri.Scheme); + Assert.Equal(SourceGeneratedDocumentUri.Scheme, result.Uri.GetRequiredParsedUri().Scheme); } [Theory, CombinatorialData] @@ -279,7 +280,7 @@ End Class CreateTextDocumentPositionParams(caret), CancellationToken.None); } - private static async Task GetWorkspaceForDocument(TestLspServer testLspServer, Uri fileUri) + private static async Task GetWorkspaceForDocument(TestLspServer testLspServer, DocumentUri fileUri) { var (lspWorkspace, _, _) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { Uri = fileUri }, CancellationToken.None); return lspWorkspace!; diff --git a/src/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs b/src/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs index 094dec54538ad..a55ee70852925 100644 --- a/src/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs +++ b/src/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs @@ -232,7 +232,7 @@ private protected static async Task InsertTextAsync( private protected static Task> RunGetDocumentPullDiagnosticsAsync( TestLspServer testLspServer, - Uri uri, + DocumentUri uri, bool useVSDiagnostics, string? previousResultId = null, bool useProgress = false, @@ -367,7 +367,7 @@ private protected static InitializationOptions GetInitializationOptions( /// private protected record TestDiagnosticResult(TextDocumentIdentifier TextDocument, string? ResultId, LSP.Diagnostic[]? Diagnostics) { - public Uri Uri { get; } = TextDocument.Uri; + public DocumentUri Uri { get; } = TextDocument.Uri; } [DiagnosticAnalyzer(InternalLanguageNames.TypeScript), PartNotDiscoverable] diff --git a/src/LanguageServer/ProtocolUnitTests/Diagnostics/AdditionalFileDiagnosticsTests.cs b/src/LanguageServer/ProtocolUnitTests/Diagnostics/AdditionalFileDiagnosticsTests.cs index 140598171eaa0..09974b7387be1 100644 --- a/src/LanguageServer/ProtocolUnitTests/Diagnostics/AdditionalFileDiagnosticsTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Diagnostics/AdditionalFileDiagnosticsTests.cs @@ -40,7 +40,7 @@ public async Task TestWorkspaceDiagnosticsReportsAdditionalFileDiagnostic(bool u @"C:\C.cs: []", @$"C:\Test.txt: [{MockAdditionalFileDiagnosticAnalyzer.Id}]", @"C:\CSProj1.csproj: []" - ], results.Select(r => $"{r.Uri.LocalPath}: [{string.Join(", ", r.Diagnostics.Select(d => d.Code?.Value?.ToString()))}]")); + ], results.Select(r => $"{r.Uri.GetRequiredParsedUri().LocalPath}: [{string.Join(", ", r.Diagnostics.Select(d => d.Code?.Value?.ToString()))}]")); // Asking again should give us back an unchanged diagnostic. var results2 = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics, previousResults: CreateDiagnosticParamsFromPreviousReports(results)); @@ -65,7 +65,7 @@ public async Task TestWorkspaceDiagnosticsWithRemovedAdditionalFile(bool useVSDi AssertEx.Empty(results[0].Diagnostics); Assert.Equal(MockAdditionalFileDiagnosticAnalyzer.Id, results[1].Diagnostics.Single().Code); - Assert.Equal(@"C:\Test.txt", results[1].Uri.LocalPath); + Assert.Equal(@"C:\Test.txt", results[1].Uri.GetRequiredParsedUri().LocalPath); AssertEx.Empty(results[2].Diagnostics); var initialSolution = testLspServer.GetCurrentSolution(); @@ -101,10 +101,10 @@ public async Task TestWorkspaceDiagnosticsWithAdditionalFileInMultipleProjects(b Assert.Equal(6, results.Length); Assert.Equal(MockAdditionalFileDiagnosticAnalyzer.Id, results[1].Diagnostics.Single().Code); - Assert.Equal(@"C:\Test.txt", results[1].Uri.LocalPath); + Assert.Equal(@"C:\Test.txt", results[1].Uri.GetRequiredParsedUri().LocalPath); Assert.Equal("CSProj1", ((LSP.VSDiagnostic)results[1].Diagnostics.Single()).Projects.First().ProjectName); Assert.Equal(MockAdditionalFileDiagnosticAnalyzer.Id, results[4].Diagnostics.Single().Code); - Assert.Equal(@"C:\Test.txt", results[4].Uri.LocalPath); + Assert.Equal(@"C:\Test.txt", results[4].Uri.GetRequiredParsedUri().LocalPath); Assert.Equal("CSProj2", ((LSP.VSDiagnostic)results[4].Diagnostics.Single()).Projects.First().ProjectName); // Asking again should give us back an unchanged diagnostic. diff --git a/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs b/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs index 8e5958674d2b3..f87f09eeeef41 100644 --- a/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs @@ -69,7 +69,7 @@ public async Task TestDocumentDiagnosticsForOpenFilesWithFSAOff(bool useVSDiagno testLspServer, document.GetURI(), useVSDiagnostics); Assert.Equal("CS1513", results.Single().Diagnostics.Single().Code); - Assert.NotNull(results.Single().Diagnostics.Single().CodeDescription!.Href); + Assert.NotNull(results.Single().Diagnostics.Single().CodeDescription!.Href.ParsedUri); } [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/fsharp/issues/15972")] @@ -636,7 +636,7 @@ public async Task TestDocumentDiagnosticsFromRazorServer(bool useVSDiagnostics, // Assert that we have diagnostics even though the option is set to push. Assert.Equal("CS1513", results.Single().Diagnostics.Single().Code); - Assert.NotNull(results.Single().Diagnostics.Single().CodeDescription!.Href); + Assert.NotNull(results.Single().Diagnostics.Single().CodeDescription!.Href.ParsedUri); } [Theory, CombinatorialData] @@ -660,7 +660,7 @@ public async Task TestDocumentDiagnosticsFromLiveShareServer(bool useVSDiagnosti // Assert that we have diagnostics even though the option is set to push. Assert.Equal("CS1513", results.Single().Diagnostics.Single().Code); - Assert.NotNull(results.Single().Diagnostics.Single().CodeDescription!.Href); + Assert.NotNull(results.Single().Diagnostics.Single().CodeDescription!.Href.ParsedUri); } [Theory, CombinatorialData] @@ -1374,7 +1374,7 @@ public async Task TestNoWorkspaceDiagnosticsForClosedFilesInProjectsWithIncorrec var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics); - Assert.False(results.Any(r => r.TextDocument!.Uri.LocalPath.Contains(".ts"))); + Assert.False(results.Any(r => r.TextDocument!.Uri.GetRequiredParsedUri().LocalPath.Contains(".ts"))); } [Theory, CombinatorialData] @@ -1549,7 +1549,7 @@ class A {"; var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics); Assert.Equal(3, results.Length); - Assert.Equal(ProtocolConversions.CreateAbsoluteUri(@"C:\test1.cs"), results[0].TextDocument!.Uri); + Assert.Equal(ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test1.cs"), results[0].TextDocument!.Uri); Assert.Equal("CS1513", results[0].Diagnostics.Single().Code); Assert.Equal(1, results[0].Diagnostics.Single().Range.Start.Line); AssertEx.Empty(results[1].Diagnostics); @@ -1945,9 +1945,9 @@ public async Task TestWorkspaceDiagnosticsDoesNotThrowIfProjectWithoutFilePathEx var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics); Assert.Equal(3, results.Length); - Assert.Equal(@"C:/C.cs", results[0].TextDocument.Uri.AbsolutePath); - Assert.Equal(@"C:/CSProj1.csproj", results[1].TextDocument.Uri.AbsolutePath); - Assert.Equal(@"C:/C2.cs", results[2].TextDocument.Uri.AbsolutePath); + Assert.Equal(@"C:/C.cs", results[0].TextDocument.Uri.GetRequiredParsedUri().AbsolutePath); + Assert.Equal(@"C:/CSProj1.csproj", results[1].TextDocument.Uri.GetRequiredParsedUri().AbsolutePath); + Assert.Equal(@"C:/C2.cs", results[2].TextDocument.Uri.GetRequiredParsedUri().AbsolutePath); } [Theory, CombinatorialData] diff --git a/src/LanguageServer/ProtocolUnitTests/Diagnostics/WorkspaceProjectDiagnosticsTests.cs b/src/LanguageServer/ProtocolUnitTests/Diagnostics/WorkspaceProjectDiagnosticsTests.cs index ce607c6a9fc25..27f1228914d57 100644 --- a/src/LanguageServer/ProtocolUnitTests/Diagnostics/WorkspaceProjectDiagnosticsTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Diagnostics/WorkspaceProjectDiagnosticsTests.cs @@ -30,7 +30,7 @@ public async Task TestWorkspaceDiagnosticsReportsProjectDiagnostic(bool useVSDia Assert.Equal(2, results.Length); AssertEx.Empty(results[0].Diagnostics); Assert.Equal(MockProjectDiagnosticAnalyzer.Id, results[1].Diagnostics.Single().Code); - Assert.Equal(ProtocolConversions.CreateAbsoluteUri(testLspServer.GetCurrentSolution().Projects.First().FilePath!), results[1].Uri); + Assert.Equal(ProtocolConversions.CreateAbsoluteDocumentUri(testLspServer.GetCurrentSolution().Projects.First().FilePath!), results[1].Uri); // Asking again should give us back an unchanged diagnostic. var results2 = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics, previousResults: CreateDiagnosticParamsFromPreviousReports(results)); @@ -47,7 +47,7 @@ public async Task TestWorkspaceDiagnosticsWithRemovedProject(bool useVSDiagnosti Assert.Equal(2, results.Length); AssertEx.Empty(results[0].Diagnostics); Assert.Equal(MockProjectDiagnosticAnalyzer.Id, results[1].Diagnostics.Single().Code); - Assert.Equal(ProtocolConversions.CreateAbsoluteUri(testLspServer.GetCurrentSolution().Projects.First().FilePath!), results[1].Uri); + Assert.Equal(ProtocolConversions.CreateAbsoluteDocumentUri(testLspServer.GetCurrentSolution().Projects.First().FilePath!), results[1].Uri); var initialSolution = testLspServer.GetCurrentSolution(); var newSolution = initialSolution.RemoveProject(initialSolution.Projects.First().Id); diff --git a/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.LinkedDocuments.cs b/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.LinkedDocuments.cs index f16259e730118..cbd9da7f4176c 100644 --- a/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.LinkedDocuments.cs +++ b/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.LinkedDocuments.cs @@ -99,7 +99,7 @@ void M() Assert.Empty(testLspServer.GetTrackedTexts()); } - private static async Task GetLSPSolutionAsync(TestLspServer testLspServer, Uri uri) + private static async Task GetLSPSolutionAsync(TestLspServer testLspServer, DocumentUri uri) { var (_, _, lspDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new TextDocumentIdentifier { Uri = uri }, CancellationToken.None).ConfigureAwait(false); Contract.ThrowIfNull(lspDocument); diff --git a/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.cs b/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.cs index 83bb8b0438328..5669360eb871a 100644 --- a/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageServer.Handler.DocumentChanges; +using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -457,11 +458,11 @@ void M() return (testLspServer, locationTyped, documentText.ToString()); } - private static Task DidOpen(TestLspServer testLspServer, Uri uri) => testLspServer.OpenDocumentAsync(uri); + private static Task DidOpen(TestLspServer testLspServer, DocumentUri uri) => testLspServer.OpenDocumentAsync(uri); - private static async Task DidChange(TestLspServer testLspServer, Uri uri, params (int line, int column, string text)[] changes) + private static async Task DidChange(TestLspServer testLspServer, DocumentUri uri, params (int line, int column, string text)[] changes) => await testLspServer.InsertTextAsync(uri, changes); - private static async Task DidClose(TestLspServer testLspServer, Uri uri) => await testLspServer.CloseDocumentAsync(uri); + private static async Task DidClose(TestLspServer testLspServer, DocumentUri uri) => await testLspServer.CloseDocumentAsync(uri); } } diff --git a/src/LanguageServer/ProtocolUnitTests/FormatNewFile/FormatNewFileTests.cs b/src/LanguageServer/ProtocolUnitTests/FormatNewFile/FormatNewFileTests.cs index 8dd97f5428f23..ea3ba4eafacf9 100644 --- a/src/LanguageServer/ProtocolUnitTests/FormatNewFile/FormatNewFileTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/FormatNewFile/FormatNewFileTests.cs @@ -74,11 +74,11 @@ public partial class MyComponent { Project = new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(project.FilePath) + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(project.FilePath) }, Document = new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(newFilePath) + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(newFilePath) }, Contents = input }; diff --git a/src/LanguageServer/ProtocolUnitTests/Formatting/FormatDocumentTests.cs b/src/LanguageServer/ProtocolUnitTests/Formatting/FormatDocumentTests.cs index e4ae2a35b33cc..a852df923d3d4 100644 --- a/src/LanguageServer/ProtocolUnitTests/Formatting/FormatDocumentTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Formatting/FormatDocumentTests.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -107,7 +108,7 @@ void M() private static async Task RunFormatDocumentAsync( TestLspServer testLspServer, - Uri uri, + DocumentUri uri, bool insertSpaces = true, int tabSize = 4) { @@ -115,7 +116,7 @@ void M() CreateDocumentFormattingParams(uri, insertSpaces, tabSize), CancellationToken.None); } - private static LSP.DocumentFormattingParams CreateDocumentFormattingParams(Uri uri, bool insertSpaces, int tabSize) + private static LSP.DocumentFormattingParams CreateDocumentFormattingParams(DocumentUri uri, bool insertSpaces, int tabSize) => new LSP.DocumentFormattingParams() { TextDocument = CreateTextDocumentIdentifier(uri), diff --git a/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs b/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs index 3787a64d7a2d8..d0f66a226c3ae 100644 --- a/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs @@ -43,7 +43,7 @@ public async Task CanExecuteRequestHandler(bool mutatingLspWorkspace) var request = new TestRequestTypeOne(new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs") + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test.cs") }); var response = await server.ExecuteRequestAsync(TestDocumentHandler.MethodName, request, CancellationToken.None); Assert.Equal(typeof(TestDocumentHandler).Name, response); @@ -65,7 +65,7 @@ public async Task CanExecuteNotificationHandler(bool mutatingLspWorkspace) var request = new TestRequestTypeOne(new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs") + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test.cs") }); await server.ExecuteNotificationAsync(TestNotificationHandler.MethodName, request); @@ -90,7 +90,7 @@ public async Task CanExecuteLanguageSpecificHandler(bool mutatingLspWorkspace) var request = new TestRequestTypeOne(new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.fs") + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test.fs") }); var response = await server.ExecuteRequestAsync(TestDocumentHandler.MethodName, request, CancellationToken.None); Assert.Equal(typeof(TestLanguageSpecificHandler).Name, response); @@ -103,7 +103,7 @@ public async Task CanExecuteLanguageSpecificHandlerWithDifferentRequestTypes(boo var request = new TestRequestTypeTwo(new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.vb") + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test.vb") }); var response = await server.ExecuteRequestAsync(TestDocumentHandler.MethodName, request, CancellationToken.None); Assert.Equal(typeof(TestLanguageSpecificHandlerWithDifferentParams).Name, response); @@ -144,7 +144,7 @@ public async Task NonMutatingHandlerExceptionNFWIsReported(bool mutatingLspWorks var request = new TestRequestWithDocument(new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs") + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test.cs") }); var didReport = false; @@ -172,7 +172,7 @@ public async Task NonMutatingHandlerExceptionNFWIsNotReportedForLocalRpcExceptio var request = new TestRequestWithDocument(new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs") + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test.cs") }); var didReport = false; @@ -200,7 +200,7 @@ public async Task MutatingHandlerExceptionNFWIsReported(bool mutatingLspWorkspac var request = new TestRequestWithDocument(new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs") + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test.cs") }); var didReport = false; @@ -230,7 +230,7 @@ public async Task NonMutatingHandlerCancellationExceptionNFWIsNotReported(bool m var request = new TestRequestWithDocument(new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs") + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test.cs") }); var didReport = false; @@ -258,7 +258,7 @@ public async Task MutatingHandlerCancellationExceptionNFWIsNotReported(bool muta var request = new TestRequestWithDocument(new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs") + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test.cs") }); var didReport = false; @@ -287,7 +287,7 @@ public async Task TestMutatingHandlerCrashesIfUnableToDetermineLanguage(bool mut // Run a mutating request against a file which we have no saved languageId for // and where the language cannot be determined from the URI. // This should crash the server. - var looseFileUri = ProtocolConversions.CreateAbsoluteUri(@"untitled:untitledFile"); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"untitled:untitledFile"); var request = new TestRequestTypeOne(new TextDocumentIdentifier { Uri = looseFileUri diff --git a/src/LanguageServer/ProtocolUnitTests/LanguageServerTargetTests.cs b/src/LanguageServer/ProtocolUnitTests/LanguageServerTargetTests.cs index b9faef82ff489..2df0109f0d22d 100644 --- a/src/LanguageServer/ProtocolUnitTests/LanguageServerTargetTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/LanguageServerTargetTests.cs @@ -93,7 +93,7 @@ public async Task LanguageServerSucceedsAfterInitializedCalled(bool mutatingLspW TextDocument = new TextDocumentItem { Text = "sometext", - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\location\file.json"), + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\location\file.json"), } }; @@ -111,7 +111,7 @@ public async Task LanguageServerRejectsRequestsBeforeInitialized(bool mutatingLs TextDocument = new TextDocumentItem { Text = "sometext", - Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\location\file.json"), + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\location\file.json"), } }; var ex = await Assert.ThrowsAsync(async () => await server.ExecuteRequestAsync(Methods.TextDocumentDidOpenName, didOpenParams, CancellationToken.None)); diff --git a/src/LanguageServer/ProtocolUnitTests/MapCode/MapCodeTests.cs b/src/LanguageServer/ProtocolUnitTests/MapCode/MapCodeTests.cs index 0aaf597dec428..5fc0cac555ce3 100644 --- a/src/LanguageServer/ProtocolUnitTests/MapCode/MapCodeTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/MapCode/MapCodeTests.cs @@ -128,7 +128,7 @@ static void Main(string[] args) Assert.NotNull(results.Changes); Assert.Null(results.DocumentChanges); - Assert.True(results.Changes!.TryGetValue(ProtocolConversions.GetDocumentFilePathFromUri(documentUri), out edits)); + Assert.True(results.Changes!.TryGetValue(ProtocolConversions.GetDocumentFilePathFromUri(documentUri.GetRequiredParsedUri()), out edits)); } var documentText = await document.GetTextAsync(); diff --git a/src/LanguageServer/ProtocolUnitTests/Metadata/LspMetadataAsSourceWorkspaceTests.cs b/src/LanguageServer/ProtocolUnitTests/Metadata/LspMetadataAsSourceWorkspaceTests.cs index 36e37923ab780..d8d84b5ccfa1f 100644 --- a/src/LanguageServer/ProtocolUnitTests/Metadata/LspMetadataAsSourceWorkspaceTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Metadata/LspMetadataAsSourceWorkspaceTests.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.MetadataAsSource; +using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; @@ -116,10 +117,10 @@ public static void WriteLine(string value) {} Assert.NotNull(definitionFromMetadata); Assert.NotEmpty(definitionFromMetadata); - Assert.Contains("String.cs", definitionFromMetadata.Single().Uri.LocalPath); + Assert.Contains("String.cs", definitionFromMetadata.Single().Uri.UriString); } - private static async Task GetWorkspaceForDocument(TestLspServer testLspServer, Uri fileUri) + private static async Task GetWorkspaceForDocument(TestLspServer testLspServer, DocumentUri fileUri) { var (lspWorkspace, _, _) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { Uri = fileUri }, CancellationToken.None); return lspWorkspace!; diff --git a/src/LanguageServer/ProtocolUnitTests/Miscellaneous/LspMiscellaneousFilesWorkspaceTests.cs b/src/LanguageServer/ProtocolUnitTests/Miscellaneous/LspMiscellaneousFilesWorkspaceTests.cs index be2a76a84c976..b8e8f780e4652 100644 --- a/src/LanguageServer/ProtocolUnitTests/Miscellaneous/LspMiscellaneousFilesWorkspaceTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Miscellaneous/LspMiscellaneousFilesWorkspaceTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; @@ -36,7 +37,7 @@ void M() Assert.Null(GetMiscellaneousDocument(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. - var looseFileUri = ProtocolConversions.CreateAbsoluteUri(@"C:\SomeFile.cs"); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); await testLspServer.OpenDocumentAsync(looseFileUri, source).ConfigureAwait(false); // Verify file is added to the misc file workspace. @@ -62,7 +63,7 @@ void M() Assert.Null(GetMiscellaneousDocument(testLspServer)); - var looseFileUri = ProtocolConversions.CreateAbsoluteUri(@"C:\SomeFile.cs"); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); // Open an empty loose file and make a request to verify it gets added to the misc workspace. await testLspServer.OpenDocumentAsync(looseFileUri, string.Empty).ConfigureAwait(false); @@ -100,7 +101,7 @@ void M() Assert.Null(GetMiscellaneousDocument(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. - var looseFileUri = ProtocolConversions.CreateAbsoluteUri(@"C:\SomeFile.cs"); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); await testLspServer.OpenDocumentAsync(looseFileUri, source).ConfigureAwait(false); await AssertFileInMiscWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); @@ -147,7 +148,7 @@ void M() // Open an empty loose file and make a request to verify it gets added to the misc workspace. // Include some Unicode characters to test URL handling. - var looseFileUri = ProtocolConversions.CreateAbsoluteUri("C:\\\ue25b\ud86d\udeac.cs"); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri("C:\\\ue25b\ud86d\udeac.cs"); var looseFileTextDocumentIdentifier = new LSP.TextDocumentIdentifier { Uri = looseFileUri }; await testLspServer.OpenDocumentAsync(looseFileUri, source).ConfigureAwait(false); @@ -158,7 +159,7 @@ void M() Contract.ThrowIfNull(miscDocument); Assert.True(miscWorkspace.CurrentSolution.ContainsDocument(miscDocument.Id)); - var documentPath = ProtocolConversions.GetDocumentFilePathFromUri(looseFileUri); + var documentPath = ProtocolConversions.GetDocumentFilePathFromUri(looseFileUri.GetRequiredParsedUri()); // Update the workspace to contain the loose file. var project = testLspServer.GetCurrentSolution().Projects.Single(); @@ -197,7 +198,7 @@ void M() Assert.Null(GetMiscellaneousDocument(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. - var looseFileUri = ProtocolConversions.CreateAbsoluteUri(@"C:\SomeFile.cs"); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); await testLspServer.OpenDocumentAsync(looseFileUri, source).ConfigureAwait(false); // Trigger a request and assert we got a file in the misc workspace. @@ -229,9 +230,7 @@ void M() // Open an empty loose file that hasn't been saved with a name. -#pragma warning disable RS0030 // Do not use banned APIs - var looseFileUri = new Uri("untitled:untitledFile"); -#pragma warning restore + var looseFileUri = new DocumentUri("untitled:untitledFile"); await testLspServer.OpenDocumentAsync(looseFileUri, source, languageId: "csharp").ConfigureAwait(false); @@ -262,9 +261,7 @@ void M() // Open an empty loose file that hasn't been saved with a name. -#pragma warning disable RS0030 // Do not use banned APIs - var looseFileUri = new Uri("untitled:untitledFile"); -#pragma warning restore + var looseFileUri = new DocumentUri("untitled:untitledFile"); await testLspServer.OpenDocumentAsync(looseFileUri, source, languageId: "csharp").ConfigureAwait(false); // Make an immediate followup request as soon as we queue the didOpen. @@ -292,13 +289,13 @@ void M() Assert.Equal(7, result.Single().Range.End.Character); } - private static async Task AssertFileInMiscWorkspaceAsync(TestLspServer testLspServer, Uri fileUri) + private static async Task AssertFileInMiscWorkspaceAsync(TestLspServer testLspServer, DocumentUri fileUri) { var (lspWorkspace, _, _) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { Uri = fileUri }, CancellationToken.None); Assert.Equal(testLspServer.GetManagerAccessor().GetLspMiscellaneousFilesWorkspace(), lspWorkspace); } - private static async Task AssertFileInMainWorkspaceAsync(TestLspServer testLspServer, Uri fileUri) + private static async Task AssertFileInMainWorkspaceAsync(TestLspServer testLspServer, DocumentUri fileUri) { var (lspWorkspace, _, _) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { Uri = fileUri }, CancellationToken.None).ConfigureAwait(false); Assert.Equal(testLspServer.TestWorkspace, lspWorkspace); diff --git a/src/LanguageServer/ProtocolUnitTests/Ordering/RequestOrderingTests.cs b/src/LanguageServer/ProtocolUnitTests/Ordering/RequestOrderingTests.cs index f9bf10d53f502..e975e0d51f4d0 100644 --- a/src/LanguageServer/ProtocolUnitTests/Ordering/RequestOrderingTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Ordering/RequestOrderingTests.cs @@ -252,7 +252,7 @@ public async Task HandlerThatSkipsBuildingLSPSolutionGetsWorkspaceSolution(bool Assert.Null(solution); } - private static async Task ExecuteDidOpen(TestLspServer testLspServer, Uri documentUri) + private static async Task ExecuteDidOpen(TestLspServer testLspServer, DocumentUri documentUri) { var didOpenParams = new LSP.DidOpenTextDocumentParams { diff --git a/src/LanguageServer/ProtocolUnitTests/ProjectContext/GetTextDocumentWithContextHandlerTests.cs b/src/LanguageServer/ProtocolUnitTests/ProjectContext/GetTextDocumentWithContextHandlerTests.cs index 44befe94053bb..5abeb4692ae40 100644 --- a/src/LanguageServer/ProtocolUnitTests/ProjectContext/GetTextDocumentWithContextHandlerTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/ProjectContext/GetTextDocumentWithContextHandlerTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -99,13 +100,13 @@ public async Task SwitchingContextsChangesDefaultContext(bool mutatingLspWorkspa } } - internal static async Task RunGetProjectContext(TestLspServer testLspServer, Uri uri) + internal static async Task RunGetProjectContext(TestLspServer testLspServer, DocumentUri uri) { return await testLspServer.ExecuteRequestAsync(LSP.VSMethods.GetProjectContextsName, CreateGetProjectContextParams(uri), cancellationToken: CancellationToken.None); } - private static LSP.VSGetProjectContextsParams CreateGetProjectContextParams(Uri uri) + private static LSP.VSGetProjectContextsParams CreateGetProjectContextParams(DocumentUri uri) => new LSP.VSGetProjectContextsParams() { TextDocument = new LSP.TextDocumentItem { Uri = uri } diff --git a/src/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs b/src/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs index c7f83533e1ade..4bda5874f00ff 100644 --- a/src/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs @@ -44,9 +44,9 @@ public void CreateAbsoluteUri_LocalPaths_AllAscii() Assert.Equal(expectedAbsoluteUri, ProtocolConversions.GetAbsoluteUriString(filePath)); - var uri = ProtocolConversions.CreateAbsoluteUri(filePath); - Assert.Equal(expectedAbsoluteUri, uri.AbsoluteUri); - Assert.Equal(filePath, uri.LocalPath); + var uri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); + Assert.Equal(expectedAbsoluteUri, uri.GetRequiredParsedUri().AbsoluteUri); + Assert.Equal(filePath, uri.GetRequiredParsedUri().LocalPath); } } @@ -71,9 +71,9 @@ public void CreateAbsoluteUri_LocalPaths_Windows(string filePath, string expecte { Assert.Equal(expectedAbsoluteUri, ProtocolConversions.GetAbsoluteUriString(filePath)); - var uri = ProtocolConversions.CreateAbsoluteUri(filePath); - Assert.Equal(expectedAbsoluteUri, uri.AbsoluteUri); - Assert.Equal(filePath.Replace('/', '\\'), uri.LocalPath); + var uri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); + Assert.Equal(expectedAbsoluteUri, uri.GetRequiredParsedUri().AbsoluteUri); + Assert.Equal(filePath.Replace('/', '\\'), uri.GetRequiredParsedUri().LocalPath); } [ConditionalTheory(typeof(WindowsOnly))] @@ -85,9 +85,9 @@ public void CreateAbsoluteUri_LocalPaths_Normalized_Windows(string filePath, str { Assert.Equal(expectedRawUri, ProtocolConversions.GetAbsoluteUriString(filePath)); - var uri = ProtocolConversions.CreateAbsoluteUri(filePath); - Assert.Equal(expectedNormalizedUri, uri.AbsoluteUri); - Assert.Equal(Path.GetFullPath(filePath).Replace('/', '\\'), uri.LocalPath); + var uri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); + Assert.Equal(expectedNormalizedUri, uri.GetRequiredParsedUri().AbsoluteUri); + Assert.Equal(Path.GetFullPath(filePath).Replace('/', '\\'), uri.GetRequiredParsedUri().LocalPath); } [ConditionalTheory(typeof(UnixLikeOnly))] @@ -105,9 +105,9 @@ public void CreateAbsoluteUri_LocalPaths_Unix(string filePath, string expectedAb { Assert.Equal(expectedAbsoluteUri, ProtocolConversions.GetAbsoluteUriString(filePath)); - var uri = ProtocolConversions.CreateAbsoluteUri(filePath); - Assert.Equal(expectedAbsoluteUri, uri.AbsoluteUri); - Assert.Equal(filePath, uri.LocalPath); + var uri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); + Assert.Equal(expectedAbsoluteUri, uri.GetRequiredParsedUri().AbsoluteUri); + Assert.Equal(filePath, uri.GetRequiredParsedUri().LocalPath); } [ConditionalTheory(typeof(WindowsOnly))] @@ -130,7 +130,7 @@ public void CreateAbsoluteUri_LocalPaths_Unix(string filePath, string expectedAb public void CreateRelativePatternBaseUri_LocalPaths_Windows(string filePath, string expectedUri) { var uri = ProtocolConversions.CreateRelativePatternBaseUri(filePath); - Assert.Equal(expectedUri, uri.AbsoluteUri); + Assert.Equal(expectedUri, uri.GetRequiredParsedUri().AbsoluteUri); } [ConditionalTheory(typeof(UnixLikeOnly))] @@ -148,7 +148,7 @@ public void CreateRelativePatternBaseUri_LocalPaths_Windows(string filePath, str public void CreateRelativePatternBaseUri_LocalPaths_Unix(string filePath, string expectedRelativeUri) { var uri = ProtocolConversions.CreateRelativePatternBaseUri(filePath); - Assert.Equal(expectedRelativeUri, uri.AbsoluteUri); + Assert.Equal(expectedRelativeUri, uri.GetRequiredParsedUri().AbsoluteUri); } [ConditionalTheory(typeof(UnixLikeOnly))] @@ -160,9 +160,9 @@ public void CreateAbsoluteUri_LocalPaths_Normalized_Unix(string filePath, string { Assert.Equal(expectedRawUri, ProtocolConversions.GetAbsoluteUriString(filePath)); - var uri = ProtocolConversions.CreateAbsoluteUri(filePath); - Assert.Equal(expectedNormalizedUri, uri.AbsoluteUri); - Assert.Equal(filePath, uri.LocalPath); + var uri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); + Assert.Equal(expectedNormalizedUri, uri.GetRequiredParsedUri().AbsoluteUri); + Assert.Equal(filePath, uri.GetRequiredParsedUri().LocalPath); } [Theory] @@ -171,7 +171,7 @@ public void CreateAbsoluteUri_LocalPaths_Normalized_Unix(string filePath, string [InlineData("xy://host/%2525%EE%89%9B/%C2%89%EC%9E%BD")] public void CreateAbsoluteUri_Urls(string url) { - Assert.Equal(url, ProtocolConversions.CreateAbsoluteUri(url).AbsoluteUri); + Assert.Equal(url, ProtocolConversions.CreateAbsoluteDocumentUri(url).GetRequiredParsedUri().AbsoluteUri); } [Fact] @@ -324,7 +324,7 @@ void M() await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Open an empty loose file. - var looseFileUri = ProtocolConversions.CreateAbsoluteUri(@"C:\SomeFile.cs"); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); await testLspServer.OpenDocumentAsync(looseFileUri, source).ConfigureAwait(false); var document = await GetTextDocumentAsync(testLspServer, looseFileUri); @@ -335,7 +335,7 @@ void M() Assert.True(projectContext.IsMiscellaneous); } - internal static async Task GetTextDocumentAsync(TestLspServer testLspServer, Uri uri) + internal static async Task GetTextDocumentAsync(TestLspServer testLspServer, DocumentUri uri) { var (_, _, textDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new TextDocumentIdentifier { Uri = uri }, CancellationToken.None); return textDocument; diff --git a/src/LanguageServer/ProtocolUnitTests/References/FindImplementationsTests.cs b/src/LanguageServer/ProtocolUnitTests/References/FindImplementationsTests.cs index a4ae9d8449b3f..931aeaf473de0 100644 --- a/src/LanguageServer/ProtocolUnitTests/References/FindImplementationsTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/References/FindImplementationsTests.cs @@ -91,7 +91,7 @@ void IA.M() var position = new LSP.Position { Line = 2, Character = 9 }; var results = await RunFindImplementationAsync(testLspServer, new LSP.Location { - Uri = ProtocolConversions.CreateAbsoluteUri($"C:\\{TestSpanMapper.GeneratedFileName}"), + Uri = ProtocolConversions.CreateAbsoluteDocumentUri($"C:\\{TestSpanMapper.GeneratedFileName}"), Range = new LSP.Range { Start = position, End = position } }); AssertLocationsEqual([TestSpanMapper.MappedFileLocation], results); diff --git a/src/LanguageServer/ProtocolUnitTests/RelatedDocuments/RelatedDocumentsTests.cs b/src/LanguageServer/ProtocolUnitTests/RelatedDocuments/RelatedDocumentsTests.cs index 6679d9089c729..7aaac23612b49 100644 --- a/src/LanguageServer/ProtocolUnitTests/RelatedDocuments/RelatedDocumentsTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/RelatedDocuments/RelatedDocumentsTests.cs @@ -21,7 +21,7 @@ public sealed class RelatedDocumentsTests(ITestOutputHelper testOutputHelper) { private static async Task RunGetRelatedDocumentsAsync( TestLspServer testLspServer, - Uri uri, + DocumentUri uri, bool useProgress = false) { BufferedProgress? progress = useProgress ? BufferedProgress.Create(null) : null; diff --git a/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs b/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs index 68c71fadb2528..63569e7bcc5dd 100644 --- a/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs @@ -170,7 +170,7 @@ void M2() var renameText = "RENAME"; var renameParams = CreateRenameParams(new LSP.Location { - Uri = ProtocolConversions.CreateAbsoluteUri($"C:\\{TestSpanMapper.GeneratedFileName}"), + Uri = ProtocolConversions.CreateAbsoluteDocumentUri($"C:\\{TestSpanMapper.GeneratedFileName}"), Range = new LSP.Range { Start = startPosition, End = endPosition } }, "RENAME"); diff --git a/src/LanguageServer/ProtocolUnitTests/SpellCheck/SpellCheckTests.cs b/src/LanguageServer/ProtocolUnitTests/SpellCheck/SpellCheckTests.cs index f9eaf873a3a5f..72078a6891fdf 100644 --- a/src/LanguageServer/ProtocolUnitTests/SpellCheck/SpellCheckTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/SpellCheck/SpellCheckTests.cs @@ -372,7 +372,7 @@ public async Task TestNoWorkspaceDiagnosticsForClosedFilesInProjectsWithIncorrec var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer); - Assert.True(results.All(r => r.TextDocument!.Uri.LocalPath == "C:\\C.cs")); + Assert.True(results.All(r => r.TextDocument!.Uri.GetRequiredParsedUri().LocalPath == "C:\\C.cs")); } // [Fact] @@ -599,7 +599,7 @@ private static Task CloseDocumentAsync(TestLspServer testLspServer, Document doc private static async Task RunGetDocumentSpellCheckSpansAsync( TestLspServer testLspServer, - Uri uri, + DocumentUri uri, string? previousResultId = null, bool useProgress = false) { @@ -622,7 +622,7 @@ private static async Task RunGetDocumentS private static async Task RunGetWorkspaceSpellCheckSpansAsync( TestLspServer testLspServer, - ImmutableArray<(string resultId, Uri uri)>? previousResults = null, + ImmutableArray<(string resultId, DocumentUri uri)>? previousResults = null, bool useProgress = false) { BufferedProgress? progress = useProgress ? BufferedProgress.Create(null) : null; @@ -654,7 +654,7 @@ private static async Task InsertTextAsync( } private static VSInternalDocumentSpellCheckableParams CreateDocumentParams( - Uri uri, + DocumentUri uri, string? previousResultId = null, IProgress? progress = null) { @@ -667,7 +667,7 @@ private static VSInternalDocumentSpellCheckableParams CreateDocumentParams( } private static VSInternalWorkspaceSpellCheckableParams CreateWorkspaceParams( - ImmutableArray<(string resultId, Uri uri)>? previousResults = null, + ImmutableArray<(string resultId, DocumentUri uri)>? previousResults = null, IProgress? progress = null) { return new VSInternalWorkspaceSpellCheckableParams @@ -677,7 +677,7 @@ private static VSInternalWorkspaceSpellCheckableParams CreateWorkspaceParams( }; } - private static ImmutableArray<(string resultId, Uri uri)> CreateParamsFromPreviousReports(VSInternalWorkspaceSpellCheckableReport[] results) + private static ImmutableArray<(string resultId, DocumentUri uri)> CreateParamsFromPreviousReports(VSInternalWorkspaceSpellCheckableReport[] results) { return [.. results.Select(r => (r.ResultId!, r.TextDocument.Uri))]; } diff --git a/src/LanguageServer/ProtocolUnitTests/UriTests.cs b/src/LanguageServer/ProtocolUnitTests/UriTests.cs index 8fe9ef3348d4b..ccf43b5a7dc5c 100644 --- a/src/LanguageServer/ProtocolUnitTests/UriTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/UriTests.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CommonLanguageServerProtocol.Framework; +using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -44,7 +45,7 @@ void M() await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Open an empty loose file with a file URI. - var looseFileUri = ProtocolConversions.CreateAbsoluteUri(filePath); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); await testLspServer.OpenDocumentAsync(looseFileUri, source, languageId: "csharp").ConfigureAwait(false); // Verify file is added to the misc file workspace. @@ -70,7 +71,7 @@ void M() await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Open an empty loose file that hasn't been saved with a name. - var looseFileUri = ProtocolConversions.CreateAbsoluteUri(@"untitled:untitledFile"); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"untitled:untitledFile"); await testLspServer.OpenDocumentAsync(looseFileUri, source, languageId: "csharp").ConfigureAwait(false); // Verify file is added to the misc file workspace. @@ -78,7 +79,7 @@ void M() Assert.True(workspace is LspMiscellaneousFilesWorkspace); AssertEx.NotNull(document); Assert.Equal(looseFileUri, document.GetURI()); - Assert.Equal(looseFileUri.OriginalString, document.FilePath); + Assert.Equal(looseFileUri.UriString, document.FilePath); } [Theory, CombinatorialData] @@ -100,7 +101,7 @@ public class A await using var testLspServer = await CreateXmlTestLspServerAsync(markup, mutatingLspWorkspace); var workspaceDocument = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single(); - var expectedDocumentUri = ProtocolConversions.CreateAbsoluteUri(documentFilePath); + var expectedDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(documentFilePath); await testLspServer.OpenDocumentAsync(expectedDocumentUri).ConfigureAwait(false); @@ -115,8 +116,8 @@ public class A // Try again, this time with a uri with different case sensitivity. This is supported, and is needed by Xaml. { - var lowercaseUri = ProtocolConversions.CreateAbsoluteUri(documentFilePath.ToLowerInvariant()); - Assert.NotEqual(expectedDocumentUri.AbsolutePath, lowercaseUri.AbsolutePath); + var lowercaseUri = ProtocolConversions.CreateAbsoluteDocumentUri(documentFilePath.ToLowerInvariant()); + Assert.NotEqual(expectedDocumentUri.GetRequiredParsedUri().AbsolutePath, lowercaseUri.GetRequiredParsedUri().AbsolutePath); var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { Uri = lowercaseUri }, CancellationToken.None); Assert.False(workspace is LspMiscellaneousFilesWorkspace); AssertEx.NotNull(document); @@ -138,9 +139,7 @@ public async Task TestWorkspaceDocument_WithFileAndGitScheme(bool mutatingLspWor // Add a git version of this document. Instead of "file://FILEPATH" the uri is "git://FILEPATH" -#pragma warning disable RS0030 // Do not use banned APIs - var gitDocumentUri = new Uri(fileDocumentUri.ToString().Replace("file", "git")); -#pragma warning restore + var gitDocumentUri = new DocumentUri(fileDocumentUri.ToString().Replace("file", "git")); var gitDocumentText = "GitText"; await testLspServer.OpenDocumentAsync(gitDocumentUri, gitDocumentText); @@ -191,9 +190,7 @@ public async Task TestFindsExistingDocumentWhenUriHasDifferentEncodingAsync(bool // Now make a request using the encoded document to ensure the server is able to find the document in misc C# files. var encodedUriString = @"git:/c:/Users/dabarbet/source/repos/ConsoleApp10/ConsoleApp10/Program.cs?%7B%7B%22path%22:%22c:%5C%5CUsers%5C%5Cdabarbet%5C%5Csource%5C%5Crepos%5C%5CConsoleApp10%5C%5CConsoleApp10%5C%5CProgram.cs%22,%22ref%22:%22~%22%7D%7D"; -#pragma warning disable RS0030 // Do not use banned APIs - var encodedUri = new Uri(encodedUriString, UriKind.Absolute); -#pragma warning restore RS0030 // Do not use banned APIs + var encodedUri = new DocumentUri(encodedUriString); var info = await testLspServer.ExecuteRequestAsync(CustomResolveHandler.MethodName, new CustomResolveParams(new LSP.TextDocumentIdentifier { Uri = encodedUri }), CancellationToken.None); Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace?.Kind); @@ -215,16 +212,14 @@ public async Task TestFindsExistingDocumentWhenUriHasDifferentCasingForCaseInsen { await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); -#pragma warning disable RS0030 // Do not use banned APIs - var upperCaseUri = new Uri(@"file:///C:/Users/dabarbet/source/repos/XUnitApp1/UnitTest1.cs", UriKind.Absolute); - var lowerCaseUri = new Uri(@"file:///c:/Users/dabarbet/source/repos/XUnitApp1/UnitTest1.cs", UriKind.Absolute); -#pragma warning restore RS0030 // Do not use banned APIs + var upperCaseUri = new DocumentUri(@"file:///C:/Users/dabarbet/source/repos/XUnitApp1/UnitTest1.cs"); + var lowerCaseUri = new DocumentUri(@"file:///c:/Users/dabarbet/source/repos/XUnitApp1/UnitTest1.cs"); // Execute the request as JSON directly to avoid the test client serializing System.Uri. var requestJson = $$$""" { "textDocument": { - "uri": "{{{upperCaseUri.OriginalString}}}", + "uri": "{{{upperCaseUri.UriString}}}", "languageId": "csharp", "text": "LSP text" } @@ -263,16 +258,14 @@ public async Task TestUsesDifferentDocumentForDifferentCaseWithNonUncUriAsync(bo { await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); -#pragma warning disable RS0030 // Do not use banned APIs - var upperCaseUri = new Uri(@"git:/Blah", UriKind.Absolute); - var lowerCaseUri = new Uri(@"git:/blah", UriKind.Absolute); -#pragma warning restore RS0030 // Do not use banned APIs + var upperCaseUri = new DocumentUri(@"git:/Blah"); + var lowerCaseUri = new DocumentUri(@"git:/blah"); // Execute the request as JSON directly to avoid the test client serializing System.Uri. var requestJson = $$$""" { "textDocument": { - "uri": "{{{upperCaseUri.OriginalString}}}", + "uri": "{{{upperCaseUri.UriString}}}", "languageId": "csharp", "text": "LSP text" } @@ -302,7 +295,7 @@ public async Task TestDoesNotCrashIfUnableToDetermineLanguageInfo(bool mutatingL await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Open an empty loose file that hasn't been saved with a name. - var looseFileUri = ProtocolConversions.CreateAbsoluteUri(@"untitled:untitledFile"); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"untitled:untitledFile"); await testLspServer.OpenDocumentAsync(looseFileUri, "hello", languageId: "csharp").ConfigureAwait(false); // Verify file is added to the misc file workspace. @@ -310,7 +303,7 @@ public async Task TestDoesNotCrashIfUnableToDetermineLanguageInfo(bool mutatingL Assert.True(workspace is LspMiscellaneousFilesWorkspace); AssertEx.NotNull(document); Assert.Equal(looseFileUri, document.GetURI()); - Assert.Equal(looseFileUri.OriginalString, document.FilePath); + Assert.Equal(looseFileUri.UriString, document.FilePath); // Close the document (deleting the saved language information) await testLspServer.CloseDocumentAsync(looseFileUri); @@ -323,6 +316,41 @@ await Assert.ThrowsAnyAsync(async () Assert.False(testLspServer.GetQueueAccessor()!.Value.IsComplete()); } + [Theory] + // Invalid URIs + [InlineData(true, "file://invalid^uri")] + [InlineData(false, "file://invalid^uri")] + [InlineData(true, "perforce://%239/some/file/here/source.cs")] + [InlineData(false, "perforce://%239/some/file/here/source.cs")] + // Valid URI, but System.Uri cannot parse it. + [InlineData(true, "vscode-notebook-cell://dev-container+7b2/workspaces/devkit-crash/notebook.ipynb")] + [InlineData(false, "vscode-notebook-cell://dev-container+7b2/workspaces/devkit-crash/notebook.ipynb")] + // Valid URI, but System.Uri cannot parse it. + [InlineData(true, "perforce://@=1454483/some/file/here/source.cs")] + [InlineData(false, "perforce://@=1454483/some/file/here/source.cs")] + public async Task TestOpenDocumentWithInvalidUri(bool mutatingLspWorkspace, string uriString) + { + // Create a server that supports LSP misc files + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + // Open file with a URI System.Uri cannot parse. This should not crash the server. + var invalidUri = new DocumentUri(uriString); + // ParsedUri should be null as System.Uri cannot parse it. + Assert.Null(invalidUri.ParsedUri); + await testLspServer.OpenDocumentAsync(invalidUri, string.Empty, languageId: "csharp").ConfigureAwait(false); + + // Verify requests succeed and that the file is in misc. + var info = await testLspServer.ExecuteRequestAsync(CustomResolveHandler.MethodName, + new CustomResolveParams(new LSP.TextDocumentIdentifier { Uri = invalidUri }), CancellationToken.None); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, info!.WorkspaceKind); + Assert.Equal(LanguageNames.CSharp, info.ProjectLanguage); + + // Verify we can modify the document in misc. + await testLspServer.InsertTextAsync(invalidUri, (0, 0, "hello")); + var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { Uri = invalidUri }, CancellationToken.None); + Assert.Equal("hello", (await document!.GetTextAsync()).ToString()); + } + private record class ResolvedDocumentInfo(string WorkspaceKind, string ProjectLanguage); private record class CustomResolveParams([property: JsonPropertyName("textDocument")] LSP.TextDocumentIdentifier TextDocument); diff --git a/src/LanguageServer/ProtocolUnitTests/VSTypeScriptHandlerTests.cs b/src/LanguageServer/ProtocolUnitTests/VSTypeScriptHandlerTests.cs index 043c8670f2733..e01b73c21b2b0 100644 --- a/src/LanguageServer/ProtocolUnitTests/VSTypeScriptHandlerTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/VSTypeScriptHandlerTests.cs @@ -21,6 +21,7 @@ using StreamJsonRpc; using Xunit; using Xunit.Abstractions; +using System.Text.Json.Serialization; namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; public class VSTypeScriptHandlerTests : AbstractLanguageServerProtocolTests @@ -127,7 +128,7 @@ private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Str return languageServer; } - internal record TSRequest(Uri Document, string Project); + internal record TSRequest([property: JsonConverter(typeof(DocumentUriConverter))] DocumentUri Document, string Project); [ExportTypeScriptLspServiceFactory(typeof(TypeScriptHandler)), PartNotDiscoverable, Shared] internal class TypeScriptHandlerFactory : AbstractVSTypeScriptRequestHandlerFactory @@ -157,7 +158,7 @@ internal class TypeScriptHandler : AbstractVSTypeScriptRequestHandler HandleRequestAsync(TSRequest request, TypeScriptRequestContext context, CancellationToken cancellationToken) diff --git a/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs b/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs index 5866d332036e1..09581c9205d34 100644 --- a/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Roslyn.Test.Utilities.TestGenerators; using Xunit; @@ -246,7 +247,7 @@ public async Task TestLspTransfersDocumentToNewWorkspaceAsync(bool mutatingLspWo // Include some Unicode characters to test URL handling. var newDocumentFilePath = "C:\\NewDoc\\\ue25b\ud86d\udeac.cs"; var newDocumentInfo = DocumentInfo.Create(newDocumentId, "NewDoc.cs", filePath: newDocumentFilePath, loader: new TestTextLoader("New Doc")); - var newDocumentUri = ProtocolConversions.CreateAbsoluteUri(newDocumentFilePath); + var newDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(newDocumentFilePath); // Open the document via LSP before the workspace sees it. await testLspServer.OpenDocumentAsync(newDocumentUri, "LSP text"); @@ -372,8 +373,8 @@ public async Task TestLspUpdatesCorrectWorkspaceWithMultipleWorkspacesAsync(bool Assert.True(IsWorkspaceRegistered(testLspServer.TestWorkspace, testLspServer)); Assert.True(IsWorkspaceRegistered(testWorkspaceTwo, testLspServer)); - var firstWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteUri(@"C:\FirstWorkspace.cs"); - var secondWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteUri(@"C:\SecondWorkspace.cs"); + var firstWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\FirstWorkspace.cs"); + var secondWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SecondWorkspace.cs"); await testLspServer.OpenDocumentAsync(firstWorkspaceDocumentUri); // Verify we can get both documents from their respective workspaces. @@ -427,8 +428,8 @@ public async Task TestWorkspaceEventUpdatesCorrectWorkspaceWithMultipleWorkspace // Wait for workspace operations to complete for the second workspace. await WaitForWorkspaceOperationsAsync(testWorkspaceTwo); - var firstWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteUri(@"C:\FirstWorkspace.cs"); - var secondWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteUri(@"C:\SecondWorkspace.cs"); + var firstWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\FirstWorkspace.cs"); + var secondWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SecondWorkspace.cs"); await testLspServer.OpenDocumentAsync(firstWorkspaceDocumentUri); // Verify we can get both documents from their respective workspaces. @@ -542,7 +543,7 @@ public async Task TestLspDocumentPreferredOverProjectSystemDocumentAddInMutating // Open the doc var filePath = "c:\\\ue25b\ud86d\udeac.cs"; - var documentUri = ProtocolConversions.CreateAbsoluteUri(filePath); + var documentUri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); await testLspServer.OpenDocumentAsync(documentUri, "Text"); // Initially the doc will be in the lsp misc workspace. @@ -750,7 +751,7 @@ public async Task TestForksWithRemovedGeneratorAsync(bool mutatingLspWorkspace) Assert.NotSame(testLspServer.TestWorkspace.CurrentSolution, sourceGeneratedDocument.Project.Solution); } - private static async Task OpenDocumentAndVerifyLspTextAsync(Uri documentUri, TestLspServer testLspServer, string openText = "LSP text") + private static async Task OpenDocumentAndVerifyLspTextAsync(DocumentUri documentUri, TestLspServer testLspServer, string openText = "LSP text") { await testLspServer.OpenDocumentAsync(documentUri, openText); @@ -766,7 +767,7 @@ private static bool IsWorkspaceRegistered(Workspace workspace, TestLspServer tes return testLspServer.GetManagerAccessor().IsWorkspaceRegistered(workspace); } - private static async Task<(Workspace? workspace, Document? document)> GetLspWorkspaceAndDocumentAsync(Uri uri, TestLspServer testLspServer) + private static async Task<(Workspace? workspace, Document? document)> GetLspWorkspaceAndDocumentAsync(DocumentUri uri, TestLspServer testLspServer) { var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(CreateTextDocumentIdentifier(uri), CancellationToken.None).ConfigureAwait(false); return (workspace, document as Document); diff --git a/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentUriTests.cs b/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentUriTests.cs index e71bedfbdb23e..4303d979695cc 100644 --- a/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentUriTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentUriTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -32,8 +32,8 @@ public async Task UrisRoundTrip() new SourceGeneratorIdentity("GeneratorAssembly", "Generator.dll", new Version(1, 0), "GeneratorType"), HintName); var uri = SourceGeneratedDocumentUri.Create(identity); - Assert.Equal(SourceGeneratedDocumentUri.Scheme, uri.Scheme); - var deserialized = SourceGeneratedDocumentUri.DeserializeIdentity(testLspServer.TestWorkspace.CurrentSolution, uri); + Assert.Equal(SourceGeneratedDocumentUri.Scheme, uri.GetRequiredParsedUri().Scheme); + var deserialized = SourceGeneratedDocumentUri.DeserializeIdentity(testLspServer.TestWorkspace.CurrentSolution, uri.GetRequiredParsedUri()); AssertEx.NotNull(deserialized); Assert.Equal(identity, deserialized.Value); @@ -41,4 +41,4 @@ public async Task UrisRoundTrip() // Debug name is not considered as a the usual part of equality, but we want to ensure we pass this through too Assert.Equal(generatedDocumentId.DebugName, deserialized.Value.DocumentId.DebugName); } -} \ No newline at end of file +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorCohostDocumentRequestHandler.cs b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorCohostDocumentRequestHandler.cs index 30fb43c5301d7..53a3706e7c329 100644 --- a/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorCohostDocumentRequestHandler.cs +++ b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorCohostDocumentRequestHandler.cs @@ -20,7 +20,7 @@ internal abstract class AbstractRazorCohostDocumentRequestHandler context.Method; - internal Uri? Uri => context.TextDocument?.GetURI(); + + [Obsolete("Use DocumentUri instead")] + internal Uri? Uri => context.TextDocument?.GetURI().GetRequiredParsedUri(); + internal DocumentUri? DocumentUri => context.TextDocument?.GetURI(); /// internal Workspace? Workspace => context.Workspace; /// diff --git a/src/Tools/ExternalAccess/Razor/RazorUri.cs b/src/Tools/ExternalAccess/Razor/RazorUri.cs index dd0e03df5dab1..f2366f1f09645 100644 --- a/src/Tools/ExternalAccess/Razor/RazorUri.cs +++ b/src/Tools/ExternalAccess/Razor/RazorUri.cs @@ -4,14 +4,19 @@ using System; using Microsoft.CodeAnalysis.LanguageServer; +using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; internal static class RazorUri { + [Obsolete("Use RazorUri.GetUriFromFilePath instead")] public static Uri CreateAbsoluteUri(string absolutePath) => ProtocolConversions.CreateAbsoluteUri(absolutePath); + public static DocumentUri CreateAbsoluteDocumentUri(string absolutePath) + => ProtocolConversions.CreateAbsoluteDocumentUri(absolutePath); + public static string GetDocumentFilePathFromUri(Uri uri) => ProtocolConversions.GetDocumentFilePathFromUri(uri); } diff --git a/src/Tools/ExternalAccess/Razor/SolutionExtensions.cs b/src/Tools/ExternalAccess/Razor/SolutionExtensions.cs index 358ea29bf613b..efff1002c6d9a 100644 --- a/src/Tools/ExternalAccess/Razor/SolutionExtensions.cs +++ b/src/Tools/ExternalAccess/Razor/SolutionExtensions.cs @@ -4,16 +4,17 @@ using System; using System.Collections.Immutable; +using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; internal static class SolutionExtensions { public static ImmutableArray GetTextDocuments(this Solution solution, Uri documentUri) - => LanguageServer.Extensions.GetTextDocuments(solution, documentUri); + => LanguageServer.Extensions.GetTextDocuments(solution, new(documentUri)); public static ImmutableArray GetDocumentIds(this Solution solution, Uri documentUri) - => LanguageServer.Extensions.GetDocumentIds(solution, documentUri); + => LanguageServer.Extensions.GetDocumentIds(solution, new(documentUri)); public static int GetWorkspaceVersion(this Solution solution) => solution.WorkspaceVersion; diff --git a/src/Tools/ExternalAccess/Razor/TextDocumentExtensions.cs b/src/Tools/ExternalAccess/Razor/TextDocumentExtensions.cs index d9ef4e5898b5a..41734d31af7fe 100644 --- a/src/Tools/ExternalAccess/Razor/TextDocumentExtensions.cs +++ b/src/Tools/ExternalAccess/Razor/TextDocumentExtensions.cs @@ -6,12 +6,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageServer; +using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; internal static class TextDocumentExtensions { public static Uri CreateUri(this TextDocument document) + => document.GetURI().GetRequiredParsedUri(); + + public static DocumentUri CreateDocumentUri(this TextDocument document) => document.GetURI(); public static async Task GetChecksumAsync(this TextDocument document, CancellationToken cancellationToken) diff --git a/src/Tools/ExternalAccess/Xaml/External/ResolveDataConversions.cs b/src/Tools/ExternalAccess/Xaml/External/ResolveDataConversions.cs index bec4c319fe13a..4def672c4537a 100644 --- a/src/Tools/ExternalAccess/Xaml/External/ResolveDataConversions.cs +++ b/src/Tools/ExternalAccess/Xaml/External/ResolveDataConversions.cs @@ -4,7 +4,9 @@ using System; using System.Text.Json; +using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; using LSP = Roslyn.LanguageServer.Protocol; @@ -16,20 +18,20 @@ private record DataResolveData(object Data, LSP.TextDocumentIdentifier Document) private record DataIdResolveData(long DataId, LSP.TextDocumentIdentifier Document) : DocumentResolveData(Document); public static object ToResolveData(object data, Uri uri) - => new DataResolveData(data, new LSP.TextDocumentIdentifier { Uri = uri }); + => new DataResolveData(data, new LSP.TextDocumentIdentifier { Uri = new(uri) }); public static (object? data, Uri? uri) FromResolveData(object? requestData) { Contract.ThrowIfNull(requestData); var resolveData = JsonSerializer.Deserialize((JsonElement)requestData); - return (resolveData?.Data, resolveData?.Document.Uri); + return (resolveData?.Data, resolveData?.Document.Uri.GetRequiredParsedUri()); } internal static object ToCachedResolveData(object data, Uri uri, ResolveDataCache resolveDataCache) { var dataId = resolveDataCache.UpdateCache(data); - return new DataIdResolveData(dataId, new LSP.TextDocumentIdentifier { Uri = uri }); + return new DataIdResolveData(dataId, new LSP.TextDocumentIdentifier { Uri = new(uri) }); } internal static (object? data, Uri? uri) FromCachedResolveData(object? lspData, ResolveDataCache resolveDataCache) @@ -48,6 +50,6 @@ internal static (object? data, Uri? uri) FromCachedResolveData(object? lspData, var data = resolveDataCache.GetCachedEntry(resolveData.DataId); var document = resolveData.Document; - return (data, document.Uri); + return (data, document.Uri.GetRequiredParsedUri()); } } diff --git a/src/Tools/ExternalAccess/Xaml/External/XamlRequestHandlerBase.cs b/src/Tools/ExternalAccess/Xaml/External/XamlRequestHandlerBase.cs index 847df9d9dcc31..7256368655b43 100644 --- a/src/Tools/ExternalAccess/Xaml/External/XamlRequestHandlerBase.cs +++ b/src/Tools/ExternalAccess/Xaml/External/XamlRequestHandlerBase.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Roslyn.LanguageServer.Protocol; using LSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.ExternalAccess.Xaml; @@ -24,7 +25,7 @@ public XamlRequestHandlerBase(IXamlRequestHandler? xamlRequ public bool RequiresLSPSolution => true; public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(TRequest request) - => new() { Uri = GetTextDocumentUri(request) }; + => new() { Uri = new(GetTextDocumentUri(request)) }; public abstract Uri GetTextDocumentUri(TRequest request); diff --git a/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs b/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs index 0559d1c49b659..bc93359b89082 100644 --- a/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs +++ b/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs @@ -46,7 +46,7 @@ internal sealed partial class DocumentOutlineViewModel { TextDocument = new TextDocumentIdentifier { - Uri = ProtocolConversions.CreateAbsoluteUri(textViewFilePath), + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(textViewFilePath), }, UseHierarchicalSymbols = true }; diff --git a/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchResultViewFactory.cs b/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchResultViewFactory.cs index 04f24b82d0579..219c53b9db340 100644 --- a/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchResultViewFactory.cs +++ b/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchResultViewFactory.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Search.Data; +using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.NavigateTo; @@ -66,7 +67,7 @@ public Task> GetPreviewPanelsAsync(S Uri? absoluteUri; if (document.SourceGeneratedDocumentIdentity is not null) { - absoluteUri = SourceGeneratedDocumentUri.Create(document.SourceGeneratedDocumentIdentity.Value); + absoluteUri = SourceGeneratedDocumentUri.Create(document.SourceGeneratedDocumentIdentity.Value).GetRequiredParsedUri(); } else { @@ -89,6 +90,7 @@ public Task> GetPreviewPanelsAsync(S { new RoslynSearchResultPreviewPanel( _provider, + // Editor APIs require a parseable System.Uri instance absoluteUri, projectGuid, roslynResult.SearchResult.NavigableItem.SourceSpan.ToSpan(), diff --git a/src/VisualStudio/LiveShare/Impl/Client/RemoteLanguageServiceWorkspace.cs b/src/VisualStudio/LiveShare/Impl/Client/RemoteLanguageServiceWorkspace.cs index 93b7878aa2496..817967df6281b 100644 --- a/src/VisualStudio/LiveShare/Impl/Client/RemoteLanguageServiceWorkspace.cs +++ b/src/VisualStudio/LiveShare/Impl/Client/RemoteLanguageServiceWorkspace.cs @@ -287,7 +287,12 @@ public async Task RefreshAllFilesAsync() public async Task GetDocumentSpanFromLocationAsync(LSP.Location location, CancellationToken cancellationToken) { - var document = GetOrAddDocument(location.Uri.LocalPath); + if (location.Uri.ParsedUri is null) + { + return null; + } + + var document = GetOrAddDocument(location.Uri.ParsedUri.LocalPath); if (document == null) { return null; diff --git a/src/VisualStudio/LiveShare/Test/ProjectsHandlerTests.cs b/src/VisualStudio/LiveShare/Test/ProjectsHandlerTests.cs index bf830d1fd3bae..9f89d1b453f05 100644 --- a/src/VisualStudio/LiveShare/Test/ProjectsHandlerTests.cs +++ b/src/VisualStudio/LiveShare/Test/ProjectsHandlerTests.cs @@ -35,7 +35,7 @@ private static CustomProtocol.Project CreateLspProject(Project project) { Language = project.Language, Name = project.Name, - SourceFiles = [.. project.Documents.Select(document => document.GetURI())] + SourceFiles = [.. project.Documents.Select(document => document.GetURI().GetRequiredParsedUri())] }; } } diff --git a/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs b/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs index 6511946b2de07..28bd323a8d0e5 100644 --- a/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs +++ b/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs @@ -115,7 +115,7 @@ public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileSe if (sourceDefinition.Span != null) { // If the Span is not null, use the span. - var document = await solution.GetTextDocumentAsync(new TextDocumentIdentifier { Uri = ProtocolConversions.CreateAbsoluteUri(sourceDefinition.FilePath) }, cancellationToken).ConfigureAwait(false); + var document = await solution.GetTextDocumentAsync(new TextDocumentIdentifier { Uri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceDefinition.FilePath) }, cancellationToken).ConfigureAwait(false); if (document != null) { return await ProtocolConversions.TextSpanToLocationAsync( @@ -132,7 +132,7 @@ public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileSe var sourceText = SourceText.From(fileStream); return new LSP.Location { - Uri = new Uri(sourceDefinition.FilePath), + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceDefinition.FilePath), Range = ProtocolConversions.TextSpanToRange(sourceDefinition.Span.Value, sourceText) }; } @@ -144,7 +144,7 @@ public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileSe return new LSP.Location { - Uri = new Uri(sourceDefinition.FilePath), + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceDefinition.FilePath), Range = new LSP.Range() { Start = position, End = position } }; } @@ -182,7 +182,7 @@ public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileSe var linePosSpan = declarationFile.IdentifierLocation.GetLineSpan().Span; locations.Add(new LSP.Location { - Uri = new Uri(declarationFile.FilePath), + Uri = ProtocolConversions.CreateAbsoluteDocumentUri(declarationFile.FilePath), Range = ProtocolConversions.LinePositionToRange(linePosSpan), }); } diff --git a/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/XamlRequestExecutionQueue.cs b/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/XamlRequestExecutionQueue.cs index d973ca8e34384..d609e3cd0ddc1 100644 --- a/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/XamlRequestExecutionQueue.cs +++ b/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/XamlRequestExecutionQueue.cs @@ -25,8 +25,7 @@ public XamlRequestExecutionQueue( protected internal override void BeforeRequest(TRequest request) { - if (request is ITextDocumentParams textDocumentParams && - textDocumentParams.TextDocument is { Uri: { IsAbsoluteUri: true } documentUri }) + if (request is ITextDocumentParams textDocumentParams && textDocumentParams.TextDocument.Uri.ParsedUri is Uri documentUri && documentUri.IsAbsoluteUri) { _projectService.TrackOpenDocument(documentUri.LocalPath); }