|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Editor.Implementation.TextDiffing;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Preview;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.CodeAnalysis.Utilities;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Differencing;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.Implementation.Preview;
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal abstract class AbstractPreviewFactoryService<TDifferenceViewer>(
IThreadingContext threadingContext,
ITextBufferFactoryService textBufferFactoryService,
ITextBufferCloneService textBufferCloneService,
IContentTypeRegistryService contentTypeRegistryService,
IProjectionBufferFactoryService projectionBufferFactoryService,
EditorOptionsService editorOptionsService,
ITextDifferencingSelectorService differenceSelectorService,
IDifferenceBufferFactoryService differenceBufferService,
ITextDocumentFactoryService textDocumentFactoryService,
ITextViewRoleSet previewRoleSet) : IPreviewFactoryService
where TDifferenceViewer : IDifferenceViewer
{
private const double DefaultZoomLevel = 0.75;
private readonly ITextViewRoleSet _previewRoleSet = previewRoleSet;
private readonly ITextBufferFactoryService _textBufferFactoryService = textBufferFactoryService;
private readonly ITextBufferCloneService _textBufferCloneService = textBufferCloneService;
private readonly IContentTypeRegistryService _contentTypeRegistryService = contentTypeRegistryService;
private readonly IProjectionBufferFactoryService _projectionBufferFactoryService = projectionBufferFactoryService;
private readonly EditorOptionsService _editorOptionsService = editorOptionsService;
private readonly ITextDifferencingSelectorService _differenceSelectorService = differenceSelectorService;
private readonly IDifferenceBufferFactoryService _differenceBufferService = differenceBufferService;
private readonly ITextDocumentFactoryService _textDocumentFactoryService = textDocumentFactoryService;
private static readonly StringDifferenceOptions s_differenceOptions = new()
{
DifferenceType = StringDifferenceTypes.Word | StringDifferenceTypes.Line,
};
protected readonly IThreadingContext ThreadingContext = threadingContext;
public SolutionPreviewResult? GetSolutionPreviews(Solution oldSolution, Solution? newSolution, CancellationToken cancellationToken)
=> GetSolutionPreviews(oldSolution, newSolution, DefaultZoomLevel, cancellationToken);
public SolutionPreviewResult? GetSolutionPreviews(Solution oldSolution, Solution? newSolution, double zoomLevel, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// Note: The order in which previews are added to the below list is significant.
// Preview for a changed document is preferred over preview for changed references and so on.
var previewItems = new List<SolutionPreviewItem>();
SolutionChangeSummary? changeSummary = null;
if (newSolution != null)
{
var solutionChanges = newSolution.GetChanges(oldSolution);
var ignoreUnchangeableDocuments = oldSolution.Workspace.IgnoreUnchangeableDocumentsWhenApplyingChanges;
foreach (var projectChanges in solutionChanges.GetProjectChanges())
{
cancellationToken.ThrowIfCancellationRequested();
var projectId = projectChanges.ProjectId;
var oldProject = projectChanges.OldProject;
var newProject = projectChanges.NewProject;
// Exclude changes to unchangeable documents if they will be ignored when applied to workspace.
foreach (var documentId in projectChanges.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true, ignoreUnchangeableDocuments))
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c =>
await CreateChangedDocumentPreviewViewAsync(oldSolution.GetRequiredDocument(documentId), newSolution.GetRequiredDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline()));
}
foreach (var documentId in projectChanges.GetAddedDocuments())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c =>
await CreateAddedDocumentPreviewViewAsync(newSolution.GetRequiredDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline()));
}
foreach (var documentId in projectChanges.GetRemovedDocuments())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(oldProject.Id, documentId, async c =>
await CreateRemovedDocumentPreviewViewAsync(oldSolution.GetRequiredDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline()));
}
foreach (var documentId in projectChanges.GetChangedAdditionalDocuments())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c =>
await CreateChangedAdditionalDocumentPreviewViewAsync(oldSolution.GetRequiredAdditionalDocument(documentId), newSolution.GetRequiredAdditionalDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline()));
}
foreach (var documentId in projectChanges.GetAddedAdditionalDocuments())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c =>
await CreateAddedAdditionalDocumentPreviewViewAsync(newSolution.GetRequiredAdditionalDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline()));
}
foreach (var documentId in projectChanges.GetRemovedAdditionalDocuments())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(oldProject.Id, documentId, async c =>
await CreateRemovedAdditionalDocumentPreviewViewAsync(oldSolution.GetRequiredAdditionalDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline()));
}
foreach (var documentId in projectChanges.GetChangedAnalyzerConfigDocuments())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c =>
await CreateChangedAnalyzerConfigDocumentPreviewViewAsync(oldSolution.GetRequiredAnalyzerConfigDocument(documentId), newSolution.GetRequiredAnalyzerConfigDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline()));
}
foreach (var documentId in projectChanges.GetAddedAnalyzerConfigDocuments())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(documentId.ProjectId, documentId, async c =>
await CreateAddedAnalyzerConfigDocumentPreviewViewAsync(newSolution.GetRequiredAnalyzerConfigDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline()));
}
foreach (var documentId in projectChanges.GetRemovedAnalyzerConfigDocuments())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(oldProject.Id, documentId, async c =>
await CreateRemovedAnalyzerConfigDocumentPreviewViewAsync(oldSolution.GetRequiredAnalyzerConfigDocument(documentId), zoomLevel, c).ConfigureAwaitRunInline()));
}
foreach (var metadataReference in projectChanges.GetAddedMetadataReferences())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(oldProject.Id, null,
string.Format(EditorFeaturesResources.Adding_reference_0_to_1, metadataReference.Display, oldProject.Name)));
}
foreach (var metadataReference in projectChanges.GetRemovedMetadataReferences())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(oldProject.Id, null,
string.Format(EditorFeaturesResources.Removing_reference_0_from_1, metadataReference.Display, oldProject.Name)));
}
foreach (var projectReference in projectChanges.GetAddedProjectReferences())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(oldProject.Id, null,
string.Format(EditorFeaturesResources.Adding_reference_0_to_1, newSolution.GetRequiredProject(projectReference.ProjectId).Name, oldProject.Name)));
}
foreach (var projectReference in projectChanges.GetRemovedProjectReferences())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(oldProject.Id, null,
string.Format(EditorFeaturesResources.Removing_reference_0_from_1, oldSolution.GetRequiredProject(projectReference.ProjectId).Name, oldProject.Name)));
}
foreach (var analyzer in projectChanges.GetAddedAnalyzerReferences())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(oldProject.Id, null,
string.Format(EditorFeaturesResources.Adding_analyzer_reference_0_to_1, analyzer.Display, oldProject.Name)));
}
foreach (var analyzer in projectChanges.GetRemovedAnalyzerReferences())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(oldProject.Id, null,
string.Format(EditorFeaturesResources.Removing_analyzer_reference_0_from_1, analyzer.Display, oldProject.Name)));
}
}
foreach (var project in solutionChanges.GetAddedProjects())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(project.Id, null,
string.Format(EditorFeaturesResources.Adding_project_0, project.Name)));
}
foreach (var project in solutionChanges.GetRemovedProjects())
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(project.Id, null,
string.Format(EditorFeaturesResources.Removing_project_0, project.Name)));
}
foreach (var projectChanges in solutionChanges.GetProjectChanges().Where(ProjectReferencesChanged))
{
cancellationToken.ThrowIfCancellationRequested();
previewItems.Add(new SolutionPreviewItem(projectChanges.OldProject.Id, null,
string.Format(EditorFeaturesResources.Changing_project_references_for_0, projectChanges.OldProject.Name)));
}
changeSummary = new SolutionChangeSummary(oldSolution, newSolution, solutionChanges);
}
return new SolutionPreviewResult(ThreadingContext, previewItems, changeSummary);
}
private bool ProjectReferencesChanged(ProjectChanges projectChanges)
{
var oldProjectReferences = projectChanges.OldProject.ProjectReferences.ToDictionary(r => r.ProjectId);
var newProjectReferences = projectChanges.NewProject.ProjectReferences.ToDictionary(r => r.ProjectId);
// These are the set of project reference that remained in the project. We don't care
// about project references that were added or removed. Those will already be reported.
var preservedProjectIds = oldProjectReferences.Keys.Intersect(newProjectReferences.Keys);
foreach (var projectId in preservedProjectIds)
{
var oldProjectReference = oldProjectReferences[projectId];
var newProjectReference = newProjectReferences[projectId];
if (!oldProjectReference.Equals(newProjectReference))
{
return true;
}
}
return false;
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>> CreateAddedDocumentPreviewViewAsync(Document document, CancellationToken cancellationToken)
=> CreateAddedDocumentPreviewViewAsync(document, DefaultZoomLevel, cancellationToken);
private async ValueTask<IDifferenceViewerPreview<TDifferenceViewer>> CreateAddedDocumentPreviewViewCoreAsync(ITextDocument newEditorDocument, ReferenceCountedDisposable<PreviewWorkspace> workspace, TextDocument document, double zoomLevel, CancellationToken cancellationToken)
{
// IProjectionBufferFactoryService is a Visual Studio API which is not documented as free-threaded
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var firstLine = string.Format(EditorFeaturesResources.Adding_0_to_1_with_content_colon,
document.Name, document.Project.Name);
var originalBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer(
sourceSpans: [firstLine, "\r\n"], registryService: _contentTypeRegistryService);
var span = new SnapshotSpan(newEditorDocument.TextBuffer.CurrentSnapshot, Span.FromBounds(0, newEditorDocument.TextBuffer.CurrentSnapshot.Length))
.CreateTrackingSpan(SpanTrackingMode.EdgeExclusive);
var changedBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer(
sourceSpans: [firstLine, "\r\n", span], registryService: _contentTypeRegistryService);
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
return await CreateNewDifferenceViewerAsync(null, workspace, originalBuffer, changedBuffer, [newEditorDocument], zoomLevel, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
private async Task<IDifferenceViewerPreview<TDifferenceViewer>> CreateAddedTextDocumentPreviewViewAsync<TDocument>(
TDocument document,
double zoomLevel,
Func<TDocument, CancellationToken, ValueTask<ITextDocument>> createEditorDocumentAsync,
CancellationToken cancellationToken)
where TDocument : TextDocument
{
// createBufferAsync must be called from the main thread
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
var newEditorDocument = await createEditorDocumentAsync(document, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
// Create PreviewWorkspace around the buffer to be displayed in the diff preview
// so that all IDE services (colorizer, squiggles etc.) light up in this buffer.
using var rightWorkspace = new ReferenceCountedDisposable<PreviewWorkspace>(new PreviewWorkspace(document.Project.Solution));
rightWorkspace.Target.OpenDocument(document.Id, newEditorDocument.TextBuffer.AsTextContainer());
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
return await CreateAddedDocumentPreviewViewCoreAsync(newEditorDocument, rightWorkspace, document, zoomLevel, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>> CreateAddedDocumentPreviewViewAsync(Document document, double zoomLevel, CancellationToken cancellationToken)
{
return CreateAddedTextDocumentPreviewViewAsync(
document, zoomLevel,
createEditorDocumentAsync: (textDocument, cancellationToken) => CreateNewBufferAsync(textDocument, cancellationToken),
cancellationToken);
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>> CreateAddedAdditionalDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken)
{
return CreateAddedTextDocumentPreviewViewAsync(
document, zoomLevel,
createEditorDocumentAsync: CreateNewPlainTextBufferAsync,
cancellationToken);
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>> CreateAddedAnalyzerConfigDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken)
{
return CreateAddedTextDocumentPreviewViewAsync(
document, zoomLevel,
createEditorDocumentAsync: CreateNewPlainTextBufferAsync,
cancellationToken);
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>> CreateRemovedDocumentPreviewViewAsync(Document document, CancellationToken cancellationToken)
=> CreateRemovedDocumentPreviewViewAsync(document, DefaultZoomLevel, cancellationToken);
private async ValueTask<IDifferenceViewerPreview<TDifferenceViewer>> CreateRemovedDocumentPreviewViewCoreAsync(ITextDocument oldEditorDocument, ReferenceCountedDisposable<PreviewWorkspace> workspace, TextDocument document, double zoomLevel, CancellationToken cancellationToken)
{
// IProjectionBufferFactoryService is a Visual Studio API which is not documented as free-threaded
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var firstLine = string.Format(EditorFeaturesResources.Removing_0_from_1_with_content_colon,
document.Name, document.Project.Name);
var span = new SnapshotSpan(oldEditorDocument.TextBuffer.CurrentSnapshot, Span.FromBounds(0, oldEditorDocument.TextBuffer.CurrentSnapshot.Length))
.CreateTrackingSpan(SpanTrackingMode.EdgeExclusive);
var originalBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer(
sourceSpans: [firstLine, "\r\n", span], registryService: _contentTypeRegistryService);
var changedBuffer = _projectionBufferFactoryService.CreatePreviewProjectionBuffer(
sourceSpans: [firstLine, "\r\n"], registryService: _contentTypeRegistryService);
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
return await CreateNewDifferenceViewerAsync(workspace, null, originalBuffer, changedBuffer, [oldEditorDocument], zoomLevel, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
private async Task<IDifferenceViewerPreview<TDifferenceViewer>> CreateRemovedTextDocumentPreviewViewAsync<TDocument>(
TDocument document,
double zoomLevel,
Func<TDocument, CancellationToken, ValueTask<ITextDocument>> createEditorDocumentAsync,
CancellationToken cancellationToken)
where TDocument : TextDocument
{
// createBufferAsync must be called from the main thread
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
// Note: We don't use the original buffer that is associated with oldDocument
// (and possibly open in the editor) for oldBuffer below. This is because oldBuffer
// will be used inside a projection buffer inside our inline diff preview below
// and platform's implementation currently has a bug where projection buffers
// are being leaked. This leak means that if we use the original buffer that is
// currently visible in the editor here, the projection buffer span calculation
// would be triggered every time user changes some code in this buffer (even though
// the diff view would long have been dismissed by the time user edits the code)
// resulting in crashes. Instead we create a new buffer from the same content.
// TODO: We could use ITextBufferCloneService instead here to clone the original buffer.
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
var oldEditorDocument = await createEditorDocumentAsync(document, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
// Create PreviewWorkspace around the buffer to be displayed in the diff preview
// so that all IDE services (colorizer, squiggles etc.) light up in this buffer.
using var leftWorkspace = new ReferenceCountedDisposable<PreviewWorkspace>(new PreviewWorkspace(document.Project.Solution));
leftWorkspace.Target.OpenDocument(document.Id, oldEditorDocument.TextBuffer.AsTextContainer());
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
return await CreateRemovedDocumentPreviewViewCoreAsync(oldEditorDocument, leftWorkspace, document, zoomLevel, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>> CreateRemovedDocumentPreviewViewAsync(Document document, double zoomLevel, CancellationToken cancellationToken)
{
return CreateRemovedTextDocumentPreviewViewAsync(
document, zoomLevel,
createEditorDocumentAsync: (textDocument, cancellationToken) => CreateNewBufferAsync(textDocument, cancellationToken),
cancellationToken);
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>> CreateRemovedAdditionalDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken)
{
return CreateRemovedTextDocumentPreviewViewAsync(
document, zoomLevel,
createEditorDocumentAsync: CreateNewPlainTextBufferAsync,
cancellationToken);
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>> CreateRemovedAnalyzerConfigDocumentPreviewViewAsync(TextDocument document, double zoomLevel, CancellationToken cancellationToken)
{
return CreateRemovedTextDocumentPreviewViewAsync(
document, zoomLevel,
createEditorDocumentAsync: CreateNewPlainTextBufferAsync,
cancellationToken);
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>?> CreateChangedDocumentPreviewViewAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken)
=> CreateChangedDocumentPreviewViewAsync(oldDocument, newDocument, DefaultZoomLevel, cancellationToken);
public async Task<IDifferenceViewerPreview<TDifferenceViewer>?> CreateChangedDocumentPreviewViewAsync(Document oldDocument, Document newDocument, double zoomLevel, CancellationToken cancellationToken)
{
// CreateNewBufferAsync must be called from the main thread
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
// Note: We don't use the original buffer that is associated with oldDocument
// (and currently open in the editor) for oldBuffer below. This is because oldBuffer
// will be used inside a projection buffer inside our inline diff preview below
// and platform's implementation currently has a bug where projection buffers
// are being leaked. This leak means that if we use the original buffer that is
// currently visible in the editor here, the projection buffer span calculation
// would be triggered every time user changes some code in this buffer (even though
// the diff view would long have been dismissed by the time user edits the code)
// resulting in crashes. Instead we create a new buffer from the same content.
// TODO: We could use ITextBufferCloneService instead here to clone the original buffer.
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
var oldEditorDocument = await CreateNewBufferAsync(oldDocument, cancellationToken);
var oldBuffer = oldEditorDocument.TextBuffer;
var newEditorDocument = await CreateNewBufferAsync(newDocument, cancellationToken);
var newBuffer = newEditorDocument.TextBuffer;
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
// Convert the diffs to be line based.
// Compute the diffs between the old text and the new.
var diffResult = ComputeEditDifferences(oldDocument, newDocument, cancellationToken);
// Need to show the spans in the right that are different.
// We also need to show the spans that are in conflict.
var originalSpans = GetOriginalSpans(diffResult, cancellationToken);
var changedSpans = GetChangedSpans(diffResult, cancellationToken);
string? description = null;
NormalizedSpanCollection allSpans;
if (newDocument.SupportsSyntaxTree)
{
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
var newRoot = await newDocument.GetRequiredSyntaxRootAsync(cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
var conflictNodes = newRoot.GetAnnotatedNodesAndTokens(ConflictAnnotation.Kind);
var conflictSpans = conflictNodes.Select(n => n.Span.ToSpan()).ToList();
var conflictDescriptions = conflictNodes.SelectMany(n => n.GetAnnotations(ConflictAnnotation.Kind))
.Select(a => $"❌ {ConflictAnnotation.GetDescription(a)}")
.Distinct();
var warningNodes = newRoot.GetAnnotatedNodesAndTokens(WarningAnnotation.Kind);
var warningSpans = warningNodes.Select(n => n.Span.ToSpan()).ToList();
var warningDescriptions = warningNodes.SelectMany(n => n.GetAnnotations(WarningAnnotation.Kind))
.Select(a => $"⚠ {WarningAnnotation.GetDescription(a)}")
.Distinct();
var suppressDiagnosticsNodes = newRoot.GetAnnotatedNodesAndTokens(SuppressDiagnosticsAnnotation.Kind);
var suppressDiagnosticsSpans = suppressDiagnosticsNodes.Select(n => n.Span.ToSpan()).ToList();
AttachAnnotationsToBuffer(newBuffer, conflictSpans, warningSpans, suppressDiagnosticsSpans);
description = conflictSpans.Count == 0 && warningSpans.Count == 0
? null
: string.Join(Environment.NewLine, conflictDescriptions.Concat(warningDescriptions));
allSpans = new NormalizedSpanCollection(conflictSpans.Concat(warningSpans).Concat(changedSpans));
}
else
{
allSpans = new NormalizedSpanCollection(changedSpans);
}
var originalLineSpans = CreateLineSpans(oldBuffer.CurrentSnapshot, originalSpans, cancellationToken);
var changedLineSpans = CreateLineSpans(newBuffer.CurrentSnapshot, allSpans, cancellationToken);
if (!originalLineSpans.Any())
{
// This means that we have no differences (likely because of conflicts).
// In such cases, use the same spans for the left (old) buffer as the right (new) buffer.
originalLineSpans = changedLineSpans;
}
// Create PreviewWorkspaces around the buffers to be displayed on the left and right
// so that all IDE services (colorizer, squiggles etc.) light up in these buffers.
//
// Performance: Replace related documents to oldBuffer and newBuffer in these workspaces with the
// relating SourceText. This prevents cascading forks as taggers call to
// GetOpenTextDocumentInCurrentContextWithChanges would eventually wind up
// calling Solution.WithDocumentText using the related ids.
var leftSolution = oldDocument.Project.Solution;
var allLeftIds = leftSolution.GetRelatedDocumentIds(oldDocument.Id);
leftSolution = leftSolution.WithDocumentText(allLeftIds, oldBuffer.AsTextContainer().CurrentText, PreservationMode.PreserveIdentity);
using var leftWorkspace = new ReferenceCountedDisposable<PreviewWorkspace>(new PreviewWorkspace(leftSolution));
leftWorkspace.Target.OpenDocument(oldDocument.Id, oldBuffer.AsTextContainer());
var rightSolution = newDocument.Project.Solution;
var allRightIds = rightSolution.GetRelatedDocumentIds(newDocument.Id);
rightSolution = rightSolution.WithDocumentText(allRightIds, newBuffer.AsTextContainer().CurrentText, PreservationMode.PreserveIdentity);
using var rightWorkspace = new ReferenceCountedDisposable<PreviewWorkspace>(new PreviewWorkspace(rightSolution));
rightWorkspace.Target.OpenDocument(newDocument.Id, newBuffer.AsTextContainer());
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
return await CreateChangedDocumentViewAsync(
oldEditorDocument, newEditorDocument, description, originalLineSpans, changedLineSpans,
leftWorkspace, rightWorkspace, zoomLevel, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
// NOTE: We are only sharing this code between additional documents and analyzer config documents,
// which are essentially plain text documents. Regular source documents need special handling
// and hence have a different implementation.
private async Task<IDifferenceViewerPreview<TDifferenceViewer>?> CreateChangedAdditionalOrAnalyzerConfigDocumentPreviewViewAsync(
TextDocument oldDocument,
TextDocument newDocument,
double zoomLevel,
CancellationToken cancellationToken)
{
Debug.Assert(oldDocument.Kind is TextDocumentKind.AdditionalDocument or TextDocumentKind.AnalyzerConfigDocument);
// openTextDocument must be called from the main thread
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
// Note: We don't use the original buffer that is associated with oldDocument
// (and currently open in the editor) for oldBuffer below. This is because oldBuffer
// will be used inside a projection buffer inside our inline diff preview below
// and platform's implementation currently has a bug where projection buffers
// are being leaked. This leak means that if we use the original buffer that is
// currently visible in the editor here, the projection buffer span calculation
// would be triggered every time user changes some code in this buffer (even though
// the diff view would long have been dismissed by the time user edits the code)
// resulting in crashes. Instead we create a new buffer from the same content.
// TODO: We could use ITextBufferCloneService instead here to clone the original buffer.
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
var oldEditorDocument = await CreateNewPlainTextBufferAsync(oldDocument, cancellationToken);
var oldBuffer = oldEditorDocument.TextBuffer;
var newEditorDocument = await CreateNewPlainTextBufferAsync(newDocument, cancellationToken);
var newBuffer = newEditorDocument.TextBuffer;
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
// Convert the diffs to be line based.
// Compute the diffs between the old text and the new.
var diffResult = ComputeEditDifferences(oldDocument, newDocument, cancellationToken);
// Need to show the spans in the right that are different.
var originalSpans = GetOriginalSpans(diffResult, cancellationToken);
var changedSpans = GetChangedSpans(diffResult, cancellationToken);
var originalLineSpans = CreateLineSpans(oldBuffer.CurrentSnapshot, originalSpans, cancellationToken);
var changedLineSpans = CreateLineSpans(newBuffer.CurrentSnapshot, changedSpans, cancellationToken);
// TODO: Why aren't we attaching conflict / warning annotations here like we do for regular documents above?
// Create PreviewWorkspaces around the buffers to be displayed on the left and right
// so that all IDE services (colorizer, squiggles etc.) light up in these buffers.
using var leftWorkspace = new ReferenceCountedDisposable<PreviewWorkspace>(new PreviewWorkspace(oldDocument.Project.Solution));
leftWorkspace.Target.OpenDocument(oldDocument.Id, oldBuffer.AsTextContainer());
using var rightWorkspace = new ReferenceCountedDisposable<PreviewWorkspace>(new PreviewWorkspace(newDocument.Project.Solution));
rightWorkspace.Target.OpenDocument(newDocument.Id, newBuffer.AsTextContainer());
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
return await CreateChangedDocumentViewAsync(
oldEditorDocument, newEditorDocument, description: null, originalLineSpans, changedLineSpans,
leftWorkspace, rightWorkspace, zoomLevel, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>?> CreateChangedAdditionalDocumentPreviewViewAsync(TextDocument oldDocument, TextDocument newDocument, double zoomLevel, CancellationToken cancellationToken)
{
return CreateChangedAdditionalOrAnalyzerConfigDocumentPreviewViewAsync(
oldDocument, newDocument, zoomLevel, cancellationToken);
}
public Task<IDifferenceViewerPreview<TDifferenceViewer>?> CreateChangedAnalyzerConfigDocumentPreviewViewAsync(TextDocument oldDocument, TextDocument newDocument, double zoomLevel, CancellationToken cancellationToken)
{
return CreateChangedAdditionalOrAnalyzerConfigDocumentPreviewViewAsync(
oldDocument, newDocument, zoomLevel, cancellationToken);
}
private async ValueTask<IDifferenceViewerPreview<TDifferenceViewer>?> CreateChangedDocumentViewAsync(ITextDocument oldEditorDocument, ITextDocument newEditorDocument, string? description,
List<LineSpan> originalSpans, List<LineSpan> changedSpans, ReferenceCountedDisposable<PreviewWorkspace> leftWorkspace, ReferenceCountedDisposable<PreviewWorkspace> rightWorkspace,
double zoomLevel, CancellationToken cancellationToken)
{
if (!(originalSpans.Any() && changedSpans.Any()))
{
// Both line spans must be non-empty. Otherwise, below projection buffer factory API call will throw.
// So if either is empty (signaling that there are no changes to preview in the document), then we bail out.
// This can happen in cases where the user has already applied the fix and light bulb has already been dismissed,
// but platform hasn't cancelled the preview operation yet. Since the light bulb has already been dismissed at
// this point, the preview that we return will never be displayed to the user. So returning null here is harmless.
// TODO: understand how this can even happen. The diff input is stable -- we shouldn't be depending on some sort of
// state that could change underneath us. If we know the file changed, how would we discover here it didn't?
return null;
}
// IProjectionBufferFactoryService is a Visual Studio API which is not documented as free-threaded
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var originalBuffer = _projectionBufferFactoryService.CreateProjectionBufferWithoutIndentation(
_contentTypeRegistryService,
_editorOptionsService.Factory.GlobalOptions,
oldEditorDocument.TextBuffer.CurrentSnapshot,
"...",
description,
[.. originalSpans]);
var changedBuffer = _projectionBufferFactoryService.CreateProjectionBufferWithoutIndentation(
_contentTypeRegistryService,
_editorOptionsService.Factory.GlobalOptions,
newEditorDocument.TextBuffer.CurrentSnapshot,
"...",
description,
[.. changedSpans]);
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
return await CreateNewDifferenceViewerAsync(leftWorkspace, rightWorkspace, originalBuffer, changedBuffer, [oldEditorDocument, newEditorDocument], zoomLevel, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
private static void AttachAnnotationsToBuffer(
ITextBuffer newBuffer, IEnumerable<Span> conflictSpans, IEnumerable<Span> warningSpans, IEnumerable<Span> suppressDiagnosticsSpans)
{
// Attach the spans to the buffer.
newBuffer.Properties.AddProperty(PredefinedPreviewTaggerKeys.ConflictSpansKey, new NormalizedSnapshotSpanCollection(newBuffer.CurrentSnapshot, conflictSpans));
newBuffer.Properties.AddProperty(PredefinedPreviewTaggerKeys.WarningSpansKey, new NormalizedSnapshotSpanCollection(newBuffer.CurrentSnapshot, warningSpans));
newBuffer.Properties.AddProperty(PredefinedPreviewTaggerKeys.SuppressDiagnosticsSpansKey, new NormalizedSnapshotSpanCollection(newBuffer.CurrentSnapshot, suppressDiagnosticsSpans));
}
private async ValueTask<ITextDocument> CreateNewBufferAsync(Document document, CancellationToken cancellationToken)
{
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var contentTypeService = document.GetRequiredLanguageService<IContentTypeLanguageService>();
var contentType = contentTypeService.GetDefaultContentType();
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
return await CreateTextBufferCoreAsync(document, contentType, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
private async ValueTask<ITextDocument> CreateNewPlainTextBufferAsync(TextDocument document, CancellationToken cancellationToken)
{
// ITextBufferFactoryService is a Visual Studio API which is not documented as free-threaded
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var contentType = _textBufferFactoryService.TextContentType;
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
return await CreateTextBufferCoreAsync(document, contentType, cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
private async ValueTask<ITextDocument> CreateTextBufferCoreAsync(TextDocument document, IContentType contentType, CancellationToken cancellationToken)
{
ThreadingContext.ThrowIfNotOnUIThread();
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF)
var text = await document.State.GetTextAsync(cancellationToken);
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
var buffer = _textBufferCloneService.Clone(text, contentType);
// Associate buffer with a text document with random file path to satisfy extensibility points expecting
// absolute file path. Ensure the new path preserves the same extension as before as that extension is used by
// LSP to determine the language of the document.
var textDocument = _textDocumentFactoryService.CreateTextDocument(
buffer, Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), document.Name));
return textDocument;
}
protected abstract IDifferenceViewerPreview<TDifferenceViewer> CreateDifferenceViewerPreview(TDifferenceViewer viewer);
protected abstract Task<TDifferenceViewer> CreateDifferenceViewAsync(IDifferenceBuffer diffBuffer, ITextViewRoleSet previewRoleSet, DifferenceViewMode mode, double zoomLevel, CancellationToken cancellationToken);
private async ValueTask<IDifferenceViewerPreview<TDifferenceViewer>> CreateNewDifferenceViewerAsync(
ReferenceCountedDisposable<PreviewWorkspace>? leftWorkspace, ReferenceCountedDisposable<PreviewWorkspace>? rightWorkspace,
IProjectionBuffer originalBuffer, IProjectionBuffer changedBuffer,
ImmutableArray<ITextDocument> editorDocumentsToClose,
double zoomLevel, CancellationToken cancellationToken)
{
// IWpfDifferenceViewerFactoryService is a Visual Studio API which is not documented as free-threaded
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
// leftWorkspace can be null if the change is adding a document.
// rightWorkspace can be null if the change is removing a document.
// However both leftWorkspace and rightWorkspace can't be null at the same time.
Contract.ThrowIfTrue((leftWorkspace == null) && (rightWorkspace == null));
var diffBuffer = _differenceBufferService.CreateDifferenceBuffer(
originalBuffer, changedBuffer,
new StringDifferenceOptions(), disableEditing: true);
var mode = leftWorkspace == null ? DifferenceViewMode.RightViewOnly :
rightWorkspace == null ? DifferenceViewMode.LeftViewOnly :
DifferenceViewMode.Inline;
var diffViewer = await CreateDifferenceViewAsync(diffBuffer, _previewRoleSet, mode, zoomLevel, cancellationToken).ConfigureAwait(true);
// Claim ownership of the workspace references
leftWorkspace = leftWorkspace?.TryAddReference();
rightWorkspace = rightWorkspace?.TryAddReference();
diffViewer.Closed += (s, e) =>
{
// Workaround Editor bug. The editor has an issue where they sometimes crash when
// trying to apply changes to projection buffer. So, when the user actually invokes
// a SuggestedAction we may then edit a text buffer, which the editor will then
// try to propagate through the projections we have here over that buffer. To ensure
// that that doesn't happen, we clear out the projections first so that this crash
// won't happen.
originalBuffer.DeleteSpans(0, originalBuffer.CurrentSnapshot.SpanCount);
changedBuffer.DeleteSpans(0, changedBuffer.CurrentSnapshot.SpanCount);
leftWorkspace?.Dispose();
leftWorkspace = null;
rightWorkspace?.Dispose();
rightWorkspace = null;
// Also ensure any editor ITextDocument(s) we created get appropriately disposed of.
foreach (var editorDocument in editorDocumentsToClose)
{
editorDocument.Dispose();
}
};
return CreateDifferenceViewerPreview(diffViewer);
}
private static List<LineSpan> CreateLineSpans(ITextSnapshot textSnapshot, NormalizedSpanCollection allSpans, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var result = new List<LineSpan>();
foreach (var span in allSpans)
{
cancellationToken.ThrowIfCancellationRequested();
var lineSpan = GetLineSpan(textSnapshot, span);
MergeLineSpans(result, lineSpan);
}
return result;
}
// Find the lines that surround the span of the difference. Try to expand the span to
// include both the previous and next lines so that we can show more context to the
// user.
private static LineSpan GetLineSpan(
ITextSnapshot snapshot,
Span span)
{
var startLine = snapshot.GetLineNumberFromPosition(span.Start);
var endLine = snapshot.GetLineNumberFromPosition(span.End);
if (startLine > 0)
{
startLine--;
}
if (endLine < snapshot.LineCount)
{
endLine++;
}
return LineSpan.FromBounds(startLine, endLine);
}
// Adds a line span to the spans we've been collecting. If the line span overlaps or
// abuts a previous span then the two are merged.
private static void MergeLineSpans(List<LineSpan> lineSpans, LineSpan nextLineSpan)
{
if (lineSpans.Count > 0)
{
var lastLineSpan = lineSpans.Last();
// We merge them if there's no more than one line between the two. Otherwise
// we'd show "..." between two spans where we could just show the actual code.
if (nextLineSpan.Start >= lastLineSpan.Start && nextLineSpan.Start <= (lastLineSpan.End + 1))
{
nextLineSpan = LineSpan.FromBounds(lastLineSpan.Start, nextLineSpan.End);
lineSpans.RemoveAt(lineSpans.Count - 1);
}
}
lineSpans.Add(nextLineSpan);
}
private IHierarchicalDifferenceCollection ComputeEditDifferences(TextDocument oldDocument, TextDocument newDocument, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// Get the text that's actually in the editor.
var oldText = oldDocument.GetTextSynchronously(cancellationToken);
var newText = newDocument.GetTextSynchronously(cancellationToken);
// Defer to the editor to figure out what changes the client made.
var diffService = _differenceSelectorService.GetTextDifferencingService(
oldDocument.Project.Services.GetRequiredService<IContentTypeLanguageService>().GetDefaultContentType());
diffService ??= _differenceSelectorService.DefaultTextDifferencingService;
return diffService.DiffSourceTexts(oldText, newText, s_differenceOptions);
}
private static NormalizedSpanCollection GetOriginalSpans(IHierarchicalDifferenceCollection diffResult, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var lineSpans = new List<Span>();
foreach (var difference in diffResult)
{
cancellationToken.ThrowIfCancellationRequested();
var mappedSpan = diffResult.LeftDecomposition.GetSpanInOriginal(difference.Left);
lineSpans.Add(mappedSpan);
}
return new NormalizedSpanCollection(lineSpans);
}
private static NormalizedSpanCollection GetChangedSpans(IHierarchicalDifferenceCollection diffResult, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var lineSpans = new List<Span>();
foreach (var difference in diffResult)
{
cancellationToken.ThrowIfCancellationRequested();
var mappedSpan = diffResult.RightDecomposition.GetSpanInOriginal(difference.Right);
lineSpans.Add(mappedSpan);
}
return new NormalizedSpanCollection(lineSpans);
}
}
|