diff --git a/src/VisualStudio/Core/Def/Implementation/CodeLens/RemoteCodeLensReferencesService.cs b/src/VisualStudio/Core/Def/Implementation/CodeLens/RemoteCodeLensReferencesService.cs index faa2c4bb96bf1f3b1f08cfdd24bd131b7a0439a6..51cf7077bdb2f444fa1c28ee1b37c4d4c0fcd127 100644 --- a/src/VisualStudio/Core/Def/Implementation/CodeLens/RemoteCodeLensReferencesService.cs +++ b/src/VisualStudio/Core/Def/Implementation/CodeLens/RemoteCodeLensReferencesService.cs @@ -169,6 +169,11 @@ public RemoteCodeLensReferencesService() } var excerpter = document.Services.GetService(); + if (excerpter == null) + { + continue; + } + var referenceExcerpt = await excerpter.TryExcerptAsync(document, span, ExcerptMode.SingleLine, cancellationToken).ConfigureAwait(false); var tooltipExcerpt = await excerpter.TryExcerptAsync(document, span, ExcerptMode.Tooltip, cancellationToken).ConfigureAwait(false); diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/TextEditApplication.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/TextEditApplication.cs index 343342fb72d1b517cee1835b6e9593295e4e01e2..4155be4feb94f1331fdddd461b9de3e2bf14e92e 100644 --- a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/TextEditApplication.cs +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/TextEditApplication.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#nullable enable + +using System.Collections.Immutable; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Undo; using Microsoft.CodeAnalysis.Text; @@ -13,17 +16,30 @@ internal static class TextEditApplication { internal static void UpdateText(SourceText newText, ITextBuffer buffer, EditOptions options) { - using var edit = buffer.CreateEdit(options, reiteratedVersionNumber: null, editTag: null); var oldSnapshot = buffer.CurrentSnapshot; var oldText = oldSnapshot.AsText(); var changes = newText.GetTextChanges(oldText); + UpdateText(changes.ToImmutableArray(), buffer, oldSnapshot, oldText, options); + } + + public static void UpdateText(ImmutableArray textChanges, ITextBuffer buffer, EditOptions options) + { + var oldSnapshot = buffer.CurrentSnapshot; + var oldText = oldSnapshot.AsText(); + + UpdateText(textChanges, buffer, oldSnapshot, oldText, options); + } + + private static void UpdateText(ImmutableArray textChanges, ITextBuffer buffer, ITextSnapshot oldSnapshot, SourceText oldText, EditOptions options) + { + using var edit = buffer.CreateEdit(options, reiteratedVersionNumber: null, editTag: null); if (CodeAnalysis.Workspace.TryGetWorkspace(oldText.Container, out var workspace)) { - var undoService = workspace.Services.GetService(); + var undoService = workspace.Services.GetRequiredService(); undoService.BeginUndoTransaction(oldSnapshot); } - foreach (var change in changes) + foreach (var change in textChanges) { edit.Replace(change.Span.Start, change.Span.Length, change.NewText); } diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs index 43831ef281bbdbe0e1e9b20ec8e54dcc2ad91fe3..897a01a2b9f5d11b9670a815654fbdd4167f7157 100644 --- a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs @@ -653,6 +653,76 @@ protected override void ApplyAnalyzerReferenceRemoved(ProjectId projectId, Analy } } + internal override void ApplyMappedFileChanges(SolutionChanges solutionChanges) + { + // Get the original text changes from all documents and call the span mapping service to get span mappings for the text changes. + // Create mapped text changes using the mapped spans and original text changes' text. + + // Mappings for opened razor files are retrieved via the LSP client making a request to the razor server. + // If we wait for the result on the UI thread, we will hit a bug in the LSP client that brings us to a code path + // using ConfigureAwait(true). This deadlocks as it then attempts to return to the UI thread which is already blocked by us. + // Instead, we invoke this in JTF run which will mitigate deadlocks when the ConfigureAwait(true) + // tries to switch back to the main thread in the LSP client. + // Link to LSP client bug for ConfigureAwait(true) - https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1216657 + var mappedChanges = _threadingContext.JoinableTaskFactory.Run(() => GetMappedTextChanges(solutionChanges)); + + // Group the mapped text changes by file, then apply all mapped text changes for the file. + foreach (var changesForFile in mappedChanges) + { + // It doesn't matter which of the file's projectIds we pass to the invisible editor, so just pick the first. + var projectId = changesForFile.Value.First().ProjectId; + // Make sure we only take distinct changes - we'll have duplicates from different projects for linked files or multi-targeted files. + var distinctTextChanges = changesForFile.Value.Select(change => change.TextChange).Distinct().ToImmutableArray(); + using var invisibleEditor = new InvisibleEditor(ServiceProvider.GlobalProvider, changesForFile.Key, GetHierarchy(projectId), needsSave: true, needsUndoDisabled: false); + TextEditApplication.UpdateText(distinctTextChanges, invisibleEditor.TextBuffer, EditOptions.None); + } + + return; + + async Task> GetMappedTextChanges(SolutionChanges solutionChanges) + { + var filePathToMappedTextChanges = new MultiDictionary(); + foreach (var projectChanges in solutionChanges.GetProjectChanges()) + { + foreach (var changedDocumentId in projectChanges.GetChangedDocuments()) + { + var oldDocument = projectChanges.OldProject.GetRequiredDocument(changedDocumentId); + if (!ShouldApplyChangesToMappedDocuments(oldDocument, out var mappingService)) + { + continue; + } + + var newDocument = projectChanges.NewProject.GetRequiredDocument(changedDocumentId); + var textChanges = (await newDocument.GetTextChangesAsync(oldDocument, CancellationToken.None).ConfigureAwait(false)).ToImmutableArray(); + var mappedSpanResults = await mappingService.MapSpansAsync(oldDocument, textChanges.Select(tc => tc.Span), CancellationToken.None).ConfigureAwait(false); + + Contract.ThrowIfFalse(mappedSpanResults.Length == textChanges.Length); + + for (var i = 0; i < mappedSpanResults.Length; i++) + { + // Only include changes that could be mapped. + var newText = textChanges[i].NewText; + if (!mappedSpanResults[i].IsDefault && newText != null) + { + var newTextChange = new TextChange(mappedSpanResults[i].Span, newText); + filePathToMappedTextChanges.Add(mappedSpanResults[i].FilePath, (newTextChange, projectChanges.ProjectId)); + } + } + } + } + + return filePathToMappedTextChanges; + } + + bool ShouldApplyChangesToMappedDocuments(CodeAnalysis.Document document, [NotNullWhen(true)] out ISpanMappingService? spanMappingService) + { + spanMappingService = document.Services.GetService(); + // Only consider files that are mapped and that we are unable to apply changes to. + // TODO - refactor how this is determined - https://github.com/dotnet/roslyn/issues/47908 + return spanMappingService != null && document?.CanApplyChange() == false; + } + } + protected override void ApplyProjectReferenceAdded( ProjectId projectId, ProjectReference projectReference) { diff --git a/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentNavigationService.cs b/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentNavigationService.cs index 7792651a7787d9b9b53503bfa1ca8f2e1f5afc23..2450b1543317c14c13a88430d68f82710a38399b 100644 --- a/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentNavigationService.cs +++ b/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentNavigationService.cs @@ -40,6 +40,7 @@ internal sealed class VisualStudioDocumentNavigationService : ForegroundThreadAf private readonly IServiceProvider _serviceProvider; private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactoryService; private readonly IVsRunningDocumentTable4 _runningDocumentTable; + private readonly IThreadingContext _threadingContext; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] @@ -52,6 +53,7 @@ internal sealed class VisualStudioDocumentNavigationService : ForegroundThreadAf _serviceProvider = serviceProvider; _editorAdaptersFactoryService = editorAdaptersFactoryService; _runningDocumentTable = (IVsRunningDocumentTable4)serviceProvider.GetService(typeof(SVsRunningDocumentTable)); + _threadingContext = threadingContext; } public bool CanNavigateToSpan(Workspace workspace, DocumentId documentId, TextSpan textSpan) @@ -303,12 +305,15 @@ private bool TryNavigateToMappedFile(Workspace workspace, Document generatedDocu return false; } - private static MappedSpanResult? GetMappedSpan(ISpanMappingService spanMappingService, Document generatedDocument, TextSpan textSpan) + private MappedSpanResult? GetMappedSpan(ISpanMappingService spanMappingService, Document generatedDocument, TextSpan textSpan) { - var results = System.Threading.Tasks.Task.Run(async () => - { - return await spanMappingService.MapSpansAsync(generatedDocument, SpecializedCollections.SingletonEnumerable(textSpan), CancellationToken.None).ConfigureAwait(true); - }).WaitAndGetResult(CancellationToken.None); + // Mappings for opened razor files are retrieved via the LSP client making a request to the razor server. + // If we wait for the result on the UI thread, we will hit a bug in the LSP client that brings us to a code path + // using ConfigureAwait(true). This deadlocks as it then attempts to return to the UI thread which is already blocked by us. + // Instead, we invoke this in JTF run which will mitigate deadlocks when the ConfigureAwait(true) + // tries to switch back to the main thread in the LSP client. + // Link to LSP client bug for ConfigureAwait(true) - https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1216657 + var results = _threadingContext.JoinableTaskFactory.Run(() => spanMappingService.MapSpansAsync(generatedDocument, SpecializedCollections.SingletonEnumerable(textSpan), CancellationToken.None)); if (!results.IsDefaultOrEmpty) { diff --git a/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/Extensions.cs b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/Extensions.cs index 7b219e0224479f7c4c95b5660ed5ae90f8e340a8..9690c79a1c558f81d216262c7f3929e1d84016ef 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/Extensions.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/Extensions.cs @@ -14,12 +14,12 @@ public static bool CanApplyChange([NotNullWhen(returnValue: true)] this TextDocu => document?.State.CanApplyChange() ?? false; public static bool CanApplyChange([NotNullWhen(returnValue: true)] this TextDocumentState? document) - => document?.Services.GetService().CanApplyChange ?? false; + => document?.Services.GetService()?.CanApplyChange ?? false; public static bool SupportsDiagnostics([NotNullWhen(returnValue: true)] this TextDocument? document) => document?.State.SupportsDiagnostics() ?? false; public static bool SupportsDiagnostics([NotNullWhen(returnValue: true)] this TextDocumentState? document) - => document?.Services.GetService().SupportDiagnostics ?? false; + => document?.Services.GetService()?.SupportDiagnostics ?? false; } } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/IDocumentServiceProvider.cs b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/IDocumentServiceProvider.cs index dacbf6b3120a758be031de23b580431c91172e5a..3fca4684de8d0b7b3d91943aa8e2193f47e408f7 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/IDocumentServiceProvider.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/IDocumentServiceProvider.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#nullable enable + namespace Microsoft.CodeAnalysis.Host { internal interface IDocumentServiceProvider @@ -10,6 +12,6 @@ internal interface IDocumentServiceProvider /// Gets a document specific service provided by the host identified by the service type. /// If the host does not provide the service, this method returns null. /// - TService GetService() where TService : class, IDocumentService; + TService? GetService() where TService : class, IDocumentService; } } diff --git a/src/Workspaces/Core/Portable/Workspace/Workspace.cs b/src/Workspaces/Core/Portable/Workspace/Workspace.cs index 3961220f3c068786d3e0a628bff259ff7738e260..a2a9dbcf84e76180a20d3298b56719e4aa152c21 100644 --- a/src/Workspaces/Core/Portable/Workspace/Workspace.cs +++ b/src/Workspaces/Core/Portable/Workspace/Workspace.cs @@ -1220,6 +1220,9 @@ internal virtual bool TryApplyChanges(Solution newSolution, IProgressTracker pro progressTracker.ItemCompleted(); } + // changes in mapped files outside the workspace (may span multiple projects) + this.ApplyMappedFileChanges(solutionChanges); + // removed projects foreach (var proj in solutionChanges.GetRemovedProjects()) { @@ -1248,6 +1251,11 @@ internal virtual bool TryApplyChanges(Solution newSolution, IProgressTracker pro } } + internal virtual void ApplyMappedFileChanges(SolutionChanges solutionChanges) + { + return; + } + private void CheckAllowedSolutionChanges(SolutionChanges solutionChanges) { if (solutionChanges.GetRemovedProjects().Any() && !this.CanApplyChange(ApplyChangesKind.RemoveProject))