From ca093ad918298a3a6e5e5cc041669c154b07efaa Mon Sep 17 00:00:00 2001 From: Anthony Martin Date: Mon, 31 Jan 2022 22:09:36 -0500 Subject: [PATCH] Add MS Docs link to resource symbol hovers (#5782) --- .../HoverTests.cs | 35 ++++++++++++- .../Handlers/BicepHoverHandler.cs | 49 ++++++++++++++++--- src/vscode-bicep/src/test/e2e/hover.test.ts | 5 +- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/Bicep.LangServer.IntegrationTests/HoverTests.cs b/src/Bicep.LangServer.IntegrationTests/HoverTests.cs index 433891fe52b..702b49e68e9 100644 --- a/src/Bicep.LangServer.IntegrationTests/HoverTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/HoverTests.cs @@ -301,8 +301,8 @@ var test|Param string h => h!.Contents.MarkupContent!.Value.Should().EndWith("```\nthis is my module\n"), h => h!.Contents.MarkupContent!.Value.Should().EndWith("```\nthis is my param\n"), h => h!.Contents.MarkupContent!.Value.Should().EndWith("```\nthis is my var\n"), - h => h!.Contents.MarkupContent!.Value.Should().EndWith("```\nthis is my \nmultiline \nresource\n"), - h => h!.Contents.MarkupContent!.Value.Should().EndWith("```\nthis is my output\n")); + h => h!.Contents.MarkupContent!.Value.Should().EndWith("```\nthis is my \nmultiline \nresource \n[View Type Documentation](https://docs.microsoft.com/azure/templates/test.rp/discriminatortests?tabs=bicep)\n"), + h => h!.Contents.MarkupContent!.Value.Should().EndWith("```\nthis is my output \n\n")); } [TestMethod] @@ -377,6 +377,37 @@ public async Task Function_hovers_include_descriptions_if_function_overload_has_ h => h!.Contents.MarkupContent!.Value.Should().Be("```bicep\nfunction concat('abc', 'def'): string\n```\nCombines multiple string, integer, or boolean values and returns them as a concatenated string.\n")); } + [TestMethod] + public async Task Resource_hovers_should_include_documentation_links_for_known_resource_types() + { + var hovers = await RequestHoversAtCursorLocations(@" +resource fo|o 'Test.Rp/basicTests@2020-01-01' = {} + +@description('This resource also has a description!') +resource b|ar 'Test.Rp/basicTests@2020-01-01' = {} + +resource m|adeUp 'Test.MadeUp/nonExistentResourceType@2020-01-01' = {} +"); + + hovers.Should().SatisfyRespectively( + h => h!.Contents.MarkupContent!.Value.Should().BeEquivalentToIgnoringNewlines(@"```bicep +resource foo 'Test.Rp/basicTests@2020-01-01' +``` +[View Type Documentation](https://docs.microsoft.com/azure/templates/test.rp/basictests?tabs=bicep) +"), + h => h!.Contents.MarkupContent!.Value.Should().BeEquivalentToIgnoringNewlines(@"```bicep +resource bar 'Test.Rp/basicTests@2020-01-01' +``` +This resource also has a description! +[View Type Documentation](https://docs.microsoft.com/azure/templates/test.rp/basictests?tabs=bicep) +"), + h => h!.Contents.MarkupContent!.Value.Should().BeEquivalentToIgnoringNewlines(@"```bicep +resource madeUp 'Test.MadeUp/nonExistentResourceType@2020-01-01' +``` + +")); + } + [TestMethod] public async Task Function_hovers_display_without_descriptions_if_function_overload_has_not_been_resolved() { diff --git a/src/Bicep.LangServer/Handlers/BicepHoverHandler.cs b/src/Bicep.LangServer/Handlers/BicepHoverHandler.cs index dcc345907e7..08220010b47 100644 --- a/src/Bicep.LangServer/Handlers/BicepHoverHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepHoverHandler.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Bicep.Core.Semantics; +using Bicep.Core.Semantics.Namespaces; using Bicep.Core.Syntax; +using Bicep.Core.TypeSystem; using Bicep.LanguageServer.Providers; using Bicep.LanguageServer.Utils; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; @@ -50,39 +53,54 @@ public BicepHoverHandler(ISymbolResolver symbolResolver) }); } + private static string? TryGetDescriptionMarkdown(SymbolResolutionResult result, DeclaredSymbol symbol) + { + if (symbol.DeclaringSyntax is StatementSyntax statementSyntax && + SemanticModelHelper.TryGetDescription(result.Context.Compilation.GetEntrypointSemanticModel(), statementSyntax) is {} description) + { + return description; + } + + return null; + } + private static string? GetMarkdown(HoverParams request, SymbolResolutionResult result) { - // all of the generated markdown includes the language id to avoid VS code rendering + // all of the generated markdown includes the language id to avoid VS code rendering // with multiple borders switch (result.Symbol) { case ImportedNamespaceSymbol import: return CodeBlockWithDescription( - $"import {import.Name}", SemanticModelHelper.TryGetDescription(result.Context.Compilation.GetEntrypointSemanticModel(), import.DeclaringImport)); + $"import {import.Name}", TryGetDescriptionMarkdown(result, import)); case ParameterSymbol parameter: return CodeBlockWithDescription( - $"param {parameter.Name}: {parameter.Type}", SemanticModelHelper.TryGetDescription(result.Context.Compilation.GetEntrypointSemanticModel(), parameter.DeclaringParameter)); + $"param {parameter.Name}: {parameter.Type}", TryGetDescriptionMarkdown(result, parameter)); case VariableSymbol variable: - return CodeBlockWithDescription($"var {variable.Name}: {variable.Type}", SemanticModelHelper.TryGetDescription(result.Context.Compilation.GetEntrypointSemanticModel(), variable.DeclaringVariable)); + return CodeBlockWithDescription($"var {variable.Name}: {variable.Type}", TryGetDescriptionMarkdown(result, variable)); case ResourceSymbol resource: + var docsSuffix = TryGetTypeDocumentationLink(resource) is {} typeDocsLink ? $"[View Type Documentation]({typeDocsLink})" : ""; + var description = TryGetDescriptionMarkdown(result, resource); + return CodeBlockWithDescription( - $"resource {resource.Name}\n{resource.Type}", SemanticModelHelper.TryGetDescription(result.Context.Compilation.GetEntrypointSemanticModel(), resource.DeclaringResource)); + $"resource {resource.Name} {(resource.Type is ResourceType ? $"'{resource.Type}'" : resource.Type)}", + description is {} ? $"{description}\n{docsSuffix}" : docsSuffix); case ModuleSymbol module: var filePath = SyntaxHelper.TryGetModulePath(module.DeclaringModule, out _); if (filePath != null) { - return CodeBlockWithDescription($"module {module.Name}\n'{filePath}'", SemanticModelHelper.TryGetDescription(result.Context.Compilation.GetEntrypointSemanticModel(), module.DeclaringModule)); + return CodeBlockWithDescription($"module {module.Name} '{filePath}'", TryGetDescriptionMarkdown(result, module)); } - return CodeBlockWithDescription($"module {module.Name}", SemanticModelHelper.TryGetDescription(result.Context.Compilation.GetEntrypointSemanticModel(), module.DeclaringModule)); + return CodeBlockWithDescription($"module {module.Name}", TryGetDescriptionMarkdown(result, module)); case OutputSymbol output: return CodeBlockWithDescription( - $"output {output.Name}: {output.Type}", SemanticModelHelper.TryGetDescription(result.Context.Compilation.GetEntrypointSemanticModel(), output.DeclaringOutput)); + $"output {output.Name}: {output.Type}", TryGetDescriptionMarkdown(result, output)); case BuiltInNamespaceSymbol builtInNamespace: return CodeBlock($"{builtInNamespace.Name} namespace"); @@ -147,6 +165,21 @@ private static string GetFunctionMarkdown(FunctionSymbol function, FunctionCallS return CodeBlock(buffer.ToString()); } + private static string? TryGetTypeDocumentationLink(ResourceSymbol resource) + { + if (resource.TryGetResourceType() is {} resourceType && + resourceType.DeclaringNamespace.ProviderNameEquals(AzNamespaceType.BuiltInName) && + resourceType.DeclaringNamespace.ResourceTypeProvider.HasDefinedType(resourceType.TypeReference)) + { + var provider = resourceType.TypeReference.TypeSegments.First().ToLowerInvariant(); + var typePath = resourceType.TypeReference.TypeSegments.Skip(1).Select(x => x.ToLowerInvariant()); + + return $"https://docs.microsoft.com/azure/templates/{provider}/{string.Join('/', typePath)}?tabs=bicep"; + } + + return null; + } + protected override HoverRegistrationOptions CreateRegistrationOptions(HoverCapability capability, ClientCapabilities clientCapabilities) => new() { DocumentSelector = DocumentSelectorFactory.Create() diff --git a/src/vscode-bicep/src/test/e2e/hover.test.ts b/src/vscode-bicep/src/test/e2e/hover.test.ts index ac8879846ca..cd4278102a7 100644 --- a/src/vscode-bicep/src/test/e2e/hover.test.ts +++ b/src/vscode-bicep/src/test/e2e/hover.test.ts @@ -70,8 +70,9 @@ describe("hover", (): void => { endLine: 108, endCharacter: 13, contents: [ - codeblock( - "resource vnet\nMicrosoft.Network/virtualNetworks@2020-06-01" + codeblockWithDescription( + "resource vnet 'Microsoft.Network/virtualNetworks@2020-06-01'", + "[View Type Documentation](https://docs.microsoft.com/azure/templates/microsoft.network/virtualnetworks?tabs=bicep)" ), ], });