From af158582d77fc715e42a8feb00be07325b1c9e96 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Thu, 23 Jul 2020 19:00:35 -0700 Subject: [PATCH 1/5] Updated .NET SDK. --- .pipelines/init.yml | 2 +- azure-pipelines.yml | 2 +- build.json | 2 +- global.json | 2 +- test-assets/test-projects/global.json | 2 +- tests/OmniSharp.MSBuild.Tests/ProjectLoadListenerTests.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pipelines/init.yml b/.pipelines/init.yml index 18ea38537b..56b43d7843 100644 --- a/.pipelines/init.yml +++ b/.pipelines/init.yml @@ -1,7 +1,7 @@ parameters: # Configuration: Release Verbosity: Normal - DotNetVersion: "3.1.201" + DotNetVersion: "3.1.302" CakeVersion: "0.32.1" NuGetVersion: "4.9.2" steps: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3405336a87..f06fe4f0ae 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,7 +22,7 @@ resources: variables: Verbosity: Diagnostic - DotNetVersion: "3.1.201" + DotNetVersion: "3.1.302" CakeVersion: "0.32.1" NuGetVersion: "4.9.2" GitVersionVersion: "5.0.1" diff --git a/build.json b/build.json index 69eece7ac1..d5a5937666 100644 --- a/build.json +++ b/build.json @@ -2,7 +2,7 @@ "DotNetInstallScriptURL": "https://dot.net/v1", "DotNetChannel": "Preview", "DotNetVersions": [ - "3.1.201", + "3.1.302", "5.0.100-preview.7.20366.6" ], "RequiredMonoVersion": "6.6.0", diff --git a/global.json b/global.json index 4f07b54704..fcfbc1acf6 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "3.1.201" + "version": "3.1.302" } } diff --git a/test-assets/test-projects/global.json b/test-assets/test-projects/global.json index 4f07b54704..fcfbc1acf6 100644 --- a/test-assets/test-projects/global.json +++ b/test-assets/test-projects/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "3.1.201" + "version": "3.1.302" } } diff --git a/tests/OmniSharp.MSBuild.Tests/ProjectLoadListenerTests.cs b/tests/OmniSharp.MSBuild.Tests/ProjectLoadListenerTests.cs index b6961521da..74460e8e91 100644 --- a/tests/OmniSharp.MSBuild.Tests/ProjectLoadListenerTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/ProjectLoadListenerTests.cs @@ -224,7 +224,7 @@ public async Task The_correct_sdk_version_is_emitted() using (var host = CreateMSBuildTestHost(testProject.Directory, emitter.AsExportDescriptionProvider(LoggerFactory))) { Assert.Single(emitter.ReceivedMessages); - Assert.Equal(GetHashedFileExtension("3.1.201"), emitter.ReceivedMessages[0].SdkVersion); + Assert.Equal(GetHashedFileExtension("3.1.302"), emitter.ReceivedMessages[0].SdkVersion); } } From 017a90988de1ac8eb457604bbfffe5bf62076181 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Fri, 24 Jul 2020 01:19:10 -0700 Subject: [PATCH 2/5] 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); + } + } +} From 279424136056b559bcceba7e6f436ec99ff7bff4 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Fri, 24 Jul 2020 16:34:47 -0700 Subject: [PATCH 3/5] PR Feedback Rewrite quickinfo loop to be fully iterative and not create closures. Moved the new service to V1. Simplify the output. --- .../Models/v1/QuickInfoRequest.cs | 9 + .../Models/{v2 => v1}/QuickInfoResponse.cs | 16 +- .../Models/v2/QuickInfoRequest.cs | 9 - .../OmniSharpEndpoints.cs | 2 +- .../Services/QuickInfoProvider.cs | 156 +++++++--- .../QuickInfoProviderFacts.cs | 272 ++++++++---------- 6 files changed, 249 insertions(+), 215 deletions(-) create mode 100644 src/OmniSharp.Abstractions/Models/v1/QuickInfoRequest.cs rename src/OmniSharp.Abstractions/Models/{v2 => v1}/QuickInfoResponse.cs (55%) delete mode 100644 src/OmniSharp.Abstractions/Models/v2/QuickInfoRequest.cs diff --git a/src/OmniSharp.Abstractions/Models/v1/QuickInfoRequest.cs b/src/OmniSharp.Abstractions/Models/v1/QuickInfoRequest.cs new file mode 100644 index 0000000000..884e9eb855 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v1/QuickInfoRequest.cs @@ -0,0 +1,9 @@ +using OmniSharp.Mef; + +namespace OmniSharp.Models +{ + [OmniSharpEndpoint(OmniSharpEndpoints.QuickInfo, typeof(QuickInfoRequest), typeof(QuickInfoResponse))] + public class QuickInfoRequest : Request + { + } +} diff --git a/src/OmniSharp.Abstractions/Models/v2/QuickInfoResponse.cs b/src/OmniSharp.Abstractions/Models/v1/QuickInfoResponse.cs similarity index 55% rename from src/OmniSharp.Abstractions/Models/v2/QuickInfoResponse.cs rename to src/OmniSharp.Abstractions/Models/v1/QuickInfoResponse.cs index a678a6fbba..4bdb73988e 100644 --- a/src/OmniSharp.Abstractions/Models/v2/QuickInfoResponse.cs +++ b/src/OmniSharp.Abstractions/Models/v1/QuickInfoResponse.cs @@ -1,22 +1,14 @@ #nullable enable -namespace OmniSharp.Models.v2 +using System.Collections.Immutable; + +namespace OmniSharp.Models { 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 ImmutableArray Sections { get; set; } } public struct QuickInfoResponseSection diff --git a/src/OmniSharp.Abstractions/Models/v2/QuickInfoRequest.cs b/src/OmniSharp.Abstractions/Models/v2/QuickInfoRequest.cs deleted file mode 100644 index ab365d91cb..0000000000 --- a/src/OmniSharp.Abstractions/Models/v2/QuickInfoRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -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/OmniSharpEndpoints.cs b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs index ac4b1c0396..141768c42e 100644 --- a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs +++ b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs @@ -44,6 +44,7 @@ public static class OmniSharpEndpoints public const string Diagnostics = "/diagnostics"; public const string ReAnalyze = "/reanalyze"; + public const string QuickInfo = "/quickinfo"; public static class V2 { @@ -66,7 +67,6 @@ public static class V2 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 index 9a277ca2a9..6e7764b66e 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs @@ -6,21 +6,23 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.QuickInfo; using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Logging; using OmniSharp.Mef; -using OmniSharp.Models.v2; +using OmniSharp.Models; using OmniSharp.Options; #nullable enable namespace OmniSharp.Roslyn.CSharp.Services { - [OmniSharpHandler(OmniSharpEndpoints.V2.QuickInfo, LanguageNames.CSharp)] + [OmniSharpHandler(OmniSharpEndpoints.QuickInfo, LanguageNames.CSharp)] public class QuickInfoProvider : IRequestHandler { - // Based on https://github.com/dotnet/roslyn/blob/master/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs + // Based on https://github.com/dotnet/roslyn/blob/7dc32a952e77c96c31cae6a2ba6d253a558fc7ff/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 + // https://github.com/dotnet/roslyn/issues/46254 tracks making these public /// /// Indicates the start of a text container. The elements after through (but not @@ -34,21 +36,27 @@ public class QuickInfoProvider : IRequestHandler. /// private const string ContainerEnd = nameof(ContainerEnd); + /// + /// Section kind for nullability analysis. + /// + internal const string NullabilityAnalysis = nameof(NullabilityAnalysis); private readonly OmniSharpWorkspace _workspace; private readonly FormattingOptions _formattingOptions; + private readonly ILogger? _logger; [ImportingConstructor] - public QuickInfoProvider(OmniSharpWorkspace workspace, FormattingOptions formattingOptions) + public QuickInfoProvider(OmniSharpWorkspace workspace, FormattingOptions formattingOptions, ILoggerFactory? loggerFactory) { _workspace = workspace; _formattingOptions = formattingOptions; + _logger = loggerFactory?.CreateLogger(); } public async Task Handle(QuickInfoRequest request) { var document = _workspace.GetDocument(request.FileName); - var response = new QuickInfoResponse(); + var response = new QuickInfoResponse() { Sections = ImmutableArray.Empty }; if (document is null) { @@ -58,6 +66,7 @@ public async Task Handle(QuickInfoRequest request) var quickInfoService = QuickInfoService.GetService(document); if (quickInfoService is null) { + _logger?.LogWarning($"QuickInfo service was null for {document.FilePath}"); return response; } @@ -67,54 +76,112 @@ public async Task Handle(QuickInfoRequest request) var quickInfo = await quickInfoService.GetQuickInfoAsync(document, position); if (quickInfo is null) { + _logger?.LogTrace($"No QuickInfo found for {document.FilePath}:{request.Line},{request.Column}"); return response; } + var sectionBuilder = ImmutableArray.CreateBuilder(quickInfo.Sections.Length); + var stringBuilder = new StringBuilder(); - 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) + bool foundDescription = false; + foreach (var section in quickInfo.Sections) { - response.Summary = getMarkdown(documentation.TaggedParts); + switch (section.Kind) + { + case QuickInfoSectionKinds.Description: + sectionBuilder.Insert(0, new QuickInfoResponseSection { IsCSharpCode = true, Text = section.Text }); + foundDescription = true; + break; + + case QuickInfoSectionKinds.TypeParameters: + stringBuilder.Clear(); + foreach (var text in section.TaggedParts) + { + switch (text.Tag) + { + case TextTags.LineBreak: + appendIfNeeded(); + stringBuilder.Clear(); + continue; + + default: + stringBuilder.Append(text.Text); + break; + } + } + + appendIfNeeded(); + break; + + void appendIfNeeded() + { + var currentString = stringBuilder.ToString().Trim(); + if (currentString == string.Empty) + { + return; + } + + sectionBuilder.Add(new QuickInfoResponseSection { IsCSharpCode = true, Text = currentString }); + } + + case QuickInfoSectionKinds.AnonymousTypes: + sectionBuilder.Add(new QuickInfoResponseSection { IsCSharpCode = false, Text = getMarkdown(section.TaggedParts, stringBuilder, _formattingOptions, out int remainingIndex, untilLineBreak: true) }); + + if (remainingIndex < section.TaggedParts.Length) + { + sectionBuilder.Add(new QuickInfoResponseSection + { + IsCSharpCode = true, + Text = string.Concat(section.TaggedParts.Skip(remainingIndex + 1).Select(s => s.Text)) + }); + } + + break; + + case QuickInfoSectionKinds.DocumentationComments: + sectionBuilder.Insert(foundDescription ? 1 : 0, + new QuickInfoResponseSection { IsCSharpCode = false, Text = getMarkdown(section.TaggedParts, stringBuilder, _formattingOptions, out _) }); + break; + + case NullabilityAnalysis: + var nullabilityText = getMarkdown(section.TaggedParts, stringBuilder, _formattingOptions, out _); + if (!nullabilityText.Contains(_formattingOptions.NewLine)) + { + // Italicize the text for emphasis + nullabilityText = $"_{nullabilityText}_"; + } + sectionBuilder.Add(new QuickInfoResponseSection { IsCSharpCode = false, Text = nullabilityText }); + break; + + default: + sectionBuilder.Add(new QuickInfoResponseSection { IsCSharpCode = false, Text = getMarkdown(section.TaggedParts, stringBuilder, _formattingOptions, out _) }); + break; + } } - 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(); + response.Sections = sectionBuilder.ToImmutable(); return response; - string getMarkdown(ImmutableArray taggedTexts) + static string getMarkdown(ImmutableArray taggedTexts, StringBuilder stringBuilder, FormattingOptions formattingOptions, out int lastIndex, bool untilLineBreak = false) { bool isInCodeBlock = false; - var sb = new StringBuilder(); + stringBuilder.Clear(); + lastIndex = 0; for (int i = 0; i < taggedTexts.Length; i++) { var current = taggedTexts[i]; + lastIndex = i; switch (current.Tag) { case TextTags.Text when !isInCodeBlock: - sb.Append(current.Text); + stringBuilder.Append(current.Text); break; case TextTags.Text: endBlock(); - sb.Append(current.Text); + stringBuilder.Append(current.Text); break; case TextTags.Space when isInCodeBlock: @@ -123,32 +190,33 @@ string getMarkdown(ImmutableArray taggedTexts) endBlock(); } - sb.Append(current.Text); + stringBuilder.Append(current.Text); break; case TextTags.Space: case TextTags.Punctuation: - sb.Append(current.Text); + stringBuilder.Append(current.Text); break; case ContainerStart: - // Markdown needs 2 linebreaks to make a new paragraph addNewline(); - addNewline(); - sb.Append(current.Text); + stringBuilder.Append(current.Text); break; case ContainerEnd: - // Markdown needs 2 linebreaks to make a new paragraph - addNewline(); addNewline(); break; + case TextTags.LineBreak when untilLineBreak + && stringBuilder.ToString().Trim() is var currentString + && currentString != string.Empty: + addNewline(); + return currentString; + case TextTags.LineBreak: if (!nextIsTag(ContainerStart, i) && !nextIsTag(ContainerEnd, i)) { addNewline(); - addNewline(); } break; @@ -156,9 +224,9 @@ string getMarkdown(ImmutableArray taggedTexts) if (!isInCodeBlock) { isInCodeBlock = true; - sb.Append('`'); + stringBuilder.Append('`'); } - sb.Append(current.Text); + stringBuilder.Append(current.Text); break; } } @@ -168,7 +236,7 @@ string getMarkdown(ImmutableArray taggedTexts) endBlock(); } - return sb.ToString().Trim(); + return stringBuilder.ToString().Trim(); void addNewline() { @@ -177,12 +245,14 @@ void addNewline() endBlock(); } - sb.Append(_formattingOptions.NewLine); + // Markdown needs 2 linebreaks to make a new paragraph + stringBuilder.Append(formattingOptions.NewLine); + stringBuilder.Append(formattingOptions.NewLine); } void endBlock() { - sb.Append('`'); + stringBuilder.Append('`'); isInCodeBlock = false; } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs index 9778b98ef2..5e34655656 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs @@ -1,17 +1,21 @@ -using System.IO; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; using System.Threading.Tasks; -using OmniSharp.Models.v2; +using OmniSharp.Models; using OmniSharp.Options; using OmniSharp.Roslyn.CSharp.Services; using TestUtility; using Xunit; using Xunit.Abstractions; +#nullable enable + namespace OmniSharp.Roslyn.CSharp.Tests { public class QuickInfoProviderFacts : AbstractSingleRequestHandlerTestFixture { - protected override string EndpointName => OmniSharpEndpoints.V2.QuickInfo; + protected override string EndpointName => OmniSharpEndpoints.QuickInfo; public QuickInfoProviderFacts(ITestOutputHelper output, SharedOmniSharpHostFixture sharedOmniSharpHostFixture) : base(output, sharedOmniSharpHostFixture) @@ -39,9 +43,7 @@ public void M(int i) 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); + AssertContents(response.Sections, "(parameter) int i", "Some content `C`"); } [Fact] @@ -52,13 +54,12 @@ public async Task OmitsNamespaceForNonRegularCSharpSyntax() var testFile = new TestFile("dummy.csx", source); var workspace = TestHelpers.CreateCsxWorkspace(testFile); - var controller = new QuickInfoProvider(workspace, new FormattingOptions()); + var controller = new QuickInfoProvider(workspace, new FormattingOptions(), null); 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); + AssertContents(response.Sections, "class Foo"); } + [Fact] public async Task TypesFromInlineAssemlbyReferenceContainDocumentation() { @@ -73,12 +74,10 @@ public async Task TypesFromInlineAssemlbyReferenceContainDocumentation() var position = testFile.Content.GetPointFromPosition(); var workspace = TestHelpers.CreateCsxWorkspace(testFile); - var controller = new QuickInfoProvider(workspace, new FormattingOptions()); + var controller = new QuickInfoProvider(workspace, new FormattingOptions(), null); 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); + AssertContents(response.Sections, "class ClassLibraryWithDocumentation.DocumentedClass", "This class performs an important function."); } [Fact] @@ -99,12 +98,8 @@ class Baz {}"; 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); + AssertContents(responseInNormalNamespace.Sections, "class Bar.Foo"); + AssertContents(responseInGlobalNamespace.Sections, "class Baz"); } [Fact] @@ -121,9 +116,7 @@ class Foo {} 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); + AssertContents(response.Sections, "class Bar.Foo"); } [Fact] @@ -142,9 +135,7 @@ class Xyz {} 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); + AssertContents(response.Sections, "class Bar.Foo.Xyz"); } [Fact] @@ -157,13 +148,11 @@ class Bar {} var testFile = new TestFile("dummy.csx", source); var workspace = TestHelpers.CreateCsxWorkspace(testFile); - var controller = new QuickInfoProvider(workspace, new FormattingOptions()); + var controller = new QuickInfoProvider(workspace, new FormattingOptions(), null); 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); + AssertContents(response.Sections, "class Foo.Bar"); } private static TestFile s_testFile = new TestFile("dummy.cs", @@ -208,140 +197,106 @@ 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); + AssertContents(response.Sections, "void Console.WriteLine(string value) (+ 18 overloads)"); } [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); + AssertContents(response.Sections, "void Foo.MyMethod(string name, Foo foo, Foo2 foo2)"); } [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); + AssertContents(response.Sections, "class System.String"); } [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); + AssertContents(response.Sections, "class Bar.Foo"); } [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); + AssertContents(response.Sections, "class Bar2.Foo2"); } [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); + AssertContents(response.Sections, "interface System.Collections.Generic.IDictionary", + new QuickInfoResponseSection { IsCSharpCode = true, Text = $"TKey is string" }, + new QuickInfoResponseSection { IsCSharpCode = true, Text = $"TValue is IEnumerable" }); } [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); + AssertContents(response.Sections, "(parameter) string name"); } [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); + AssertContents(response.Sections, "(parameter) Foo foo"); } [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); + AssertContents(response.Sections, "(parameter) Foo2 foo2"); } [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); + AssertContents(response.Sections, "(parameter) int index = 2"); } [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); + AssertContents(response.Sections, "(field) Foo2 Foo._someField"); } [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); + AssertContents(response.Sections, "(constant) int Foo.foo = 1"); } [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); + AssertContents(response.Sections, "Foo2 Foo.SomeProperty { get; }"); } [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); + AssertContents(response.Sections, "IDictionary> Foo.SomeDict { get; }"); } [Fact] @@ -357,11 +312,8 @@ class testissue } }"; 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); + AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)", + new QuickInfoResponseSection { IsCSharpCode = false, Text = "You may have some additional information about this class here." }); } [Fact] @@ -376,9 +328,7 @@ class testissue } }"; 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); + AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)", "Checks if object is tagged with the tag."); } [Fact] @@ -393,10 +343,8 @@ class testissue } }"; 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); + AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)", + new QuickInfoResponseSection { IsCSharpCode = false, Text = "Returns:\n\n Returns true if object is tagged with tag." }); } [Fact] @@ -411,11 +359,7 @@ class testissue } }"; 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); + AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)"); } [Fact] @@ -431,10 +375,8 @@ class testissue } }"; 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); + AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)", + new QuickInfoResponseSection { IsCSharpCode = false, Text = "Exceptions:\n\n A\n\n B" }); } [Fact] @@ -450,9 +392,7 @@ class testissue } }"; var response = await GetTypeLookUpResponse(content); - Assert.Equal("bool testissue.Compare(int gameObject, string tagName)", response.Description); - Assert.Null(response.Summary); - Assert.Empty(response.RemainingSections); + AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)"); } [Fact] @@ -472,9 +412,8 @@ public class TestClass } }"; 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); + AssertContents(response.Sections, "T[] TestClass.mkArray(int n, List list)", + "Creates a new array of arbitrary type `T` and adds the elements of incoming list to it if possible"); } [Fact] @@ -494,9 +433,8 @@ public class TestClass } }"; 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); + AssertContents(response.Sections, "T in TestClass.mkArray", + "The element type of the array"); } [Fact] @@ -516,9 +454,7 @@ public static T[] mkArray(int n, List list) } }"; var response = await GetTypeLookUpResponse(content); - Assert.Null(response.Description); - Assert.Null(response.Summary); - Assert.Null(response.RemainingSections); + Assert.Empty(response.Sections); } [Fact] @@ -537,10 +473,8 @@ 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); + AssertContents(response.Sections, "string Employee.Name { }", "The Name property represents the employee's name.", + new QuickInfoResponseSection { IsCSharpCode = false, Text = "Value:\n\n The Name property gets/sets the value of the string field, _name." }); } [Fact] @@ -555,10 +489,7 @@ public class TestClass } }"; 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); + AssertContents(response.Sections, "void TestClass.DoWork(int Int1)", "DoWork is a method in the TestClass class. `System.Console.WriteLine(string)` for information about output statements."); } [Fact] @@ -575,9 +506,7 @@ public class TestClass } }"; 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); + AssertContents(response.Sections, "T[] TestClass.mkArray(int n)", "Creates a new array of arbitrary type `T`"); } [Fact] @@ -603,9 +532,7 @@ public class TestClass } }"; var response = await GetTypeLookUpResponse(content); - Assert.Equal("int TestClass.GetZero()", response.Description); - Assert.Null(response.Summary); - Assert.Empty(response.RemainingSections); + AssertContents(response.Sections, "int TestClass.GetZero()"); } [Fact] @@ -623,9 +550,7 @@ public class TestClass } "; 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); + AssertContents(response.Sections, "void TestClass.DoWork(int Int1)", "DoWork is a method in the TestClass class.\n\n\n\nHere's how you could make a second paragraph in a description."); } [Fact] @@ -646,9 +571,7 @@ 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); + AssertContents(response.Sections, "void TestClass.DoWork(int Int1)", "DoWork is a method in the TestClass class. `TestClass.Main()`"); } [Fact] @@ -665,9 +588,7 @@ class testissue } }"; 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); + AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)", "Checks if object is tagged with the tag."); } [Fact] @@ -688,13 +609,10 @@ class testissue } }"; 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[] { + AssertContents(response.Sections, "T[] testissue.Compare(int gameObject)", "Checks if object is tagged with the tag.", 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); + new QuickInfoResponseSection { IsCSharpCode = false, Text = "Exceptions:\n\n `System.Exception`" }); } [Fact] @@ -709,9 +627,7 @@ public class TestClass } }"; 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); + AssertContents(response.Sections, "void TestClass.DoWork(int Int1)", "DoWork is a method in the TestClass class."); } [Fact] @@ -727,9 +643,7 @@ 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); + AssertContents(response.Sections, "(parameter) int gameObject", "The game object."); } [Fact] @@ -745,9 +659,48 @@ 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); + AssertContents(response.Sections, "(parameter) string tagName", "Name of the tag."); + } + + [Fact] + public async Task AnonymousTypeSubstitution() + { + string content = @" +class C +{ + void M1(T t) {} + void M2() + { + var a = new { X = 1, Y = 2 }; + M$$1(a); + } +}"; + var response = await GetTypeLookUpResponse(content); + AssertContents(response.Sections, "void C.M1<'a>('a t)", + new QuickInfoResponseSection { IsCSharpCode = false, Text = "Anonymous Types:" }, + new QuickInfoResponseSection { IsCSharpCode = true, Text = $" 'a is new {{ int X, int Y }}" }); + } + + [Fact] + public async Task InheritDoc() + { + string content = @" +class Program +{ + /// Hello World + public static void A() { } + + /// + public static void B() { } + + public static void Main() + { + A(); + B$$(); + } +}"; + var response = await GetTypeLookUpResponse(content); + AssertContents(response.Sections, "void Program.B()", "Hello World"); } private async Task GetTypeLookUpResponse(string content) @@ -769,5 +722,24 @@ private async Task GetTypeLookUpResponse(int line, int column return await requestHandler.Handle(request); } + + private void AssertContents(ImmutableArray actual, string description, params QuickInfoResponseSection[] otherSections) + { + AssertContents(actual, description, summary: null, otherSections); + } + + private void AssertContents(ImmutableArray actual, string description, string? summary = null, params QuickInfoResponseSection[] otherSections) + { + var expected = new List(); + expected.Add(new QuickInfoResponseSection { IsCSharpCode = true, Text = description }); + if (summary is object) + { + expected.Add(new QuickInfoResponseSection { IsCSharpCode = false, Text = summary }); + } + + expected.AddRange(otherSections); + + Assert.Equal(expected, actual); + } } } From 8d0dbc4c9a9aa7a4b9fc37999902b7482c896ec5 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Fri, 24 Jul 2020 18:20:01 -0700 Subject: [PATCH 4/5] Simplify the API by sending a pre-markdowned string, rather than sending parts that the client needs to reassemble themselves. --- .../Models/v1/QuickInfoResponse.cs | 18 +- .../Services/QuickInfoProvider.cs | 158 +++++++++--------- .../QuickInfoProviderFacts.cs | 122 +++++--------- 3 files changed, 131 insertions(+), 167 deletions(-) diff --git a/src/OmniSharp.Abstractions/Models/v1/QuickInfoResponse.cs b/src/OmniSharp.Abstractions/Models/v1/QuickInfoResponse.cs index 4bdb73988e..c356af97dd 100644 --- a/src/OmniSharp.Abstractions/Models/v1/QuickInfoResponse.cs +++ b/src/OmniSharp.Abstractions/Models/v1/QuickInfoResponse.cs @@ -6,22 +6,8 @@ namespace OmniSharp.Models public class QuickInfoResponse { /// - /// Other relevant information to the symbol under the cursor. + /// QuickInfo for the given position, rendered as markdown. /// - public ImmutableArray Sections { 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}"" }}"; - } + public string Markdown { get; set; } = string.Empty; } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs b/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs index 6e7764b66e..9d828220cd 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs @@ -1,5 +1,4 @@ -using System.Collections.Immutable; -using System.Composition; +using System.Composition; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -56,7 +55,7 @@ public QuickInfoProvider(OmniSharpWorkspace workspace, FormattingOptions formatt public async Task Handle(QuickInfoRequest request) { var document = _workspace.GetDocument(request.FileName); - var response = new QuickInfoResponse() { Sections = ImmutableArray.Empty }; + var response = new QuickInfoResponse(); if (document is null) { @@ -80,97 +79,108 @@ public async Task Handle(QuickInfoRequest request) return response; } - var sectionBuilder = ImmutableArray.CreateBuilder(quickInfo.Sections.Length); - var stringBuilder = new StringBuilder(); + var finalTextBuilder = new StringBuilder(); + var sectionTextBuilder = new StringBuilder(); + + var description = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description); + if (description is object) + { + appendSectionAsCsharp(description, finalTextBuilder, _formattingOptions, includeSpaceAtStart: false); + } + + var summary = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments); + if (summary is object) + { + buildSectionAsMarkdown(summary, sectionTextBuilder, _formattingOptions, out _); + appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions); + } - bool foundDescription = false; foreach (var section in quickInfo.Sections) { switch (section.Kind) { case QuickInfoSectionKinds.Description: - sectionBuilder.Insert(0, new QuickInfoResponseSection { IsCSharpCode = true, Text = section.Text }); - foundDescription = true; - break; + case QuickInfoSectionKinds.DocumentationComments: + continue; case QuickInfoSectionKinds.TypeParameters: - stringBuilder.Clear(); - foreach (var text in section.TaggedParts) - { - switch (text.Tag) - { - case TextTags.LineBreak: - appendIfNeeded(); - stringBuilder.Clear(); - continue; - - default: - stringBuilder.Append(text.Text); - break; - } - } - - appendIfNeeded(); + appendSectionAsCsharp(section, finalTextBuilder, _formattingOptions); break; - void appendIfNeeded() - { - var currentString = stringBuilder.ToString().Trim(); - if (currentString == string.Empty) - { - return; - } - - sectionBuilder.Add(new QuickInfoResponseSection { IsCSharpCode = true, Text = currentString }); - } - case QuickInfoSectionKinds.AnonymousTypes: - sectionBuilder.Add(new QuickInfoResponseSection { IsCSharpCode = false, Text = getMarkdown(section.TaggedParts, stringBuilder, _formattingOptions, out int remainingIndex, untilLineBreak: true) }); - - if (remainingIndex < section.TaggedParts.Length) - { - sectionBuilder.Add(new QuickInfoResponseSection - { - IsCSharpCode = true, - Text = string.Concat(section.TaggedParts.Skip(remainingIndex + 1).Select(s => s.Text)) - }); - } + // The first line is "Anonymous Types:" + buildSectionAsMarkdown(section, sectionTextBuilder, _formattingOptions, out int lastIndex, untilLineBreak: true); + appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions); - break; - - case QuickInfoSectionKinds.DocumentationComments: - sectionBuilder.Insert(foundDescription ? 1 : 0, - new QuickInfoResponseSection { IsCSharpCode = false, Text = getMarkdown(section.TaggedParts, stringBuilder, _formattingOptions, out _) }); + // Then we want all anonymous types to be C# highlighted + appendSectionAsCsharp(section, finalTextBuilder, _formattingOptions, lastIndex + 1); break; case NullabilityAnalysis: - var nullabilityText = getMarkdown(section.TaggedParts, stringBuilder, _formattingOptions, out _); - if (!nullabilityText.Contains(_formattingOptions.NewLine)) - { - // Italicize the text for emphasis - nullabilityText = $"_{nullabilityText}_"; - } - sectionBuilder.Add(new QuickInfoResponseSection { IsCSharpCode = false, Text = nullabilityText }); + // Italicize the nullable analysis for emphasis. + buildSectionAsMarkdown(section, sectionTextBuilder, _formattingOptions, out _); + appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions, italicize: true); break; default: - sectionBuilder.Add(new QuickInfoResponseSection { IsCSharpCode = false, Text = getMarkdown(section.TaggedParts, stringBuilder, _formattingOptions, out _) }); + buildSectionAsMarkdown(section, sectionTextBuilder, _formattingOptions, out _); + appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions); break; } } - response.Sections = sectionBuilder.ToImmutable(); + response.Markdown = finalTextBuilder.ToString().Trim(); return response; - static string getMarkdown(ImmutableArray taggedTexts, StringBuilder stringBuilder, FormattingOptions formattingOptions, out int lastIndex, bool untilLineBreak = false) + static void appendBuiltSection(StringBuilder finalTextBuilder, StringBuilder stringBuilder, FormattingOptions formattingOptions, bool italicize = false) { - bool isInCodeBlock = false; + // Two newlines to trigger a markdown new paragraph + finalTextBuilder.Append(formattingOptions.NewLine); + finalTextBuilder.Append(formattingOptions.NewLine); + if (italicize) + { + finalTextBuilder.Append("_"); + } + finalTextBuilder.Append(stringBuilder); + if (italicize) + { + finalTextBuilder.Append("_"); + } stringBuilder.Clear(); + } + + static void appendSectionAsCsharp(QuickInfoSection section, StringBuilder builder, FormattingOptions formattingOptions, int startingIndex = 0, bool includeSpaceAtStart = true) + { + if (includeSpaceAtStart) + { + builder.Append(formattingOptions.NewLine); + } + builder.Append("```csharp"); + builder.Append(formattingOptions.NewLine); + for (int i = startingIndex; i < section.TaggedParts.Length; i++) + { + TaggedText part = section.TaggedParts[i]; + if (part.Tag == TextTags.LineBreak && i + 1 != section.TaggedParts.Length) + { + builder.Append(formattingOptions.NewLine); + } + else + { + builder.Append(part.Text); + } + } + builder.Append(formattingOptions.NewLine); + builder.Append("```"); + } + + static void buildSectionAsMarkdown(QuickInfoSection section, StringBuilder stringBuilder, FormattingOptions formattingOptions, out int lastIndex, bool untilLineBreak = false) + { + bool isInCodeBlock = false; lastIndex = 0; - for (int i = 0; i < taggedTexts.Length; i++) + for (int i = 0; i < section.TaggedParts.Length; i++) { - var current = taggedTexts[i]; + var current = section.TaggedParts[i]; lastIndex = i; switch (current.Tag) @@ -185,7 +195,7 @@ static string getMarkdown(ImmutableArray taggedTexts, StringBuilder break; case TextTags.Space when isInCodeBlock: - if (nextIsTag(TextTags.Text, i)) + if (nextIsTag(i, TextTags.Text)) { endBlock(); } @@ -207,14 +217,12 @@ static string getMarkdown(ImmutableArray taggedTexts, StringBuilder addNewline(); break; - case TextTags.LineBreak when untilLineBreak - && stringBuilder.ToString().Trim() is var currentString - && currentString != string.Empty: - addNewline(); - return currentString; + case TextTags.LineBreak when untilLineBreak && stringBuilder.Length != 0: + // The section will end and another newline will be appended, no need to add yet another newline. + return; case TextTags.LineBreak: - if (!nextIsTag(ContainerStart, i) && !nextIsTag(ContainerEnd, i)) + if (stringBuilder.Length != 0 && !nextIsTag(i, ContainerStart, ContainerEnd) && i + 1 != section.TaggedParts.Length) { addNewline(); } @@ -236,7 +244,7 @@ static string getMarkdown(ImmutableArray taggedTexts, StringBuilder endBlock(); } - return stringBuilder.ToString().Trim(); + return; void addNewline() { @@ -256,10 +264,10 @@ void endBlock() isInCodeBlock = false; } - bool nextIsTag(string tag, int i) + bool nextIsTag(int i, params string[] tags) { int nextI = i + 1; - return nextI < taggedTexts.Length && taggedTexts[nextI].Tag == tag; + return nextI < section.TaggedParts.Length && tags.Contains(section.TaggedParts[nextI].Tag); } } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs index 5e34655656..42cef77a57 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs @@ -43,7 +43,7 @@ public void M(int i) var request = new QuickInfoRequest { FileName = testFile.FileName, Line = 7, Column = 17 }; var response = await requestHandler.Handle(request); - AssertContents(response.Sections, "(parameter) int i", "Some content `C`"); + Assert.Equal("```csharp\n(parameter) int i\n```\n\nSome content `C`", response.Markdown); } [Fact] @@ -57,7 +57,7 @@ public async Task OmitsNamespaceForNonRegularCSharpSyntax() var controller = new QuickInfoProvider(workspace, new FormattingOptions(), null); var response = await controller.Handle(new QuickInfoRequest { FileName = testFile.FileName, Line = 0, Column = 7 }); - AssertContents(response.Sections, "class Foo"); + Assert.Equal("```csharp\nclass Foo\n```", response.Markdown); } [Fact] @@ -77,7 +77,7 @@ public async Task TypesFromInlineAssemlbyReferenceContainDocumentation() var controller = new QuickInfoProvider(workspace, new FormattingOptions(), null); var response = await controller.Handle(new QuickInfoRequest { FileName = testFile.FileName, Line = position.Line, Column = position.Offset }); - AssertContents(response.Sections, "class ClassLibraryWithDocumentation.DocumentedClass", "This class performs an important function."); + Assert.Equal("```csharp\nclass ClassLibraryWithDocumentation.DocumentedClass\n```\n\nThis class performs an important function.", response.Markdown); } [Fact] @@ -98,8 +98,8 @@ class Baz {}"; var requestInGlobalNamespace = new QuickInfoRequest { FileName = testFile.FileName, Line = 3, Column = 19 }; var responseInGlobalNamespace = await requestHandler.Handle(requestInGlobalNamespace); - AssertContents(responseInNormalNamespace.Sections, "class Bar.Foo"); - AssertContents(responseInGlobalNamespace.Sections, "class Baz"); + Assert.Equal("```csharp\nclass Bar.Foo\n```", responseInNormalNamespace.Markdown); + Assert.Equal("```csharp\nclass Baz\n```", responseInGlobalNamespace.Markdown); } [Fact] @@ -116,7 +116,7 @@ class Foo {} var request = new QuickInfoRequest { FileName = testFile.FileName, Line = 1, Column = 19 }; var response = await requestHandler.Handle(request); - AssertContents(response.Sections, "class Bar.Foo"); + Assert.Equal("```csharp\nclass Bar.Foo\n```", response.Markdown); } [Fact] @@ -135,7 +135,7 @@ class Xyz {} var request = new QuickInfoRequest { FileName = testFile.FileName, Line = 2, Column = 27 }; var response = await requestHandler.Handle(request); - AssertContents(response.Sections, "class Bar.Foo.Xyz"); + Assert.Equal("```csharp\nclass Bar.Foo.Xyz\n```", response.Markdown); } [Fact] @@ -152,7 +152,7 @@ class Bar {} var request = new QuickInfoRequest { FileName = testFile.FileName, Line = 1, Column = 23 }; var response = await controller.Handle(request); - AssertContents(response.Sections, "class Foo.Bar"); + Assert.Equal("```csharp\nclass Foo.Bar\n```", response.Markdown); } private static TestFile s_testFile = new TestFile("dummy.cs", @@ -197,86 +197,84 @@ public async Task DisplayFormatForMethodSymbol_Invocation() { var response = await GetTypeLookUpResponse(line: 6, column: 35); - AssertContents(response.Sections, "void Console.WriteLine(string value) (+ 18 overloads)"); + Assert.Equal("```csharp\nvoid Console.WriteLine(string value) (+ 18 overloads)\n```", response.Markdown); } [Fact] public async Task DisplayFormatForMethodSymbol_Declaration() { var response = await GetTypeLookUpResponse(line: 9, column: 35); - AssertContents(response.Sections, "void Foo.MyMethod(string name, Foo foo, Foo2 foo2)"); + Assert.Equal("```csharp\nvoid Foo.MyMethod(string name, Foo foo, Foo2 foo2)\n```", response.Markdown); } [Fact] public async Task DisplayFormatFor_TypeSymbol_Primitive() { var response = await GetTypeLookUpResponse(line: 9, column: 46); - AssertContents(response.Sections, "class System.String"); + Assert.Equal("```csharp\nclass System.String\n```", response.Markdown); } [Fact] public async Task DisplayFormatFor_TypeSymbol_ComplexType_SameNamespace() { var response = await GetTypeLookUpResponse(line: 9, column: 56); - AssertContents(response.Sections, "class Bar.Foo"); + Assert.Equal("```csharp\nclass Bar.Foo\n```", response.Markdown); } [Fact] public async Task DisplayFormatFor_TypeSymbol_ComplexType_DifferentNamespace() { var response = await GetTypeLookUpResponse(line: 9, column: 67); - AssertContents(response.Sections, "class Bar2.Foo2"); + Assert.Equal("```csharp\nclass Bar2.Foo2\n```", response.Markdown); } [Fact] public async Task DisplayFormatFor_TypeSymbol_WithGenerics() { var response = await GetTypeLookUpResponse(line: 15, column: 36); - AssertContents(response.Sections, "interface System.Collections.Generic.IDictionary", - new QuickInfoResponseSection { IsCSharpCode = true, Text = $"TKey is string" }, - new QuickInfoResponseSection { IsCSharpCode = true, Text = $"TValue is IEnumerable" }); + Assert.Equal("```csharp\ninterface System.Collections.Generic.IDictionary\n```\n```csharp\n\nTKey is string\nTValue is IEnumerable\n```", response.Markdown); } [Fact] public async Task DisplayFormatForParameterSymbol_Name_Primitive() { var response = await GetTypeLookUpResponse(line: 9, column: 51); - AssertContents(response.Sections, "(parameter) string name"); + Assert.Equal("```csharp\n(parameter) string name\n```", response.Markdown); } [Fact] public async Task DisplayFormatFor_ParameterSymbol_ComplexType_SameNamespace() { var response = await GetTypeLookUpResponse(line: 9, column: 60); - AssertContents(response.Sections, "(parameter) Foo foo"); + Assert.Equal("```csharp\n(parameter) Foo foo\n```", response.Markdown); } [Fact] public async Task DisplayFormatFor_ParameterSymbol_Name_ComplexType_DifferentNamespace() { var response = await GetTypeLookUpResponse(line: 9, column: 71); - AssertContents(response.Sections, "(parameter) Foo2 foo2"); + Assert.Equal("```csharp\n(parameter) Foo2 foo2\n```", response.Markdown); } [Fact] public async Task DisplayFormatFor_ParameterSymbol_Name_WithDefaultValue() { var response = await GetTypeLookUpResponse(line: 17, column: 48); - AssertContents(response.Sections, "(parameter) int index = 2"); + Assert.Equal("```csharp\n(parameter) int index = 2\n```", response.Markdown); } [Fact] public async Task DisplayFormatFor_FieldSymbol() { var response = await GetTypeLookUpResponse(line: 11, column: 38); - AssertContents(response.Sections, "(field) Foo2 Foo._someField"); + Assert.Equal("```csharp\n(field) Foo2 Foo._someField\n```", response.Markdown); } [Fact] public async Task DisplayFormatFor_FieldSymbol_WithConstantValue() { var response = await GetTypeLookUpResponse(line: 19, column: 41); - AssertContents(response.Sections, "(constant) int Foo.foo = 1"); + Assert.Equal("```csharp\n(constant) int Foo.foo = 1\n```", response.Markdown); } [Fact] @@ -289,14 +287,14 @@ public async Task DisplayFormatFor_EnumValue() public async Task DisplayFormatFor_PropertySymbol() { var response = await GetTypeLookUpResponse(line: 13, column: 38); - AssertContents(response.Sections, "Foo2 Foo.SomeProperty { get; }"); + Assert.Equal("```csharp\nFoo2 Foo.SomeProperty { get; }\n```", response.Markdown); } [Fact] public async Task DisplayFormatFor_PropertySymbol_WithGenerics() { var response = await GetTypeLookUpResponse(line: 15, column: 70); - AssertContents(response.Sections, "IDictionary> Foo.SomeDict { get; }"); + Assert.Equal("```csharp\nIDictionary> Foo.SomeDict { get; }\n```", response.Markdown); } [Fact] @@ -312,8 +310,7 @@ class testissue } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)", - new QuickInfoResponseSection { IsCSharpCode = false, Text = "You may have some additional information about this class here." }); + Assert.Equal("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```\n\nYou may have some additional information about this class here.", response.Markdown); } [Fact] @@ -328,7 +325,7 @@ class testissue } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)", "Checks if object is tagged with the tag."); + Assert.Equal("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```\n\nChecks if object is tagged with the tag.", response.Markdown); } [Fact] @@ -343,8 +340,7 @@ class testissue } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)", - new QuickInfoResponseSection { IsCSharpCode = false, Text = "Returns:\n\n Returns true if object is tagged with tag." }); + Assert.Equal("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```\n\nReturns:\n\n Returns true if object is tagged with tag.", response.Markdown); } [Fact] @@ -359,7 +355,7 @@ class testissue } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)"); + Assert.Equal("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```", response.Markdown); } [Fact] @@ -375,8 +371,7 @@ class testissue } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)", - new QuickInfoResponseSection { IsCSharpCode = false, Text = "Exceptions:\n\n A\n\n B" }); + Assert.Equal("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```\n\nExceptions:\n\n A\n\n B", response.Markdown); } [Fact] @@ -392,7 +387,7 @@ class testissue } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)"); + Assert.Equal("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```", response.Markdown); } [Fact] @@ -412,8 +407,7 @@ public class TestClass } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "T[] TestClass.mkArray(int n, List list)", - "Creates a new array of arbitrary type `T` and adds the elements of incoming list to it if possible"); + Assert.Equal("```csharp\nT[] TestClass.mkArray(int n, List list)\n```\n\nCreates a new array of arbitrary type `T` and adds the elements of incoming list to it if possible", response.Markdown); } [Fact] @@ -433,8 +427,7 @@ public class TestClass } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "T in TestClass.mkArray", - "The element type of the array"); + Assert.Equal("```csharp\nT in TestClass.mkArray\n```\n\nThe element type of the array", response.Markdown); } [Fact] @@ -454,7 +447,7 @@ public static T[] mkArray(int n, List list) } }"; var response = await GetTypeLookUpResponse(content); - Assert.Empty(response.Sections); + Assert.Empty(response.Markdown); } [Fact] @@ -473,8 +466,7 @@ public string Na$$me } "; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "string Employee.Name { }", "The Name property represents the employee's name.", - new QuickInfoResponseSection { IsCSharpCode = false, Text = "Value:\n\n The Name property gets/sets the value of the string field, _name." }); + Assert.Equal("```csharp\nstring Employee.Name { }\n```\n\nThe Name property represents the employee's name.\n\nValue:\n\n The Name property gets/sets the value of the string field, _name.", response.Markdown); } [Fact] @@ -489,7 +481,7 @@ public class TestClass } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "void TestClass.DoWork(int Int1)", "DoWork is a method in the TestClass class. `System.Console.WriteLine(string)` for information about output statements."); + Assert.Equal("```csharp\nvoid TestClass.DoWork(int Int1)\n```\n\nDoWork is a method in the TestClass class. `System.Console.WriteLine(string)` for information about output statements.", response.Markdown); } [Fact] @@ -506,7 +498,7 @@ public class TestClass } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "T[] TestClass.mkArray(int n)", "Creates a new array of arbitrary type `T`"); + Assert.Equal("```csharp\nT[] TestClass.mkArray(int n)\n```\n\nCreates a new array of arbitrary type `T`", response.Markdown); } [Fact] @@ -532,7 +524,7 @@ public class TestClass } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "int TestClass.GetZero()"); + Assert.Equal("```csharp\nint TestClass.GetZero()\n```", response.Markdown); } [Fact] @@ -550,7 +542,7 @@ public class TestClass } "; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "void TestClass.DoWork(int Int1)", "DoWork is a method in the TestClass class.\n\n\n\nHere's how you could make a second paragraph in a description."); + Assert.Equal("```csharp\nvoid TestClass.DoWork(int Int1)\n```\n\nDoWork is a method in the TestClass class.\n\n\n\nHere's how you could make a second paragraph in a description.", response.Markdown); } [Fact] @@ -571,7 +563,7 @@ static void Main() } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "void TestClass.DoWork(int Int1)", "DoWork is a method in the TestClass class. `TestClass.Main()`"); + Assert.Equal("```csharp\nvoid TestClass.DoWork(int Int1)\n```\n\nDoWork is a method in the TestClass class. `TestClass.Main()`", response.Markdown); } [Fact] @@ -588,7 +580,7 @@ class testissue } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "bool testissue.Compare(int gameObject, string tagName)", "Checks if object is tagged with the tag."); + Assert.Equal("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```\n\nChecks if object is tagged with the tag.", response.Markdown); } [Fact] @@ -609,10 +601,9 @@ class testissue } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "T[] testissue.Compare(int gameObject)", "Checks if object is tagged with the tag.", - 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`" }); + Assert.Equal( + "```csharp\nT[] testissue.Compare(int gameObject)\n```\n\nChecks if object is tagged with the tag.\n\nYou may have some additional information about this class here.\n\nReturns:\n\n Returns an array of type `T`.\n\n\n\nExceptions:\n\n `System.Exception`", + response.Markdown); } [Fact] @@ -627,7 +618,7 @@ public class TestClass } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "void TestClass.DoWork(int Int1)", "DoWork is a method in the TestClass class."); + Assert.Equal("```csharp\nvoid TestClass.DoWork(int Int1)\n```\n\nDoWork is a method in the TestClass class.", response.Markdown); } [Fact] @@ -643,7 +634,7 @@ public static bool Compare(int gam$$eObject, string tagName) } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "(parameter) int gameObject", "The game object."); + Assert.Equal("```csharp\n(parameter) int gameObject\n```\n\nThe game object.", response.Markdown); } [Fact] @@ -659,7 +650,7 @@ public static bool Compare(int gameObject, string tag$$Name) } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "(parameter) string tagName", "Name of the tag."); + Assert.Equal("```csharp\n(parameter) string tagName\n```\n\nName of the tag.", response.Markdown); } [Fact] @@ -676,9 +667,7 @@ void M2() } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "void C.M1<'a>('a t)", - new QuickInfoResponseSection { IsCSharpCode = false, Text = "Anonymous Types:" }, - new QuickInfoResponseSection { IsCSharpCode = true, Text = $" 'a is new {{ int X, int Y }}" }); + Assert.Equal("```csharp\nvoid C.M1<'a>('a t)\n```\n\nAnonymous Types:\n```csharp\n 'a is new { int X, int Y }\n```", response.Markdown); } [Fact] @@ -700,7 +689,7 @@ public static void Main() } }"; var response = await GetTypeLookUpResponse(content); - AssertContents(response.Sections, "void Program.B()", "Hello World"); + Assert.Equal("```csharp\nvoid Program.B()\n```\n\nHello World", response.Markdown); } private async Task GetTypeLookUpResponse(string content) @@ -722,24 +711,5 @@ private async Task GetTypeLookUpResponse(int line, int column return await requestHandler.Handle(request); } - - private void AssertContents(ImmutableArray actual, string description, params QuickInfoResponseSection[] otherSections) - { - AssertContents(actual, description, summary: null, otherSections); - } - - private void AssertContents(ImmutableArray actual, string description, string? summary = null, params QuickInfoResponseSection[] otherSections) - { - var expected = new List(); - expected.Add(new QuickInfoResponseSection { IsCSharpCode = true, Text = description }); - if (summary is object) - { - expected.Add(new QuickInfoResponseSection { IsCSharpCode = false, Text = summary }); - } - - expected.AddRange(otherSections); - - Assert.Equal(expected, actual); - } } } From 46828843c560b42d7c18616c25da63a180807b39 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Fri, 24 Jul 2020 18:31:18 -0700 Subject: [PATCH 5/5] Update dotnet tests for 3.1.302. --- tests/OmniSharp.Tests/DotNetCliServiceFacts.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/OmniSharp.Tests/DotNetCliServiceFacts.cs b/tests/OmniSharp.Tests/DotNetCliServiceFacts.cs index 0793d67891..f8fd369545 100644 --- a/tests/OmniSharp.Tests/DotNetCliServiceFacts.cs +++ b/tests/OmniSharp.Tests/DotNetCliServiceFacts.cs @@ -23,7 +23,7 @@ public void GetVersion() Assert.Equal(3, version.Major); Assert.Equal(1, version.Minor); - Assert.Equal(201, version.Patch); + Assert.Equal(302, version.Patch); Assert.Equal("", version.Release); } } @@ -39,7 +39,7 @@ public void GetInfo() Assert.Equal(3, info.Version.Major); Assert.Equal(1, info.Version.Minor); - Assert.Equal(201, info.Version.Patch); + Assert.Equal(302, info.Version.Patch); Assert.Equal("", info.Version.Release); } }