public async Task <DifferenceViewerPreview?> 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 oldBuffer = await CreateNewBufferAsync(oldDocument, cancellationToken); var newBuffer = await CreateNewBufferAsync(newDocument, cancellationToken); #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. var leftWorkspace = new PreviewWorkspace(oldDocument.Project.Solution); leftWorkspace.OpenDocument(oldDocument.Id, oldBuffer.AsTextContainer()); var rightWorkspace = new PreviewWorkspace(newDocument.Project.Solution); rightWorkspace.OpenDocument(newDocument.Id, newBuffer.AsTextContainer()); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) return(await CreateChangedDocumentViewAsync( oldBuffer, newBuffer, description, originalLineSpans, changedLineSpans, leftWorkspace, rightWorkspace, zoomLevel, cancellationToken)); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task }
public Task <object> CreateChangedDocumentPreviewViewAsync(Document oldDocument, Document newDocument, double zoomLevel, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); // 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. var oldBuffer = CreateNewBuffer(oldDocument, cancellationToken); var newBuffer = CreateNewBuffer(newDocument, cancellationToken); // 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); var description = default(string); var allSpans = default(NormalizedSpanCollection); if (newDocument.SupportsSyntaxTree) { var newRoot = newDocument.GetSyntaxRootAsync(cancellationToken).WaitAndGetResult(cancellationToken); 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(); AttachConflictAndWarningAnnotationToBuffer(newBuffer, conflictSpans, warningSpans); 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. var leftDocument = oldDocument.Project .RemoveDocument(oldDocument.Id) .AddDocument(oldDocument.Name, oldBuffer.AsTextContainer().CurrentText, oldDocument.Folders, oldDocument.FilePath); var leftWorkspace = new PreviewWorkspace(leftDocument.Project.Solution); leftWorkspace.OpenDocument(leftDocument.Id); var rightWorkspace = new PreviewWorkspace( oldDocument.WithText(newBuffer.AsTextContainer().CurrentText).Project.Solution); rightWorkspace.OpenDocument(newDocument.Id); return(CreateChangedDocumentViewAsync( oldBuffer, newBuffer, description, originalLineSpans, changedLineSpans, leftWorkspace, rightWorkspace, zoomLevel, cancellationToken)); }