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