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/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/v1/QuickInfoResponse.cs b/src/OmniSharp.Abstractions/Models/v1/QuickInfoResponse.cs new file mode 100644 index 0000000000..c356af97dd --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v1/QuickInfoResponse.cs @@ -0,0 +1,13 @@ +#nullable enable +using System.Collections.Immutable; + +namespace OmniSharp.Models +{ + public class QuickInfoResponse + { + /// + /// QuickInfo for the given position, rendered as markdown. + /// + public string Markdown { get; set; } = string.Empty; + } +} diff --git a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs index 58f82bb2f8..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 { @@ -65,6 +66,7 @@ public static class V2 public const string CodeStructure = "/v2/codestructure"; public const string Highlight = "/v2/highlight"; + } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs b/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs new file mode 100644 index 0000000000..9d828220cd --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs @@ -0,0 +1,275 @@ +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 Microsoft.Extensions.Logging; +using OmniSharp.Mef; +using OmniSharp.Models; +using OmniSharp.Options; + +#nullable enable + +namespace OmniSharp.Roslyn.CSharp.Services +{ + [OmniSharpHandler(OmniSharpEndpoints.QuickInfo, LanguageNames.CSharp)] + public class QuickInfoProvider : IRequestHandler + { + // 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 + /// 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); + /// + /// 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, 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(); + + if (document is null) + { + return response; + } + + var quickInfoService = QuickInfoService.GetService(document); + if (quickInfoService is null) + { + _logger?.LogWarning($"QuickInfo service was null for {document.FilePath}"); + 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) + { + _logger?.LogTrace($"No QuickInfo found for {document.FilePath}:{request.Line},{request.Column}"); + return response; + } + + 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); + } + + foreach (var section in quickInfo.Sections) + { + switch (section.Kind) + { + case QuickInfoSectionKinds.Description: + case QuickInfoSectionKinds.DocumentationComments: + continue; + + case QuickInfoSectionKinds.TypeParameters: + appendSectionAsCsharp(section, finalTextBuilder, _formattingOptions); + break; + + case QuickInfoSectionKinds.AnonymousTypes: + // The first line is "Anonymous Types:" + buildSectionAsMarkdown(section, sectionTextBuilder, _formattingOptions, out int lastIndex, untilLineBreak: true); + appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions); + + // Then we want all anonymous types to be C# highlighted + appendSectionAsCsharp(section, finalTextBuilder, _formattingOptions, lastIndex + 1); + break; + + case NullabilityAnalysis: + // Italicize the nullable analysis for emphasis. + buildSectionAsMarkdown(section, sectionTextBuilder, _formattingOptions, out _); + appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions, italicize: true); + break; + + default: + buildSectionAsMarkdown(section, sectionTextBuilder, _formattingOptions, out _); + appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions); + break; + } + } + + response.Markdown = finalTextBuilder.ToString().Trim(); + + return response; + + static void appendBuiltSection(StringBuilder finalTextBuilder, StringBuilder stringBuilder, FormattingOptions formattingOptions, bool italicize = 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 < section.TaggedParts.Length; i++) + { + var current = section.TaggedParts[i]; + lastIndex = i; + + switch (current.Tag) + { + case TextTags.Text when !isInCodeBlock: + stringBuilder.Append(current.Text); + break; + + case TextTags.Text: + endBlock(); + stringBuilder.Append(current.Text); + break; + + case TextTags.Space when isInCodeBlock: + if (nextIsTag(i, TextTags.Text)) + { + endBlock(); + } + + stringBuilder.Append(current.Text); + break; + + case TextTags.Space: + case TextTags.Punctuation: + stringBuilder.Append(current.Text); + break; + + case ContainerStart: + addNewline(); + stringBuilder.Append(current.Text); + break; + + case ContainerEnd: + addNewline(); + break; + + 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 (stringBuilder.Length != 0 && !nextIsTag(i, ContainerStart, ContainerEnd) && i + 1 != section.TaggedParts.Length) + { + addNewline(); + } + break; + + default: + if (!isInCodeBlock) + { + isInCodeBlock = true; + stringBuilder.Append('`'); + } + stringBuilder.Append(current.Text); + break; + } + } + + if (isInCodeBlock) + { + endBlock(); + } + + return; + + void addNewline() + { + if (isInCodeBlock) + { + endBlock(); + } + + // Markdown needs 2 linebreaks to make a new paragraph + stringBuilder.Append(formattingOptions.NewLine); + stringBuilder.Append(formattingOptions.NewLine); + } + + void endBlock() + { + stringBuilder.Append('`'); + isInCodeBlock = false; + } + + bool nextIsTag(int i, params string[] tags) + { + int nextI = i + 1; + return nextI < section.TaggedParts.Length && tags.Contains(section.TaggedParts[nextI].Tag); + } + } + } + } +} 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); } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs new file mode 100644 index 0000000000..42cef77a57 --- /dev/null +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/QuickInfoProviderFacts.cs @@ -0,0 +1,715 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Threading.Tasks; +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.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("```csharp\n(parameter) int i\n```\n\nSome content `C`", response.Markdown); + } + + [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(), null); + var response = await controller.Handle(new QuickInfoRequest { FileName = testFile.FileName, Line = 0, Column = 7 }); + + Assert.Equal("```csharp\nclass Foo\n```", response.Markdown); + } + + [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(), null); + var response = await controller.Handle(new QuickInfoRequest { FileName = testFile.FileName, Line = position.Line, Column = position.Offset }); + + Assert.Equal("```csharp\nclass ClassLibraryWithDocumentation.DocumentedClass\n```\n\nThis class performs an important function.", response.Markdown); + } + + [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("```csharp\nclass Bar.Foo\n```", responseInNormalNamespace.Markdown); + Assert.Equal("```csharp\nclass Baz\n```", responseInGlobalNamespace.Markdown); + } + + [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("```csharp\nclass Bar.Foo\n```", response.Markdown); + } + + [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("```csharp\nclass Bar.Foo.Xyz\n```", response.Markdown); + } + + [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(), null); + var request = new QuickInfoRequest { FileName = testFile.FileName, Line = 1, Column = 23 }; + var response = await controller.Handle(request); + + Assert.Equal("```csharp\nclass Foo.Bar\n```", response.Markdown); + } + + 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("```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); + 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); + 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); + 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); + Assert.Equal("```csharp\nclass Bar2.Foo2\n```", response.Markdown); + } + + [Fact] + public async Task DisplayFormatFor_TypeSymbol_WithGenerics() + { + var response = await GetTypeLookUpResponse(line: 15, column: 36); + 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); + 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); + 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); + 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); + 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); + 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); + Assert.Equal("```csharp\n(constant) int Foo.foo = 1\n```", response.Markdown); + } + + [Fact] + public async Task DisplayFormatFor_EnumValue() + { + var response = await GetTypeLookUpResponse(line: 31, column: 23); + } + + [Fact] + public async Task DisplayFormatFor_PropertySymbol() + { + var response = await GetTypeLookUpResponse(line: 13, column: 38); + 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); + Assert.Equal("```csharp\nIDictionary> Foo.SomeDict { get; }\n```", response.Markdown); + } + + [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("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```\n\nYou may have some additional information about this class here.", response.Markdown); + } + + [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("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```\n\nChecks if object is tagged with the tag.", response.Markdown); + } + + [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("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```\n\nReturns:\n\n Returns true if object is tagged with tag.", response.Markdown); + } + + [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); + Assert.Equal("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```", response.Markdown); + } + + [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("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```\n\nExceptions:\n\n A\n\n B", response.Markdown); + } + + [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("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```", response.Markdown); + } + + [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("```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] + 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("```csharp\nT in TestClass.mkArray\n```\n\nThe element type of the array", response.Markdown); + } + + [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.Empty(response.Markdown); + } + + [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("```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] + 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("```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] + 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("```csharp\nT[] TestClass.mkArray(int n)\n```\n\nCreates a new array of arbitrary type `T`", response.Markdown); + } + + [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("```csharp\nint TestClass.GetZero()\n```", response.Markdown); + } + + [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("```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] + 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("```csharp\nvoid TestClass.DoWork(int Int1)\n```\n\nDoWork is a method in the TestClass class. `TestClass.Main()`", response.Markdown); + } + + [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("```csharp\nbool testissue.Compare(int gameObject, string tagName)\n```\n\nChecks if object is tagged with the tag.", response.Markdown); + } + + [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( + "```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] + 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("```csharp\nvoid TestClass.DoWork(int Int1)\n```\n\nDoWork is a method in the TestClass class.", response.Markdown); + } + + [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("```csharp\n(parameter) int gameObject\n```\n\nThe game object.", response.Markdown); + } + + [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("```csharp\n(parameter) string tagName\n```\n\nName of the tag.", response.Markdown); + } + + [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); + 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] + 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); + Assert.Equal("```csharp\nvoid Program.B()\n```\n\nHello World", response.Markdown); + } + + 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); + } + } +} 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); } }