From 017a90988de1ac8eb457604bbfffe5bf62076181 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Fri, 24 Jul 2020 01:19:10 -0700 Subject: [PATCH] Introduces a new hover provider, under V2 of the protocol, that uses Roslyn's QuickInfoService The existing provider uses a custom handler, which this replaces. Among other benefits, this brings nullability display when available, and ensures that any new additions to roslyn's info get propagated to users of this service. Unfortunately, however, TaggedText in VS is significantly more powerful than vscode's hover renderer: that simply uses markdown. Their implementation does not support any extensions to enable C# formatting of code inline, I created a poor-man's substitute: for the description line, we treat the whole line as C#. It does mean there can be a bit of odd formatting with `(parameter)` or similar, but this exactly mirrors what typescript does so I don't think it's a big deal. For other sections, I picked sections that looked ok when formatted as C# code, and I otherwise did a simple conversion, as best I could, from tagged text to inline markdown. --- .../Models/v2/QuickInfoRequest.cs | 9 + .../Models/v2/QuickInfoResponse.cs | 35 + .../OmniSharpEndpoints.cs | 2 + .../Services/QuickInfoProvider.cs | 197 +++++ .../QuickInfoProviderFacts.cs | 773 ++++++++++++++++++ 5 files changed, 1016 insertions(+) create mode 100644 src/OmniSharp.Abstractions/Models/v2/QuickInfoRequest.cs create mode 100644 src/OmniSharp.Abstractions/Models/v2/QuickInfoResponse.cs create mode 100644 src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs create mode 100644 tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs diff --git a/src/OmniSharp.Abstractions/Models/v2/QuickInfoRequest.cs b/src/OmniSharp.Abstractions/Models/v2/QuickInfoRequest.cs new file mode 100644 index 0000000000..ab365d91cb --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v2/QuickInfoRequest.cs @@ -0,0 +1,9 @@ +using OmniSharp.Mef; + +namespace OmniSharp.Models.v2 +{ + [OmniSharpEndpoint(OmniSharpEndpoints.V2.QuickInfo, typeof(QuickInfoRequest), typeof(QuickInfoResponse))] + public class QuickInfoRequest : Request + { + } +} diff --git a/src/OmniSharp.Abstractions/Models/v2/QuickInfoResponse.cs b/src/OmniSharp.Abstractions/Models/v2/QuickInfoResponse.cs new file mode 100644 index 0000000000..a678a6fbba --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v2/QuickInfoResponse.cs @@ -0,0 +1,35 @@ +#nullable enable +namespace OmniSharp.Models.v2 +{ + public class QuickInfoResponse + { + /// + /// Description of the symbol under the cursor. This is expected to be rendered as a C# codeblock + /// + public string? Description { get; set; } + + /// + /// Documentation of the symbol under the cursor, if present. It is expected to be rendered as markdown. + /// + public string? Summary { get; set; } + + /// + /// Other relevant information to the symbol under the cursor. + /// + public QuickInfoResponseSection[]? RemainingSections { get; set; } + } + + public struct QuickInfoResponseSection + { + /// + /// If true, the text should be rendered as C# code. If false, the text should be rendered as markdown. + /// + public bool IsCSharpCode { get; set; } + public string Text { get; set; } + + public override string ToString() + { + return $@"{{ IsCSharpCode = {IsCSharpCode}, Text = ""{Text}"" }}"; + } + } +} diff --git a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs index 58f82bb2f8..ac4b1c0396 100644 --- a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs +++ b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs @@ -65,6 +65,8 @@ public static class V2 public const string CodeStructure = "/v2/codestructure"; public const string Highlight = "/v2/highlight"; + + public const string QuickInfo = "/v2/quickinfo"; } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs b/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs new file mode 100644 index 0000000000..9a277ca2a9 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs @@ -0,0 +1,197 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.QuickInfo; +using Microsoft.CodeAnalysis.Text; +using OmniSharp.Mef; +using OmniSharp.Models.v2; +using OmniSharp.Options; + +#nullable enable + +namespace OmniSharp.Roslyn.CSharp.Services +{ + [OmniSharpHandler(OmniSharpEndpoints.V2.QuickInfo, LanguageNames.CSharp)] + public class QuickInfoProvider : IRequestHandler + { + // Based on https://github.com/dotnet/roslyn/blob/master/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs + + // These are internal tag values taken from https://github.com/dotnet/roslyn/blob/master/src/Features/Core/Portable/Common/TextTags.cs + // They're copied here so that we can ensure we render blocks correctly in the markdown + + /// + /// Indicates the start of a text container. The elements after through (but not + /// including) the matching are rendered in a rectangular block which is positioned + /// as an inline element relative to surrounding elements. The text of the element + /// itself precedes the content of the container, and is typically a bullet or number header for an item in a + /// list. + /// + private const string ContainerStart = nameof(ContainerStart); + /// + /// Indicates the end of a text container. See . + /// + private const string ContainerEnd = nameof(ContainerEnd); + + private readonly OmniSharpWorkspace _workspace; + private readonly FormattingOptions _formattingOptions; + + [ImportingConstructor] + public QuickInfoProvider(OmniSharpWorkspace workspace, FormattingOptions formattingOptions) + { + _workspace = workspace; + _formattingOptions = formattingOptions; + } + + public async Task Handle(QuickInfoRequest request) + { + var document = _workspace.GetDocument(request.FileName); + var response = new QuickInfoResponse(); + + if (document is null) + { + return response; + } + + var quickInfoService = QuickInfoService.GetService(document); + if (quickInfoService is null) + { + return response; + } + + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines.GetPosition(new LinePosition(request.Line, request.Column)); + + var quickInfo = await quickInfoService.GetQuickInfoAsync(document, position); + if (quickInfo is null) + { + return response; + } + + + var sb = new StringBuilder(); + response.Description = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description)?.Text; + + var documentation = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments); + if (documentation is object) + { + response.Summary = getMarkdown(documentation.TaggedParts); + } + + response.RemainingSections = quickInfo.Sections + .Where(s => s.Kind != QuickInfoSectionKinds.Description && s.Kind != QuickInfoSectionKinds.DocumentationComments) + .Select(s => + { + switch (s.Kind) + { + case QuickInfoSectionKinds.AnonymousTypes: + case QuickInfoSectionKinds.TypeParameters: + return new QuickInfoResponseSection { IsCSharpCode = true, Text = s.Text }; + + default: + return new QuickInfoResponseSection { IsCSharpCode = false, Text = getMarkdown(s.TaggedParts) }; + } + }) + .ToArray(); + + return response; + + string getMarkdown(ImmutableArray taggedTexts) + { + bool isInCodeBlock = false; + var sb = new StringBuilder(); + for (int i = 0; i < taggedTexts.Length; i++) + { + var current = taggedTexts[i]; + + switch (current.Tag) + { + case TextTags.Text when !isInCodeBlock: + sb.Append(current.Text); + break; + + case TextTags.Text: + endBlock(); + sb.Append(current.Text); + break; + + case TextTags.Space when isInCodeBlock: + if (nextIsTag(TextTags.Text, i)) + { + endBlock(); + } + + sb.Append(current.Text); + break; + + case TextTags.Space: + case TextTags.Punctuation: + sb.Append(current.Text); + break; + + case ContainerStart: + // Markdown needs 2 linebreaks to make a new paragraph + addNewline(); + addNewline(); + sb.Append(current.Text); + break; + + case ContainerEnd: + // Markdown needs 2 linebreaks to make a new paragraph + addNewline(); + addNewline(); + break; + + case TextTags.LineBreak: + if (!nextIsTag(ContainerStart, i) && !nextIsTag(ContainerEnd, i)) + { + addNewline(); + addNewline(); + } + break; + + default: + if (!isInCodeBlock) + { + isInCodeBlock = true; + sb.Append('`'); + } + sb.Append(current.Text); + break; + } + } + + if (isInCodeBlock) + { + endBlock(); + } + + return sb.ToString().Trim(); + + void addNewline() + { + if (isInCodeBlock) + { + endBlock(); + } + + sb.Append(_formattingOptions.NewLine); + } + + void endBlock() + { + sb.Append('`'); + isInCodeBlock = false; + } + + bool nextIsTag(string tag, int i) + { + int nextI = i + 1; + return nextI < taggedTexts.Length && taggedTexts[nextI].Tag == tag; + } + } + } + } +} diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs new file mode 100644 index 0000000000..9778b98ef2 --- /dev/null +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs @@ -0,0 +1,773 @@ +using System.IO; +using System.Threading.Tasks; +using OmniSharp.Models.v2; +using OmniSharp.Options; +using OmniSharp.Roslyn.CSharp.Services; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Roslyn.CSharp.Tests +{ + public class QuickInfoProviderFacts : AbstractSingleRequestHandlerTestFixture + { + protected override string EndpointName => OmniSharpEndpoints.V2.QuickInfo; + + public QuickInfoProviderFacts(ITestOutputHelper output, SharedOmniSharpHostFixture sharedOmniSharpHostFixture) + : base(output, sharedOmniSharpHostFixture) + { } + + [Fact] + public async Task ParameterDocumentation() + { + const string source = @"namespace N +{ + class C + { + /// Some content + public void M(int i) + { + _ = i; + } + } +}"; + + var testFile = new TestFile("dummy.cs", source); + SharedOmniSharpTestHost.AddFilesToWorkspace(testFile); + var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); + + var request = new QuickInfoRequest { FileName = testFile.FileName, Line = 7, Column = 17 }; + var response = await requestHandler.Handle(request); + + Assert.Equal("(parameter) int i", response.Description); + Assert.Equal("Some content `C`", response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task OmitsNamespaceForNonRegularCSharpSyntax() + { + var source = @"class Foo {}"; + + var testFile = new TestFile("dummy.csx", source); + var workspace = TestHelpers.CreateCsxWorkspace(testFile); + + var controller = new QuickInfoProvider(workspace, new FormattingOptions()); + var response = await controller.Handle(new QuickInfoRequest { FileName = testFile.FileName, Line = 0, Column = 7 }); + + Assert.Equal("class Foo", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + [Fact] + public async Task TypesFromInlineAssemlbyReferenceContainDocumentation() + { + var testAssemblyPath = Path.Combine(TestAssets.Instance.TestBinariesFolder, "ClassLibraryWithDocumentation.dll"); + var source = + $@"#r ""{testAssemblyPath}"" + using ClassLibraryWithDocumentation; + Documented$$Class c; + "; + + var testFile = new TestFile("dummy.csx", source); + var position = testFile.Content.GetPointFromPosition(); + var workspace = TestHelpers.CreateCsxWorkspace(testFile); + + var controller = new QuickInfoProvider(workspace, new FormattingOptions()); + var response = await controller.Handle(new QuickInfoRequest { FileName = testFile.FileName, Line = position.Line, Column = position.Offset }); + + Assert.Equal("class ClassLibraryWithDocumentation.DocumentedClass", response.Description); + Assert.Equal("This class performs an important function.", response.Summary?.Trim()); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task OmitsNamespaceForTypesInGlobalNamespace() + { + const string source = @"namespace Bar { + class Foo {} + } + class Baz {}"; + + var testFile = new TestFile("dummy.cs", source); + SharedOmniSharpTestHost.AddFilesToWorkspace(testFile); + var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); + + var requestInNormalNamespace = new QuickInfoRequest { FileName = testFile.FileName, Line = 1, Column = 19 }; + var responseInNormalNamespace = await requestHandler.Handle(requestInNormalNamespace); + + var requestInGlobalNamespace = new QuickInfoRequest { FileName = testFile.FileName, Line = 3, Column = 19 }; + var responseInGlobalNamespace = await requestHandler.Handle(requestInGlobalNamespace); + + Assert.Equal("class Bar.Foo", responseInNormalNamespace.Description); + Assert.Null(responseInNormalNamespace.Summary); + Assert.Empty(responseInNormalNamespace.RemainingSections); + Assert.Equal("class Baz", responseInGlobalNamespace.Description); + Assert.Null(responseInGlobalNamespace.Summary); + Assert.Empty(responseInGlobalNamespace.RemainingSections); + } + + [Fact] + public async Task IncludesNamespaceForRegularCSharpSyntax() + { + const string source = @"namespace Bar { + class Foo {} + }"; + + var testFile = new TestFile("dummy.cs", source); + SharedOmniSharpTestHost.AddFilesToWorkspace(testFile); + var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); + + var request = new QuickInfoRequest { FileName = testFile.FileName, Line = 1, Column = 19 }; + var response = await requestHandler.Handle(request); + + Assert.Equal("class Bar.Foo", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task IncludesContainingTypeFoNestedTypesForRegularCSharpSyntax() + { + var source = @"namespace Bar { + class Foo { + class Xyz {} + } + }"; + + var testFile = new TestFile("dummy.cs", source); + SharedOmniSharpTestHost.AddFilesToWorkspace(testFile); + var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); + + var request = new QuickInfoRequest { FileName = testFile.FileName, Line = 2, Column = 27 }; + var response = await requestHandler.Handle(request); + + Assert.Equal("class Bar.Foo.Xyz", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task IncludesContainingTypeFoNestedTypesForNonRegularCSharpSyntax() + { + var source = @"class Foo { + class Bar {} + }"; + + var testFile = new TestFile("dummy.csx", source); + var workspace = TestHelpers.CreateCsxWorkspace(testFile); + + var controller = new QuickInfoProvider(workspace, new FormattingOptions()); + var request = new QuickInfoRequest { FileName = testFile.FileName, Line = 1, Column = 23 }; + var response = await controller.Handle(request); + + Assert.Equal("class Foo.Bar", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + private static TestFile s_testFile = new TestFile("dummy.cs", + @"using System; + using Bar2; + using System.Collections.Generic; + namespace Bar { + class Foo { + public Foo() { + Console.WriteLine(""abc""); + } + + public void MyMethod(string name, Foo foo, Foo2 foo2) { }; + + private Foo2 _someField = new Foo2(); + + public Foo2 SomeProperty { get; } + + public IDictionary> SomeDict { get; } + + public void Compute(int index = 2) { } + + private const int foo = 1; + } + } + + namespace Bar2 { + class Foo2 { + } + } + + namespace Bar3 { + enum Foo3 { + Val1 = 1, + Val2 + } + } + "); + + [Fact] + public async Task DisplayFormatForMethodSymbol_Invocation() + { + var response = await GetTypeLookUpResponse(line: 6, column: 35); + + Assert.Equal("void Console.WriteLine(string value) (+ 18 overloads)", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatForMethodSymbol_Declaration() + { + var response = await GetTypeLookUpResponse(line: 9, column: 35); + Assert.Equal("void Foo.MyMethod(string name, Foo foo, Foo2 foo2)", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_TypeSymbol_Primitive() + { + var response = await GetTypeLookUpResponse(line: 9, column: 46); + Assert.Equal("class System.String", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_TypeSymbol_ComplexType_SameNamespace() + { + var response = await GetTypeLookUpResponse(line: 9, column: 56); + Assert.Equal("class Bar.Foo", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_TypeSymbol_ComplexType_DifferentNamespace() + { + var response = await GetTypeLookUpResponse(line: 9, column: 67); + Assert.Equal("class Bar2.Foo2", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_TypeSymbol_WithGenerics() + { + var response = await GetTypeLookUpResponse(line: 15, column: 36); + Assert.Equal("interface System.Collections.Generic.IDictionary", response.Description); + Assert.Null(response.Summary); + Assert.Equal(new[] + { + new QuickInfoResponseSection{ IsCSharpCode = true, Text = @" +TKey is string +TValue is IEnumerable" } + }, response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatForParameterSymbol_Name_Primitive() + { + var response = await GetTypeLookUpResponse(line: 9, column: 51); + Assert.Equal("(parameter) string name", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_ParameterSymbol_ComplexType_SameNamespace() + { + var response = await GetTypeLookUpResponse(line: 9, column: 60); + Assert.Equal("(parameter) Foo foo", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_ParameterSymbol_Name_ComplexType_DifferentNamespace() + { + var response = await GetTypeLookUpResponse(line: 9, column: 71); + Assert.Equal("(parameter) Foo2 foo2", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_ParameterSymbol_Name_WithDefaultValue() + { + var response = await GetTypeLookUpResponse(line: 17, column: 48); + Assert.Equal("(parameter) int index = 2", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_FieldSymbol() + { + var response = await GetTypeLookUpResponse(line: 11, column: 38); + Assert.Equal("(field) Foo2 Foo._someField", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_FieldSymbol_WithConstantValue() + { + var response = await GetTypeLookUpResponse(line: 19, column: 41); + Assert.Equal("(constant) int Foo.foo = 1", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_EnumValue() + { + var response = await GetTypeLookUpResponse(line: 31, column: 23); + Assert.Equal("Foo3.Val2 = 2", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_PropertySymbol() + { + var response = await GetTypeLookUpResponse(line: 13, column: 38); + Assert.Equal("Foo2 Foo.SomeProperty { get; }", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task DisplayFormatFor_PropertySymbol_WithGenerics() + { + var response = await GetTypeLookUpResponse(line: 15, column: 70); + Assert.Equal("IDictionary> Foo.SomeDict { get; }", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationRemarksText() + { + string content = @" +class testissue +{ + ///You may have some additional information about this class here. + public static bool C$$ompare(int gameObject, string tagName) + { + return gameObject.TagifyCompareTag(tagName); + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("bool testissue.Compare(int gameObject, string tagName)", response.Description); + Assert.Null(response.Summary); + Assert.Equal( + new[] { new QuickInfoResponseSection { IsCSharpCode = false, Text = "You may have some additional information about this class here." } }, + response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationSummaryText() + { + string content = @" +class testissue +{ + ///Checks if object is tagged with the tag. + public static bool C$$ompare(int gameObject, string tagName) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("bool testissue.Compare(int gameObject, string tagName)", response.Description); + Assert.Equal("Checks if object is tagged with the tag.", response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationReturnsText() + { + string content = @" +class testissue +{ + ///Returns true if object is tagged with tag. + public static bool C$$ompare(int gameObject, string tagName) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("bool testissue.Compare(int gameObject, string tagName)", response.Description); + Assert.Null(response.Summary); + Assert.Equal(new[] { new QuickInfoResponseSection { IsCSharpCode = false, Text = "Returns:\n\n Returns true if object is tagged with tag." } }, + response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationExampleText() + { + string content = @" +class testissue +{ + ///Checks if object is tagged with the tag. + public static bool C$$ompare(int gameObject, string tagName) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + //var expected = + //@"Checks if object is tagged with the tag."; + Assert.Equal("bool testissue.Compare(int gameObject, string tagName)", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationExceptionText() + { + string content = @" +class testissue +{ + ///A description + ///B description + public static bool C$$ompare(int gameObject, string tagName) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("bool testissue.Compare(int gameObject, string tagName)", response.Description); + Assert.Null(response.Summary); + Assert.Equal(new[] { new QuickInfoResponseSection { IsCSharpCode = false, Text = "Exceptions:\n\n A\n\n B" } }, + response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationParameter() + { + string content = @" +class testissue +{ + /// The game object. + /// Name of the tag. + public static bool C$$ompare(int gameObject, string tagName) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("bool testissue.Compare(int gameObject, string tagName)", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationTypeParameter() + { + string content = @" +public class TestClass +{ + /// + /// Creates a new array of arbitrary type and adds the elements of incoming list to it if possible + /// + /// The element type of the array + /// The element type of the list + public static T[] m$$kArray(int n, List list) + { + return new T[n]; + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("T[] TestClass.mkArray(int n, List list)", response.Description); + Assert.Equal("Creates a new array of arbitrary type `T` and adds the elements of incoming list to it if possible", response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationTypeParameter_TypeParam1() + { + string content = @" +public class TestClass +{ + /// + /// Creates a new array of arbitrary type and adds the elements of incoming list to it if possible + /// + /// The element type of the array + /// The element type of the list + public static T[] mkArray(int n, List list) + { + return new T[n]; + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("T in TestClass.mkArray", response.Description); + Assert.Equal("The element type of the array", response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationTypeParameter_TypeParam2() + { + string content = @" +public class TestClass +{ + /// + /// Creates a new array of arbitrary type and adds the elements of incoming list to it if possible + /// + /// The element type of the array + /// The element type of the list + public static T[] mkArray(int n, List list) + { + return new T[n]; + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Null(response.Description); + Assert.Null(response.Summary); + Assert.Null(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationValueText() + { + string content = +@"public class Employee +{ + private string _name; + + /// The Name property represents the employee's name. + /// The Name property gets/sets the value of the string field, _name. + public string Na$$me + { + } +} +"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("string Employee.Name { }", response.Description); + Assert.Equal("The Name property represents the employee's name.", response.Summary); + Assert.Equal(new[] { new QuickInfoResponseSection { IsCSharpCode = false, Text = "Value:\n\n The Name property gets/sets the value of the string field, _name." } }, + response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationNestedTagSee() + { + string content = @" +public class TestClass +{ + /// DoWork is a method in the TestClass class. for information about output statements. + public static void Do$$Work(int Int1) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("void TestClass.DoWork(int Int1)", response.Description); + Assert.Equal("DoWork is a method in the TestClass class. `System.Console.WriteLine(string)` for information about output statements.", + response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationNestedTagParamRef() + { + string content = @" +public class TestClass +{ + /// Creates a new array of arbitrary type + /// The element type of the array + public static T[] mk$$Array(int n) + { + return new T[n]; + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("T[] TestClass.mkArray(int n)", response.Description); + Assert.Equal("Creates a new array of arbitrary type `T`", response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationNestedTagCode() + { + string content = @" +public class TestClass +{ + /// This sample shows how to call the method. + /// + /// class TestClass + /// { + /// static int Main() + /// { + /// return GetZero(); + /// } + /// } + /// + /// + public static int $$GetZero() + { + return 0; + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("int TestClass.GetZero()", response.Description); + Assert.Null(response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationNestedTagPara() + { + string content = @" +public class TestClass +{ + /// DoWork is a method in the TestClass class. + /// Here's how you could make a second paragraph in a description. + /// + public static void Do$$Work(int Int1) + { + } +} + "; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("void TestClass.DoWork(int Int1)", response.Description); + Assert.Equal("DoWork is a method in the TestClass class.\n\n\n\nHere's how you could make a second paragraph in a description.", response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationNestedTagSeeAlso() + { + string content = @" +public class TestClass +{ + /// DoWork is a method in the TestClass class. + /// + /// + public static void Do$$Work(int Int1) + { + } + + static void Main() + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("void TestClass.DoWork(int Int1)", response.Description); + Assert.Equal("DoWork is a method in the TestClass class. `TestClass.Main()`", response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationSummaryAndParam() + { + string content = @" +class testissue +{ + ///Checks if object is tagged with the tag. + /// The game object. + /// Name of the tag. + public static bool C$$ompare(int gameObject, string tagName) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("bool testissue.Compare(int gameObject, string tagName)", response.Description); + Assert.Equal("Checks if object is tagged with the tag.", response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationManyTags() + { + string content = @" +class testissue +{ + ///Checks if object is tagged with the tag. + ///The game object. + ///Invoke using A.Compare(5) where A is an instance of the class testissue. + ///The element type of the array + ///Thrown when something goes wrong + ///You may have some additional information about this class here. + ///Returns an array of type . + public static T[] C$$ompare(int gameObject) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("T[] testissue.Compare(int gameObject)", response.Description); + Assert.Equal("Checks if object is tagged with the tag.", response.Summary); + Assert.Equal(new[] { + new QuickInfoResponseSection { IsCSharpCode = false, Text = "You may have some additional information about this class here." }, + new QuickInfoResponseSection { IsCSharpCode = false, Text = "Returns:\n\n Returns an array of type `T`." }, + new QuickInfoResponseSection { IsCSharpCode = false, Text = "Exceptions:\n\n `System.Exception`" } + }, response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationSpaceBeforeText() + { + string content = @" +public class TestClass +{ + /// DoWork is a method in the TestClass class. + public static void Do$$Work(int Int1) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("void TestClass.DoWork(int Int1)", response.Description); + Assert.Equal("DoWork is a method in the TestClass class.", response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationForParameters1() + { + string content = @" +class testissue +{ + /// The game object. + /// Name of the tag. + public static bool Compare(int gam$$eObject, string tagName) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("(parameter) int gameObject", response.Description); + Assert.Equal("The game object.", response.Summary); + Assert.Empty(response.RemainingSections); + } + + [Fact] + public async Task StructuredDocumentationForParameters2() + { + string content = @" +class testissue +{ + /// The game object. + /// Name of the tag. + public static bool Compare(int gameObject, string tag$$Name) + { + } +}"; + var response = await GetTypeLookUpResponse(content); + Assert.Equal("(parameter) string tagName", response.Description); + Assert.Equal("Name of the tag.", response.Summary); + Assert.Empty(response.RemainingSections); + } + + private async Task GetTypeLookUpResponse(string content) + { + TestFile testFile = new TestFile("dummy.cs", content); + SharedOmniSharpTestHost.AddFilesToWorkspace(testFile); + var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); + var point = testFile.Content.GetPointFromPosition(); + var request = new QuickInfoRequest { FileName = testFile.FileName, Line = point.Line, Column = point.Offset }; + + return await requestHandler.Handle(request); + } + + private async Task GetTypeLookUpResponse(int line, int column) + { + SharedOmniSharpTestHost.AddFilesToWorkspace(s_testFile); + var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); + var request = new QuickInfoRequest { FileName = s_testFile.FileName, Line = line, Column = column }; + + return await requestHandler.Handle(request); + } + } +}