diff --git a/src/Test/Utilities/Shared/MarkedSource/MarkupTestFile.cs b/src/Test/Utilities/Shared/MarkedSource/MarkupTestFile.cs index 1fed764ee1de925b5b04967638a95d43826eebc0..0e0517bee16da57556b81d49fb3d121e5c93b358 100644 --- a/src/Test/Utilities/Shared/MarkedSource/MarkupTestFile.cs +++ b/src/Test/Utilities/Shared/MarkedSource/MarkupTestFile.cs @@ -230,11 +230,11 @@ public static void GetPositionAndSpans(string input, out string output, out int cursorPosition = pos.Value; } + public static void GetPosition(string input, out string output, out int? cursorPosition) + => GetPositionAndSpans(input, out output, out cursorPosition, out IList spans); + public static void GetPosition(string input, out string output, out int cursorPosition) - { - IList spans; - GetPositionAndSpans(input, out output, out cursorPosition, out spans); - } + => GetPositionAndSpans(input, out output, out cursorPosition, out var spans); public static void GetPositionAndSpan(string input, out string output, out int? cursorPosition, out TextSpan? textSpan) { diff --git a/src/VisualStudio/CSharp/Impl/Snippets/SnippetExpansionClient.cs b/src/VisualStudio/CSharp/Impl/Snippets/SnippetExpansionClient.cs index 0ee6d8efb41d0a3bf9082e7f41d4f8d49e6ff8a3..107871c4a46e4648577b15f8d45563b85002d9b5 100644 --- a/src/VisualStudio/CSharp/Impl/Snippets/SnippetExpansionClient.cs +++ b/src/VisualStudio/CSharp/Impl/Snippets/SnippetExpansionClient.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.LanguageServices.CSharp.Snippets.SnippetFunctions; using Microsoft.VisualStudio.LanguageServices.Implementation.Snippets; @@ -81,7 +82,9 @@ public override int GetExpansionFunction(IXMLDOMNode xmlFunctionNode, string bst } } - internal override Document AddImports(Document document, XElement snippetNode, bool placeSystemNamespaceFirst, CancellationToken cancellationToken) + internal override Document AddImports( + Document document, int position, XElement snippetNode, + bool placeSystemNamespaceFirst, CancellationToken cancellationToken) { var importsNode = snippetNode.Element(XName.Get("Imports", snippetNode.Name.NamespaceName)); if (importsNode == null || @@ -104,8 +107,14 @@ internal override Document AddImports(Document document, XElement snippetNode, b } var root = document.GetSyntaxRootSynchronously(cancellationToken); + var node = root.FindToken(position).Parent; + var container = node.GetInnermostNamespaceDeclarationWithUsings() ?? (SyntaxNode)node.GetAncestorOrThis(); - var newRoot = ((CompilationUnitSyntax)root).AddUsingDirectives(newUsingDirectives, placeSystemNamespaceFirst); + var newContainer = container is NamespaceDeclarationSyntax n + ? (SyntaxNode)n.AddUsingDirectives(newUsingDirectives, placeSystemNamespaceFirst, Formatter.Annotation) + : ((CompilationUnitSyntax)container).AddUsingDirectives(newUsingDirectives, placeSystemNamespaceFirst); + + var newRoot = root.ReplaceNode(container, newContainer); var newDocument = document.WithSyntaxRoot(newRoot); var formattedDocument = Formatter.FormatAsync(newDocument, Formatter.Annotation, cancellationToken: cancellationToken).WaitAndGetResult(cancellationToken); diff --git a/src/VisualStudio/Core/Def/Implementation/Snippets/AbstractSnippetExpansionClient.cs b/src/VisualStudio/Core/Def/Implementation/Snippets/AbstractSnippetExpansionClient.cs index 7c6e1f73c51c53d8dac92df7dfe83e0602a45615..aa6a459594aaaf90b2036ac27f993def585f62b7 100644 --- a/src/VisualStudio/Core/Def/Implementation/Snippets/AbstractSnippetExpansionClient.cs +++ b/src/VisualStudio/Core/Def/Implementation/Snippets/AbstractSnippetExpansionClient.cs @@ -56,7 +56,7 @@ public AbstractSnippetExpansionClient(Guid languageServiceGuid, ITextView textVi public abstract int GetExpansionFunction(IXMLDOMNode xmlFunctionNode, string bstrFieldName, out IVsExpansionFunction pFunc); protected abstract ITrackingSpan InsertEmptyCommentAndGetEndPositionTrackingSpan(); - internal abstract Document AddImports(Document document, XElement snippetNode, bool placeSystemNamespaceFirst, CancellationToken cancellationToken); + internal abstract Document AddImports(Document document, int position, XElement snippetNode, bool placeSystemNamespaceFirst, CancellationToken cancellationToken); public int FormatSpan(IVsTextLines pBuffer, VsTextSpan[] tsInSurfaceBuffer) { @@ -107,6 +107,7 @@ public int FormatSpan(IVsTextLines pBuffer, VsTextSpan[] tsInSurfaceBuffer) var endPositionTrackingSpan = isFullSnippetFormat ? InsertEmptyCommentAndGetEndPositionTrackingSpan() : null; var formattingSpan = CommonFormattingHelpers.GetFormattingSpan(SubjectBuffer.CurrentSnapshot, snippetTrackingSpan.GetSpan(SubjectBuffer.CurrentSnapshot)); + SubjectBuffer.CurrentSnapshot.FormatAndApplyToBuffer(formattingSpan, CancellationToken.None); if (isFullSnippetFormat) @@ -117,7 +118,9 @@ public int FormatSpan(IVsTextLines pBuffer, VsTextSpan[] tsInSurfaceBuffer) // specified in the snippet xml. In OnBeforeInsertion we have no guarantee that the // snippet xml will be available, and changing the buffer during OnAfterInsertion can // cause the underlying tracking spans to get out of sync. - AddReferencesAndImports(ExpansionSession, cancellationToken); + var currentStartPosition = snippetTrackingSpan.GetStartPoint(SubjectBuffer.CurrentSnapshot).Position; + AddReferencesAndImports( + ExpansionSession, currentStartPosition, cancellationToken); SetNewEndPosition(endPositionTrackingSpan); } @@ -512,7 +515,10 @@ private void GetCaretPositionInSurfaceBuffer(out int caretLine, out int caretCol } } - private void AddReferencesAndImports(IVsExpansionSession pSession, CancellationToken cancellationToken) + private void AddReferencesAndImports( + IVsExpansionSession pSession, + int position, + CancellationToken cancellationToken) { XElement snippetNode; if (!TryGetSnippetNode(pSession, out snippetNode)) @@ -528,7 +534,8 @@ private void AddReferencesAndImports(IVsExpansionSession pSession, CancellationT var documentOptions = documentWithImports.GetOptionsAsync(cancellationToken).WaitAndGetResult(cancellationToken); var placeSystemNamespaceFirst = documentOptions.GetOption(GenerationOptions.PlaceSystemNamespaceFirst); - documentWithImports = AddImports(documentWithImports, snippetNode, placeSystemNamespaceFirst, cancellationToken); + + documentWithImports = AddImports(documentWithImports, position, snippetNode, placeSystemNamespaceFirst, cancellationToken); AddReferences(documentWithImports.Project, snippetNode); } @@ -657,4 +664,4 @@ internal bool TryGetSpanOnHigherBuffer(SnapshotSpan snapshotSpan, ITextBuffer ta return false; } } -} +} \ No newline at end of file diff --git a/src/VisualStudio/Core/Test/Snippets/CSharpSnippetExpansionClientTests.vb b/src/VisualStudio/Core/Test/Snippets/CSharpSnippetExpansionClientTests.vb index 5c9c0794a2baa1e765bccc35fa255ecd19362a57..fee3fd948de5d7188ec5ea3dbcfb8043b1a6755d 100644 --- a/src/VisualStudio/Core/Test/Snippets/CSharpSnippetExpansionClientTests.vb +++ b/src/VisualStudio/Core/Test/Snippets/CSharpSnippetExpansionClientTests.vb @@ -6,8 +6,6 @@ Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Editor.UnitTests.Extensions Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces Imports Microsoft.CodeAnalysis.Formatting -Imports Microsoft.CodeAnalysis.Options -Imports Microsoft.VisualStudio.LanguageServices Imports Microsoft.VisualStudio.LanguageServices.CSharp.Snippets Imports Microsoft.VisualStudio.Text.Projection Imports Roslyn.Test.Utilities @@ -17,125 +15,159 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Snippets Public Async Function TestAddImport_EmptyDocument() As Task - Dim originalCode = .Value + Dim originalCode = "" Dim namespacesToAdd = {"System"} - Dim expectedUpdatedCode = .Value + Dim expectedUpdatedCode = "using System; +" + Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function Public Async Function TestAddImport_EmptyDocument_SystemAtTop() As Task - Dim originalCode = .Value + Dim originalCode = "" Dim namespacesToAdd = {"First.Alphabetically", "System.Bar"} - Dim expectedUpdatedCode = .Value +" Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function Public Async Function TestAddImport_EmptyDocument_SystemNotSortedToTop() As Task - Dim originalCode = .Value + Dim originalCode = "" Dim namespacesToAdd = {"First.Alphabetically", "System.Bar"} - Dim expectedUpdatedCode = .Value +" + Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=False, expectedUpdatedCode:=expectedUpdatedCode) End Function Public Async Function TestAddImport_AddsOnlyNewNamespaces() As Task - Dim originalCode = .Value +" Dim namespacesToAdd = {"D.E.F", "G.H.I"} - Dim expectedUpdatedCode = .Value +" + Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) + End Function + + + + Public Async Function TestAddImport_InsideNamespace() As Task + Dim originalCode = " +using A; + +namespace N +{ + using B; + + class C + { + $$ + } +}" + Dim namespacesToAdd = {"D"} + Dim expectedUpdatedCode = " +using A; + +namespace N +{ + using B; + using D; + + class C + { + + } +}" Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function Public Async Function TestAddImport_AddsOnlyNewAliasAndNamespacePairs() As Task - Dim originalCode = .Value +" Dim namespacesToAdd = {"A = E.F", "D = B.C", "G = H.I", "J = K.L"} - Dim expectedUpdatedCode = .Value +" Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function Public Async Function TestAddImport_DuplicateNamespaceDetectionDoesNotIgnoreCase() As Task - Dim originalCode = .Value + Dim originalCode = "using A.b.C; +" Dim namespacesToAdd = {"a.B.C", "A.B.c"} - Dim expectedUpdatedCode = .Value +" Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function Public Async Function TestAddImport_DuplicateAliasNamespacePairDetectionIgnoresWhitespace1() As Task - Dim originalCode = .Value + Dim originalCode = "using A = B.C; +" Dim namespacesToAdd = {"A = B.C"} - Dim expectedUpdatedCode = .Value + Dim expectedUpdatedCode = "using A = B.C; +" Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function Public Async Function TestAddImport_DuplicateAliasNamespacePairDetectionIgnoresWhitespace2() As Task - Dim originalCode = .Value + Dim originalCode = "using A = B.C; +" Dim namespacesToAdd = {"A=B.C"} - Dim expectedUpdatedCode = .Value + Dim expectedUpdatedCode = "using A = B.C; +" Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function Public Async Function TestAddImport_DuplicateAliasNamespacePairDetectionDoesNotIgnoreCase() As Task - Dim originalCode = .Value + Dim originalCode = "using A = B.C; +" Dim namespacesToAdd = {"a = b.C"} - Dim expectedUpdatedCode = .Value +" Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function Public Async Function TestAddImport_OnlyFormatNewImports() As Task - Dim originalCode = .Value +" Dim namespacesToAdd = {"D =E.F"} - Dim expectedUpdatedCode = .Value +" Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function Public Async Function TestAddImport_BadNamespaceGetsAdded() As Task - Dim originalCode = .Value + Dim originalCode = "" Dim namespacesToAdd = {"$system"} - Dim expectedUpdatedCode = .Value + Dim expectedUpdatedCode = "using $system; +" Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function @@ -311,7 +343,16 @@ using G= H.I; End Using End Function - Private Async Function TestSnippetAddImportsAsync(originalCode As String, namespacesToAdd As String(), placeSystemNamespaceFirst As Boolean, expectedUpdatedCode As String) As Tasks.Task + Private Async Function TestSnippetAddImportsAsync( + markupCode As String, + namespacesToAdd As String(), + placeSystemNamespaceFirst As Boolean, + expectedUpdatedCode As String) As Tasks.Task + + Dim originalCode As String = Nothing + Dim position As Integer? + MarkupTestFile.GetPosition(markupCode, originalCode, position) + Dim workspaceXml = CommonReferences="true"> <%= originalCode %> @@ -329,7 +370,7 @@ using G= H.I; ) Next - Using workspace = Await TestWorkspace.CreateAsync(workspaceXml) + Using workspace = Await TestWorkspace.CreateCSharpAsync(originalCode) Dim expansionClient = New SnippetExpansionClient( Guids.VisualBasicDebuggerLanguageId, workspace.Documents.Single().GetTextView(), @@ -338,13 +379,13 @@ using G= H.I; Dim updatedDocument = expansionClient.AddImports( workspace.CurrentSolution.Projects.Single().Documents.Single(), + If(position, 0), snippetNode, placeSystemNamespaceFirst, CancellationToken.None) - Assert.Equal(expectedUpdatedCode.Replace(vbLf, vbCrLf), + Assert.Equal(expectedUpdatedCode, (Await updatedDocument.GetTextAsync()).ToString()) End Using End Function - End Class -End Namespace +End Namespace \ No newline at end of file diff --git a/src/VisualStudio/Core/Test/Snippets/SnippetTestState.vb b/src/VisualStudio/Core/Test/Snippets/SnippetTestState.vb index e9a9181e163f82c2914d40a1a91729c3ecc448da..dc485d2d8759c17ec2963fd729d4e9bb0925c9ba 100644 --- a/src/VisualStudio/Core/Test/Snippets/SnippetTestState.vb +++ b/src/VisualStudio/Core/Test/Snippets/SnippetTestState.vb @@ -215,7 +215,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Snippets Throw New NotImplementedException() End Function - Friend Overrides Function AddImports(document As Document, snippetNode As XElement, placeSystemNamespaceFirst As Boolean, cancellationToken As CancellationToken) As Document + Friend Overrides Function AddImports(document As Document, position As Integer, snippetNode As XElement, placeSystemNamespaceFirst As Boolean, cancellationToken As CancellationToken) As Document Return document End Function End Class diff --git a/src/VisualStudio/Core/Test/Snippets/VisualBasicSnippetExpansionClientTests.vb b/src/VisualStudio/Core/Test/Snippets/VisualBasicSnippetExpansionClientTests.vb index 832398b28a45d9d4406958e6855a60e67d150fd1..30579c642266a8c997cffc515e07fe88ec2a947a 100644 --- a/src/VisualStudio/Core/Test/Snippets/VisualBasicSnippetExpansionClientTests.vb +++ b/src/VisualStudio/Core/Test/Snippets/VisualBasicSnippetExpansionClientTests.vb @@ -370,7 +370,16 @@ End Class End Using End Function - Private Async Function TestSnippetAddImportsAsync(originalCode As String, namespacesToAdd As String(), placeSystemNamespaceFirst As Boolean, expectedUpdatedCode As String) As Tasks.Task + Private Async Function TestSnippetAddImportsAsync( + markupCode As String, + namespacesToAdd As String(), + placeSystemNamespaceFirst As Boolean, + expectedUpdatedCode As String) As Tasks.Task + + Dim originalCode As String = Nothing + Dim position As Integer? + MarkupTestFile.GetPosition(markupCode, originalCode, position) + Dim workspaceXml = CommonReferences="true"> @@ -398,6 +407,7 @@ End Class Dim updatedDocument = expansionClient.AddImports( workspace.CurrentSolution.Projects.Single().Documents.Single(), + If(position, 0), snippetNode, placeSystemNamespaceFirst, CancellationToken.None) diff --git a/src/VisualStudio/VisualBasic/Impl/Snippets/SnippetExpansionClient.vb b/src/VisualStudio/VisualBasic/Impl/Snippets/SnippetExpansionClient.vb index caaa8e65f127b0e012f35f1f5ea5d439a3c59283..c273b64bbd3eff369e0f5a9749e723d70638fd16 100644 --- a/src/VisualStudio/VisualBasic/Impl/Snippets/SnippetExpansionClient.vb +++ b/src/VisualStudio/VisualBasic/Impl/Snippets/SnippetExpansionClient.vb @@ -87,7 +87,11 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Snippets End Select End Function - Friend Overrides Function AddImports(document As Document, snippetNode As XElement, placeSystemNamespaceFirst As Boolean, cancellationToken As CancellationToken) As Document + Friend Overrides Function AddImports(document As Document, + position As Integer, + snippetNode As XElement, + placeSystemNamespaceFirst As Boolean, + cancellationToken As CancellationToken) As Document Dim importsNode = snippetNode.Element(XName.Get("Imports", snippetNode.Name.NamespaceName)) If importsNode Is Nothing OrElse Not importsNode.HasElements() Then