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); + } + } +}