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 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); var description = conflictSpans.Count == 0 && warningSpans.Count == 0 ? null : string.Join(Environment.NewLine, conflictDescriptions.Concat(warningDescriptions)); var allSpans = new NormalizedSpanCollection(conflictSpans.Concat(warningSpans).Concat(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); 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)); }
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 async Task TestWarnsWithMatchingExtensionMethodUsedAsDelegate(bool useSymbolAnnotations) { var source = @"using System; using B; namespace A { static class AExtensions { public static void M(this int a){} } public class C1 {} } namespace B { static class BExtensions { public static void M(this object a){} } } class C { Action M(A.C1 c1) => 42.M; }"; await TestAsync( source, @"using System; using A; using B; namespace A { static class AExtensions { public static void M(this int a){} } public class C1 {} } namespace B { static class BExtensions { public static void M(this object a){} } } class C { Action M(A.C1 c1) => 42.M; }", @"using System; using A; using B; namespace A { static class AExtensions { public static void M(this int a){} } public class C1 {} } namespace B { static class BExtensions { public static void M(this object a){} } } class C { Action M(C1 c1) => 42.M; }", safe : true, useSymbolAnnotations); var doc = await GetDocument(source, useSymbolAnnotations); OptionSet options = await doc.GetOptionsAsync(); var imported = await ImportAdder.AddImportsFromSyntaxesAsync(doc, true, options); var root = await imported.GetSyntaxRootAsync(); var nodeWithWarning = root.GetAnnotatedNodes(WarningAnnotation.Kind).Single(); Assert.Equal("42.M", nodeWithWarning.ToFullString()); var warning = nodeWithWarning.GetAnnotations(WarningAnnotation.Kind).Single(); var expectedWarningMessage = string.Format(WorkspacesResources.Warning_adding_imports_will_bring_an_extension_method_into_scope_with_the_same_name_as_member_access, "M"); Assert.Equal(expectedWarningMessage, WarningAnnotation.GetDescription(warning)); }
protected async Task TestChangeNamespaceAsync( string initialMarkUp, string expectedSourceOriginal, string expectedSourceReference = null) { var testOptions = new TestParameters(); using (var workspace = CreateWorkspaceFromOptions(initialMarkUp, testOptions)) { if (workspace.Projects.Count == 2) { var project = workspace.Documents.Single(doc => !doc.SelectedSpans.IsEmpty()).Project; var dependentProject = workspace.Projects.Single(proj => proj.Id != project.Id); var references = dependentProject.ProjectReferences.ToList(); references.Add(new ProjectReference(project.Id)); dependentProject.ProjectReferences = references; workspace.OnProjectReferenceAdded(dependentProject.Id, new ProjectReference(project.Id)); } if (expectedSourceOriginal != null) { var originalDocument = workspace.Documents.Single(doc => !doc.SelectedSpans.IsEmpty()); var originalDocumentId = originalDocument.Id; var refDocument = workspace.Documents.Where(doc => doc.Id != originalDocumentId).SingleOrDefault(); var refDocumentId = refDocument?.Id; var oldAndNewSolution = await TestOperationAsync(testOptions, workspace); var oldSolution = oldAndNewSolution.Item1; var newSolution = oldAndNewSolution.Item2; var changedDocumentIds = SolutionUtilities.GetChangedDocuments(oldSolution, newSolution); Assert.True(changedDocumentIds.Contains(originalDocumentId), "original document was not changed."); var modifiedOriginalDocument = newSolution.GetDocument(originalDocumentId); var modifiedOringinalRoot = await modifiedOriginalDocument.GetSyntaxRootAsync(); // One node/token will contain the warning we attached for change namespace action. Assert.Single(modifiedOringinalRoot.DescendantNodesAndTokensAndSelf().Where(n => { IEnumerable <SyntaxAnnotation> annotations; if (n.IsNode) { annotations = n.AsNode().GetAnnotations(WarningAnnotation.Kind); } else { annotations = n.AsToken().GetAnnotations(WarningAnnotation.Kind); } return(annotations.Any(annotation => WarningAnnotation.GetDescription(annotation) == FeaturesResources.Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning)); })); var actualText = (await modifiedOriginalDocument.GetTextAsync()).ToString(); Assert.Equal(expectedSourceOriginal, actualText); if (expectedSourceReference == null) { // there shouldn't be any textual change if (changedDocumentIds.Contains(refDocumentId)) { var oldRefText = (await oldSolution.GetDocument(refDocumentId).GetTextAsync()).ToString(); var newRefText = (await newSolution.GetDocument(refDocumentId).GetTextAsync()).ToString(); Assert.Equal(oldRefText, newRefText); } } else { Assert.True(changedDocumentIds.Contains(refDocumentId)); var actualRefText = (await newSolution.GetDocument(refDocumentId).GetTextAsync()).ToString(); Assert.Equal(expectedSourceReference, actualRefText); } } else { var(actions, _) = await GetCodeActionsAsync(workspace, testOptions); if (actions.Length > 0) { var hasChangeNamespaceAction = actions.Any(action => action is CodeAction.SolutionChangeAction); Assert.False(hasChangeNamespaceAction, "Change namespace to match folder action was not expected, but shows up."); } } } async Task <Tuple <Solution, Solution> > TestOperationAsync(TestParameters parameters, TestWorkspace workspace) { var(actions, _) = await GetCodeActionsAsync(workspace, parameters); var changeNamespaceAction = actions.Single(a => a is CodeAction.SolutionChangeAction); var operations = await changeNamespaceAction.GetOperationsAsync(CancellationToken.None); return(ApplyOperationsAndGetSolution(workspace, operations)); } }
public async Task TestWarnsWithMatchingExtensionMethodUsedAsDelegate(bool useSymbolAnnotations) { var source = @"using System; using B; namespace A { static class AExtensions { public static void M(this int a){} } public class C1 {} } namespace B { static class BExtensions { public static void M(this object a){} } } class C { Action M(A.C1 c1) => 42.M; }"; await TestAsync( source, @"using System; using A; using B; namespace A { static class AExtensions { public static void M(this int a){} } public class C1 {} } namespace B { static class BExtensions { public static void M(this object a){} } } class C { Action M(A.C1 c1) => 42.M; }", @"using System; using A; using B; namespace A { static class AExtensions { public static void M(this int a){} } public class C1 {} } namespace B { static class BExtensions { public static void M(this object a){} } } class C { Action M(C1 c1) => 42.M; }", safe : true, useSymbolAnnotations); var doc = await GetDocument(source, useSymbolAnnotations); OptionSet options = await doc.GetOptionsAsync(); var imported = await ImportAdder.AddImportsFromSyntaxesAsync(doc, true, options); var root = await imported.GetSyntaxRootAsync(); var nodeWithWarning = root.GetAnnotatedNodes(WarningAnnotation.Kind).Single(); Assert.Equal("42.M", nodeWithWarning.ToFullString()); var warning = nodeWithWarning.GetAnnotations(WarningAnnotation.Kind).Single(); Assert.Equal("Adding imports will bring an extension method into scope with the same name as 'M'", WarningAnnotation.GetDescription(warning)); }