|
| 1 | +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. |
| 2 | + |
| 3 | +using System; |
| 4 | +using System.Collections.Generic; |
| 5 | +using System.Collections.Immutable; |
| 6 | +using System.Linq; |
| 7 | +using System.Text.RegularExpressions; |
| 8 | +using Microsoft.CodeAnalysis; |
| 9 | +using Microsoft.CodeAnalysis.Editor.Shared.Extensions; |
| 10 | +using Microsoft.CodeAnalysis.Editor.UnitTests; |
| 11 | +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; |
| 12 | +using Microsoft.CodeAnalysis.LanguageServer; |
| 13 | +using Microsoft.CodeAnalysis.LanguageServer.CustomProtocol; |
| 14 | +using Microsoft.CodeAnalysis.LanguageServer.Handler; |
| 15 | +using Microsoft.CodeAnalysis.Test.Utilities; |
| 16 | +using Microsoft.CodeAnalysis.Text; |
| 17 | +using Microsoft.VisualStudio.Composition; |
| 18 | +using Microsoft.VisualStudio.Text.Adornments; |
| 19 | +using Newtonsoft.Json; |
| 20 | +using Roslyn.Utilities; |
| 21 | +using Xunit; |
| 22 | +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; |
| 23 | + |
| 24 | +namespace Roslyn.Test.Utilities |
| 25 | +{ |
| 26 | + [UseExportProvider] |
| 27 | + public abstract class AbstractLanguageServerProtocolTests |
| 28 | + { |
| 29 | + protected virtual ExportProvider GetExportProvider() |
| 30 | + { |
| 31 | + var requestHelperTypes = DesktopTestHelpers.GetAllTypesImplementingGivenInterface( |
| 32 | + typeof(IRequestHandler).Assembly, typeof(IRequestHandler)); |
| 33 | + var exportProviderFactory = ExportProviderCache.GetOrCreateExportProviderFactory( |
| 34 | + TestExportProvider.EntireAssemblyCatalogWithCSharpAndVisualBasic |
| 35 | + .WithPart(typeof(LanguageServerProtocol)) |
| 36 | + .WithParts(requestHelperTypes)); |
| 37 | + return exportProviderFactory.CreateExportProvider(); |
| 38 | + } |
| 39 | + |
| 40 | + /// <summary> |
| 41 | + /// Asserts two objects are equivalent by converting to JSON and ignoring whitespace. |
| 42 | + /// </summary> |
| 43 | + /// <typeparam name="T">the JSON object type.</typeparam> |
| 44 | + /// <param name="expected">the expected object to be converted to JSON.</param> |
| 45 | + /// <param name="actual">the actual object to be converted to JSON.</param> |
| 46 | + protected static void AssertJsonEquals<T>(T expected, T actual) |
| 47 | + { |
| 48 | + var expectedStr = JsonConvert.SerializeObject(expected); |
| 49 | + var actualStr = JsonConvert.SerializeObject(actual); |
| 50 | + AssertEqualIgnoringWhitespace(expectedStr, actualStr); |
| 51 | + } |
| 52 | + |
| 53 | + protected static void AssertEqualIgnoringWhitespace(string expected, string actual) |
| 54 | + { |
| 55 | + var expectedWithoutWhitespace = Regex.Replace(expected, @"\s+", string.Empty); |
| 56 | + var actualWithoutWhitespace = Regex.Replace(actual, @"\s+", string.Empty); |
| 57 | + Assert.Equal(expectedWithoutWhitespace, actualWithoutWhitespace); |
| 58 | + } |
| 59 | + |
| 60 | + /// <summary> |
| 61 | + /// Assert that two location lists are equivalent. |
| 62 | + /// Locations are not always returned in a consistent order so they must be sorted. |
| 63 | + /// </summary> |
| 64 | + protected static void AssertLocationsEqual(IEnumerable<LSP.Location> expectedLocations, IEnumerable<LSP.Location> actualLocations) |
| 65 | + { |
| 66 | + var orderedActualLocations = actualLocations.OrderBy(CompareLocations); |
| 67 | + var orderedExpectedLocations = expectedLocations.OrderBy(CompareLocations); |
| 68 | + |
| 69 | + AssertJsonEquals(orderedExpectedLocations, orderedActualLocations); |
| 70 | + |
| 71 | + static int CompareLocations(LSP.Location l1, LSP.Location l2) |
| 72 | + { |
| 73 | + var compareDocument = l1.Uri.OriginalString.CompareTo(l2.Uri.OriginalString); |
| 74 | + var compareRange = CompareRange(l1.Range, l2.Range); |
| 75 | + return compareDocument != 0 ? compareDocument : compareRange; |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + protected static int CompareRange(LSP.Range r1, LSP.Range r2) |
| 80 | + { |
| 81 | + var compareLine = r1.Start.Line.CompareTo(r2.Start.Line); |
| 82 | + var compareChar = r1.Start.Character.CompareTo(r2.Start.Character); |
| 83 | + return compareLine != 0 ? compareLine : compareChar; |
| 84 | + } |
| 85 | + |
| 86 | + protected static string ApplyTextEdits(LSP.TextEdit[] edits, SourceText originalMarkup) |
| 87 | + { |
| 88 | + var text = originalMarkup; |
| 89 | + foreach (var edit in edits) |
| 90 | + { |
| 91 | + var lines = text.Lines; |
| 92 | + var startPosition = ProtocolConversions.PositionToLinePosition(edit.Range.Start); |
| 93 | + var endPosition = ProtocolConversions.PositionToLinePosition(edit.Range.End); |
| 94 | + var textSpan = lines.GetTextSpan(new LinePositionSpan(startPosition, endPosition)); |
| 95 | + text = text.Replace(textSpan, edit.NewText); |
| 96 | + } |
| 97 | + |
| 98 | + return text.ToString(); |
| 99 | + } |
| 100 | + |
| 101 | + protected static LSP.SymbolInformation CreateSymbolInformation(LSP.SymbolKind kind, string name, LSP.Location location, string containerName = null) |
| 102 | + => new LSP.SymbolInformation() |
| 103 | + { |
| 104 | + Kind = kind, |
| 105 | + Name = name, |
| 106 | + Location = location, |
| 107 | + ContainerName = containerName |
| 108 | + }; |
| 109 | + |
| 110 | + protected static LSP.TextDocumentIdentifier CreateTextDocumentIdentifier(Uri uri) |
| 111 | + => new LSP.TextDocumentIdentifier() |
| 112 | + { |
| 113 | + Uri = uri |
| 114 | + }; |
| 115 | + |
| 116 | + protected static LSP.TextDocumentPositionParams CreateTextDocumentPositionParams(LSP.Location caret) |
| 117 | + => new LSP.TextDocumentPositionParams() |
| 118 | + { |
| 119 | + TextDocument = CreateTextDocumentIdentifier(caret.Uri), |
| 120 | + Position = caret.Range.Start |
| 121 | + }; |
| 122 | + |
| 123 | + protected static LSP.MarkupContent CreateMarkupContent(LSP.MarkupKind kind, string value) |
| 124 | + => new LSP.MarkupContent() |
| 125 | + { |
| 126 | + Kind = kind, |
| 127 | + Value = value |
| 128 | + }; |
| 129 | + |
| 130 | + protected static LSP.CompletionParams CreateCompletionParams(LSP.Location caret) |
| 131 | + => new LSP.CompletionParams() |
| 132 | + { |
| 133 | + TextDocument = CreateTextDocumentIdentifier(caret.Uri), |
| 134 | + Position = caret.Range.Start, |
| 135 | + Context = new LSP.CompletionContext() |
| 136 | + { |
| 137 | + // TODO - completion should respect context. |
| 138 | + } |
| 139 | + }; |
| 140 | + |
| 141 | + protected static LSP.VSCompletionItem CreateCompletionItem(string text, LSP.CompletionItemKind kind, string[] tags, LSP.CompletionParams requestParameters) |
| 142 | + => new LSP.VSCompletionItem() |
| 143 | + { |
| 144 | + FilterText = text, |
| 145 | + InsertText = text, |
| 146 | + Label = text, |
| 147 | + SortText = text, |
| 148 | + InsertTextFormat = LSP.InsertTextFormat.Plaintext, |
| 149 | + Kind = kind, |
| 150 | + Data = new CompletionResolveData() |
| 151 | + { |
| 152 | + DisplayText = text, |
| 153 | + CompletionParams = requestParameters |
| 154 | + }, |
| 155 | + Icon = tags != null ? new ImageElement(tags.ToImmutableArray().GetFirstGlyph().GetImageId()) : null |
| 156 | + }; |
| 157 | + |
| 158 | + private protected static RunCodeActionParams CreateRunCodeActionParams(string codeActionTitle, LSP.Location location) |
| 159 | + => new RunCodeActionParams() |
| 160 | + { |
| 161 | + CodeActionParams = new LSP.CodeActionParams() |
| 162 | + { |
| 163 | + TextDocument = CreateTextDocumentIdentifier(location.Uri), |
| 164 | + Range = location.Range, |
| 165 | + Context = new LSP.CodeActionContext() |
| 166 | + }, |
| 167 | + Title = codeActionTitle |
| 168 | + }; |
| 169 | + |
| 170 | + /// <summary> |
| 171 | + /// Creates a solution with a document. |
| 172 | + /// </summary> |
| 173 | + /// <returns>the solution and the annotated ranges in the document.</returns> |
| 174 | + protected (Solution solution, Dictionary<string, IList<LSP.Location>> locations) CreateTestSolution(string markup) |
| 175 | + => CreateTestSolution(new string[] { markup }); |
| 176 | + |
| 177 | + /// <summary> |
| 178 | + /// Create a solution with multiple documents. |
| 179 | + /// </summary> |
| 180 | + /// <returns> |
| 181 | + /// the solution with the documents plus a list for each document of all annotated ranges in the document. |
| 182 | + /// </returns> |
| 183 | + protected (Solution solution, Dictionary<string, IList<LSP.Location>> locations) CreateTestSolution(string[] markups) |
| 184 | + { |
| 185 | + using var workspace = TestWorkspace.CreateCSharp(markups, exportProvider: GetExportProvider()); |
| 186 | + var solution = workspace.CurrentSolution; |
| 187 | + var locations = new Dictionary<string, IList<LSP.Location>>(); |
| 188 | + |
| 189 | + foreach (var document in workspace.Documents) |
| 190 | + { |
| 191 | + var text = document.TextBuffer.AsTextContainer().CurrentText; |
| 192 | + foreach (var kvp in document.AnnotatedSpans) |
| 193 | + { |
| 194 | + locations.GetOrAdd(kvp.Key, CreateLocation) |
| 195 | + .AddRange(kvp.Value.Select(s => ProtocolConversions.TextSpanToLocation(s, text, new Uri(GetDocumentFilePathFromName(document.Name))))); |
| 196 | + } |
| 197 | + |
| 198 | + // Pass in the text without markup. |
| 199 | + workspace.ChangeSolution(ChangeDocumentFilePathToValidURI(workspace.CurrentSolution, document, text)); |
| 200 | + } |
| 201 | + |
| 202 | + return (workspace.CurrentSolution, locations); |
| 203 | + |
| 204 | + // local functions |
| 205 | + static List<LSP.Location> CreateLocation(string s) => new List<LSP.Location>(); |
| 206 | + } |
| 207 | + |
| 208 | + // Private protected because LanguageServerProtocol is internal |
| 209 | + private protected static LanguageServerProtocol GetLanguageServer(Solution solution) |
| 210 | + { |
| 211 | + var workspace = (TestWorkspace)solution.Workspace; |
| 212 | + return workspace.ExportProvider.GetExportedValue<LanguageServerProtocol>(); |
| 213 | + } |
| 214 | + |
| 215 | + private static string GetDocumentFilePathFromName(string documentName) |
| 216 | + => "C:\\" + documentName; |
| 217 | + |
| 218 | + /// <summary> |
| 219 | + /// Changes the document file path. |
| 220 | + /// Adds/Removes the document instead of updating file path due to |
| 221 | + /// https://github.com/dotnet/roslyn/issues/34837 |
| 222 | + /// </summary> |
| 223 | + private static Solution ChangeDocumentFilePathToValidURI(Solution originalSolution, TestHostDocument originalDocument, SourceText text) |
| 224 | + { |
| 225 | + var documentName = originalDocument.Name; |
| 226 | + var documentPath = GetDocumentFilePathFromName(documentName); |
| 227 | + |
| 228 | + var solution = originalSolution.RemoveDocument(originalDocument.Id); |
| 229 | + |
| 230 | + var newDocumentId = DocumentId.CreateNewId(originalDocument.Project.Id); |
| 231 | + return solution.AddDocument(newDocumentId, documentName, text, filePath: documentPath); |
| 232 | + } |
| 233 | + } |
| 234 | +} |
0 commit comments