From fb9883eab47cddbbed8adc19d44817d49ebe466a Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Fri, 6 Feb 2026 00:08:54 +0000 Subject: [PATCH 01/19] improve avm module completion readability with suffix labels and prefix descriptions --- .../ModuleReferenceCompletionProviderTests.cs | 62 +++++++++++++++++-- .../ModuleReferenceCompletionProvider.cs | 44 +++++++++++-- 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index 8626d1324f3..a63819984a2 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -559,14 +559,66 @@ public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionCo }); } + [TestMethod] + public async Task GetFilteredCompletions_WithAvmModulePath_UsesSuffixLabelAndPrefixDescription() + { + var catalog = RegistryCatalogMocks.CreateCatalogWithMocks( + RegistryCatalogMocks.MockPublicMetadataProvider([ + new("bicep/avm/ptn/ai-ml/ai-foundry", null, null, []), + new("bicep/avm/ptn/ai-ml/ai-platform", null, null, []), + ]) + ); + + var (completionContext, sourceFile) = GetBicepCompletionContext("module test 'br/public:|'"); + var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( + azureContainerRegistriesProvider, + catalog, + settingsProvider, + BicepTestConstants.CreateMockTelemetryProvider().Object); + var completions = await GetAndResolveCompletionItems(sourceFile, completionContext, moduleReferenceCompletionProvider); + + completions.Should().Contain( + c => c.Label == "ai-foundry" && + c.LabelDetails != null && + c.LabelDetails.Description == "avm/ptn/ai-ml/" && + c.TextEdit!.TextEdit!.NewText == "'br/public:avm/ptn/ai-ml/ai-foundry:$0'"); + } + + [TestMethod] + public async Task GetFilteredCompletions_WithAvmPathPrefix_ReturnsMatchingCompletion() + { + var catalog = RegistryCatalogMocks.CreateCatalogWithMocks( + RegistryCatalogMocks.MockPublicMetadataProvider([ + new("bicep/avm/ptn/ai-ml/ai-foundry", null, null, []), + new("bicep/avm/ptn/ai-ml/ai-platform", null, null, []), + new("bicep/avm/ptn/ai-platform/baseline", null, null, []), + ]) + ); + + var (completionContext, sourceFile) = GetBicepCompletionContext("module test 'br/public:avm/ptn/ai-ml/|'"); + var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( + azureContainerRegistriesProvider, + catalog, + settingsProvider, + BicepTestConstants.CreateMockTelemetryProvider().Object); + var completions = await GetAndResolveCompletionItems(sourceFile, completionContext, moduleReferenceCompletionProvider); + + completions.Should().Contain( + c => c.Label == "ai-foundry" && + c.TextEdit!.TextEdit!.NewText == "'br/public:avm/ptn/ai-ml/ai-foundry:$0'"); + completions.Should().NotContain( + c => c.TextEdit!.TextEdit!.NewText == "'br/public:avm/ptn/ai-platform/baseline:$0'"); + } + [DataTestMethod] - [DataRow("module test 'br:registry.contoso.io/bicep/|'", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] - [DataRow("module test 'br:registry.contoso.io/bicep/|", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] - [DataRow("module test 'br/myRegistry:|'", "abc/foo/bar", "'br/myRegistry:abc/foo/bar:$0'")] - [DataRow("module test 'br/myRegistry_noPath:|'", "bicep/whatever/abc/foo/bar", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'")] + [DataRow("module test 'br:registry.contoso.io/bicep/|'", "bar", "bicep/whatever/abc/foo/", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] + [DataRow("module test 'br:registry.contoso.io/bicep/|", "bar", "bicep/whatever/abc/foo/", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] + [DataRow("module test 'br/myRegistry:|'", "bar", "abc/foo/", "'br/myRegistry:abc/foo/bar:$0'")] + [DataRow("module test 'br/myRegistry_noPath:|'", "bar", "bicep/whatever/abc/foo/", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'")] public async Task GetFilteredCompletions_WithPrivateModulePathCompletions_ReturnsCompletionItems( string inputWithCursors, string expectedLabel, + string expectedLabelDescription, string expectedCompletionText) { var catalog = RegistryCatalogMocks.CreateCatalogWithMocks( @@ -608,6 +660,8 @@ public async Task GetFilteredCompletions_WithPrivateModulePathCompletions_Return c => { c.Label.Should().Be(expectedLabel); + c.LabelDetails.Should().NotBeNull(); + c.LabelDetails!.Description.Should().Be(expectedLabelDescription); c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.Detail.Should().Be("d1"); c.Documentation!.MarkupContent!.Value.Should().Be("[View Documentation](contoso.com/help1)"); diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 525fa4258f7..60bd3514461 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -497,21 +497,29 @@ private async Task> GetModuleCompletions(string trim string insertText = $"'{parts.WithModulePath(moduleName).ToNotation()}:$0'"; - // Remove the base path prefix from the label if we're dealing with a module alias - var label = !string.IsNullOrWhiteSpace(parts.SpecifiedAlias) && !string.IsNullOrWhiteSpace(parts.ModulePathPrefix) + // Remove the base path prefix from the display path if we're dealing with a module alias + var displayPath = !string.IsNullOrWhiteSpace(parts.SpecifiedAlias) && !string.IsNullOrWhiteSpace(parts.ModulePathPrefix) ? moduleName.Substring(parts.ModulePathPrefixWithSeparator.Length) : moduleName; - var completionItem = CompletionItemBuilder.Create( + var (label, labelPrefix) = GetSuffixLabel(displayPath); + + var completionItemBuilder = CompletionItemBuilder.Create( CompletionItemKind.Snippet, label) .WithSnippetEdit(context.ReplacementRange, insertText) .WithFilterText(insertText) - .WithSortText(GetSortText(moduleName)) + .WithSortText(GetSortText(GetModuleSortKey(label, displayPath))) .WithResolveData( ModuleResolutionKey, new { Registry = module.Registry, Module = moduleName }) - .WithFollowupCompletion("module version completion") - .Build(); + .WithFollowupCompletion("module version completion"); + + if (!string.IsNullOrWhiteSpace(labelPrefix)) + { + completionItemBuilder.WithLabelDetails(string.Empty, labelPrefix); + } + + var completionItem = completionItemBuilder.Build(); completions.Add(completionItem); @@ -738,5 +746,29 @@ private static string GetSortText(string label, ModuleCompletionPriority priorit // We want all module completion priorities to come after other completions (e.g. local module paths), so we start with "9" return $"9{(int)priority}_{label}"; } + + private static (string Label, string? Prefix) GetSuffixLabel(string displayPath) + { + if (string.IsNullOrWhiteSpace(displayPath)) + { + return (displayPath, null); + } + + var lastSlashIndex = displayPath.LastIndexOf('/'); + if (lastSlashIndex < 0 || lastSlashIndex == displayPath.Length - 1) + { + return (displayPath, null); + } + + var prefix = displayPath.Substring(0, lastSlashIndex + 1); + var label = displayPath.Substring(lastSlashIndex + 1); + return (label, prefix); + } + + private static string GetModuleSortKey(string label, string displayPath) + { + // Prefer matches by suffix while keeping a stable, deterministic order. + return $"{label}|{displayPath}"; + } } } From dce23e1b00c0b8ad30142c4c73e6a2b5920f7a24 Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Fri, 6 Feb 2026 01:10:02 +0000 Subject: [PATCH 02/19] fixing failing tests --- .../CompletionTests.cs | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index e3f92f35c0b..eeb35782411 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -4547,15 +4547,15 @@ public async Task Private_module_version_completions(string inputWithCursors, Bi } [TestMethod] - [DataRow("module test 'br:mcr.microsoft.com/bicep/abc/foo|'", "bicep/abc/foo/bar", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br:mcr.microsoft.com/bicep/abc/foo|", "bicep/abc/foo/bar", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br/public:abc/foo|'", "abc/foo/bar", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br/public:abc/foo|", "abc/foo/bar", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("using 'br:mcr.microsoft.com/bicep/abc/foo|'", "bicep/abc/foo/bar", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - [DataRow("using 'br:mcr.microsoft.com/bicep/abc/foo|", "bicep/abc/foo/bar", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - [DataRow("using 'br/public:abc/foo|'", "abc/foo/bar", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - [DataRow("using 'br/public:abc/foo|", "abc/foo/bar", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - public async Task Public_registry_module_completions_support_prefix_matching(string text, string expectedLabelForFoo, string expectedInsertTextForFoo, BicepSourceFileKind kind) + [DataRow("module test 'br:mcr.microsoft.com/bicep/abc/foo|'", "bar", "bicep/abc/foo/", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br:mcr.microsoft.com/bicep/abc/foo|", "bar", "bicep/abc/foo/", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br/public:abc/foo|'", "bar", "abc/foo/", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br/public:abc/foo|", "bar", "abc/foo/", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("using 'br:mcr.microsoft.com/bicep/abc/foo|'", "bar", "bicep/abc/foo/", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + [DataRow("using 'br:mcr.microsoft.com/bicep/abc/foo|", "bar", "bicep/abc/foo/", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + [DataRow("using 'br/public:abc/foo|'", "bar", "abc/foo/", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + [DataRow("using 'br/public:abc/foo|", "bar", "abc/foo/", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + public async Task Public_registry_module_completions_support_prefix_matching(string text, string expectedLabelForFoo, string expectedLabelDescriptionForFoo, string expectedInsertTextForFoo, BicepSourceFileKind kind) { var extension = kind == BicepSourceFileKind.ParamsFile ? "bicepparam" : "bicep"; var (fileText, cursor) = ParserHelper.GetFileWithSingleCursor(text, '|'); @@ -4565,7 +4565,7 @@ public async Task Public_registry_module_completions_support_prefix_matching(str settingsProvider.Setup(x => x.GetSetting(LangServerConstants.GetAllAzureContainerRegistriesForCompletionsSetting)).Returns(false); var publicModuleMetadataProvider = RegistryCatalogMocks.MockPublicMetadataProvider([ - ("bicep/abc/foo/bar", "d1", "contoso.com/help1", []), + ("bicep/abc/foo/bar", "d1", "contoso.com/help1", []), ("bicep/abc/food/bar", "d2", "contoso.com/help2", []), ("bicep/abc/bar/bar", "d3", "contoso.com/help3", []), ]); @@ -4580,28 +4580,32 @@ public async Task Public_registry_module_completions_support_prefix_matching(str var completions = await file.RequestAndResolveCompletions(cursor); completions.Count().Should().Be(2); - completions.Select(x => (Label: x.Label, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( + completions.Select(x => (Label: x.Label, LabelDetails: x.LabelDetails, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( c => { c.Label.Should().Be(expectedLabelForFoo); + c.LabelDetails.Should().NotBeNull(); + c.LabelDetails!.Description.Should().Be(expectedLabelDescriptionForFoo); c.InsertText.Should().Be(expectedInsertTextForFoo); }, c => { c.Label.Should().Be(expectedLabelForFoo.Replace("foo/", "food/")); + c.LabelDetails.Should().NotBeNull(); + c.LabelDetails!.Description.Should().Be(expectedLabelDescriptionForFoo.Replace("foo/", "food/")); c.InsertText.Should().Be(expectedInsertTextForFoo.Replace("foo/", "food/")); } ); } [TestMethod] - [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|'", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br/myRegistry:abc/foo|'", "abc/foo/bar", "'br/myRegistry:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br/myRegistry_noPath:bicep/whatever/abc/foo|", "bicep/whatever/abc/foo/bar", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|'", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - [DataRow("module test 'br/myRegistry_noPath:bicep/whatever/abc/foo|", "bicep/whatever/abc/foo/bar", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - public async Task Private_registry_completions_support_prefix_matching(string text, string expectedLabelForFoo, string expectedInsertTextForFoo, BicepSourceFileKind kind) + [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|'", "bar", "bicep/whatever/abc/foo/", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|", "bar", "bicep/whatever/abc/foo/", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br/myRegistry:abc/foo|'", "bar", "abc/foo/", "'br/myRegistry:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br/myRegistry_noPath:bicep/whatever/abc/foo|", "bar", "bicep/whatever/abc/foo/", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|'", "bar", "bicep/whatever/abc/foo/", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + [DataRow("module test 'br/myRegistry_noPath:bicep/whatever/abc/foo|", "bar", "bicep/whatever/abc/foo/", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + public async Task Private_registry_completions_support_prefix_matching(string text, string expectedLabelForFoo, string expectedLabelDescriptionForFoo, string expectedInsertTextForFoo, BicepSourceFileKind kind) { var extension = kind == BicepSourceFileKind.ParamsFile ? "bicepparam" : "bicep"; var (fileText, cursor) = ParserHelper.GetFileWithSingleCursor(text, '|'); @@ -4654,25 +4658,29 @@ public async Task Private_registry_completions_support_prefix_matching(string te var completions = await file.RequestAndResolveCompletions(cursor); completions.Count().Should().Be(2); - completions.Select(x => (Label: x.Label, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( + completions.Select(x => (Label: x.Label, LabelDetails: x.LabelDetails, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( c => { c.Label.Should().Be(expectedLabelForFoo); + c.LabelDetails.Should().NotBeNull(); + c.LabelDetails!.Description.Should().Be(expectedLabelDescriptionForFoo); c.InsertText.Should().Be(expectedInsertTextForFoo); }, c => { c.Label.Should().Be(expectedLabelForFoo.Replace("foo/", "food/")); + c.LabelDetails.Should().NotBeNull(); + c.LabelDetails!.Description.Should().Be(expectedLabelDescriptionForFoo.Replace("foo/", "food/")); c.InsertText.Should().Be(expectedInsertTextForFoo.Replace("foo/", "food/")); } ); } [TestMethod] - [DataRow("module test 'br/ms:bicep/app/|'", "bicep/app/dapr-containerapp", "'br/ms:bicep/app/dapr-containerapp:$0'")] - [DataRow("module test 'br/ms_empty:bicep/app/|'", "bicep/app/dapr-containerapp", "'br/ms_empty:bicep/app/dapr-containerapp:$0'")] - [DataRow("module test 'br/ms_bicep:app/|'", "app/dapr-containerapp", "'br/ms_bicep:app/dapr-containerapp:$0'")] - public async Task Public_registry_via_alias_supports_completions(string text, string expectedLabel, string expectedInsertText) + [DataRow("module test 'br/ms:bicep/app/|'", "dapr-containerapp", "bicep/app/", "'br/ms:bicep/app/dapr-containerapp:$0'")] + [DataRow("module test 'br/ms_empty:bicep/app/|'", "dapr-containerapp", "bicep/app/", "'br/ms_empty:bicep/app/dapr-containerapp:$0'")] + [DataRow("module test 'br/ms_bicep:app/|'", "dapr-containerapp", "app/", "'br/ms_bicep:app/dapr-containerapp:$0'")] + public async Task Public_registry_via_alias_supports_completions(string text, string expectedLabel, string expectedLabelDescription, string expectedInsertText) { var (fileText, cursor) = ParserHelper.GetFileWithSingleCursor(text, '|'); var baseFolder = $"{Guid.NewGuid():D}"; @@ -4724,10 +4732,12 @@ public async Task Public_registry_via_alias_supports_completions(string text, st var completions = await file.RequestAndResolveCompletions(cursor); completions.Count().Should().Be(1); - completions.Select(x => (Label: x.Label, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( + completions.Select(x => (Label: x.Label, LabelDetails: x.LabelDetails, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( c => { c.Label.Should().Be(expectedLabel); + c.LabelDetails.Should().NotBeNull(); + c.LabelDetails!.Description.Should().Be(expectedLabelDescription); c.InsertText.Should().Be(expectedInsertText); } ); From 20fa6e70f2eb828c43e7573047691e2132fe9cda Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:11:39 +0000 Subject: [PATCH 03/19] enhance module completion documentation with detailed display names, paths, and descriptions --- .../ModuleReferenceCompletionProviderTests.cs | 44 ++++++++++++----- .../ModuleReferenceCompletionProvider.cs | 47 ++++++++++++++++++- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index a63819984a2..c3d098a3bac 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -500,10 +500,10 @@ public async Task GetFilteredCompletions_WithACRCompletionsSettingSetToTrue_AndN } [DataTestMethod] - [DataRow("module test 'br:mcr.microsoft.com/bicep/|'", "bicep/app/dapr-cntrapp1", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp1:$0'", "bicep/app/dapr-cntrapp2", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp2:$0'", 41)] - [DataRow("module test 'br:mcr.microsoft.com/bicep/|", "bicep/app/dapr-cntrapp1", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp1:$0'", "bicep/app/dapr-cntrapp2", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp2:$0'", 40)] - [DataRow("module test 'br/public:|'", "app/dapr-cntrapp1", "'br/public:app/dapr-cntrapp1:$0'", "app/dapr-cntrapp2", "'br/public:app/dapr-cntrapp2:$0'", 24)] - [DataRow("module test 'br/public:|", "app/dapr-cntrapp1", "'br/public:app/dapr-cntrapp1:$0'", "app/dapr-cntrapp2", "'br/public:app/dapr-cntrapp2:$0'", 23)] + [DataRow("module test 'br:mcr.microsoft.com/bicep/|'", "dapr-cntrapp1", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp1:$0'", "dapr-cntrapp2", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp2:$0'", 41)] + [DataRow("module test 'br:mcr.microsoft.com/bicep/|", "dapr-cntrapp1", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp1:$0'", "dapr-cntrapp2", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp2:$0'", 40)] + [DataRow("module test 'br/public:|'", "dapr-cntrapp1", "'br/public:app/dapr-cntrapp1:$0'", "dapr-cntrapp2", "'br/public:app/dapr-cntrapp2:$0'", 24)] + [DataRow("module test 'br/public:|", "dapr-cntrapp1", "'br/public:app/dapr-cntrapp1:$0'", "dapr-cntrapp2", "'br/public:app/dapr-cntrapp2:$0'", 23)] public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionContext_ReturnsCompletionItems( string inputWithCursors, string expectedLabel1, @@ -536,7 +536,10 @@ public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionCo c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.InsertText.Should().BeNull(); c.Detail.Should().BeNull(); - c.Documentation.Should().BeNull(); + c.Documentation!.MarkupContent!.Value.Should().Contain($"**Display name:** {expectedLabel1}"); + c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** bicep/app/dapr-cntrapp1"); + c.Documentation.MarkupContent.Value.Should().Contain("**Description:** N/A"); + c.Documentation.MarkupContent.Value.Should().Contain("**Documentation:** N/A"); c.TextEdit!.TextEdit!.NewText.Should().Be(expectedCompletionText1); c.TextEdit.TextEdit.Range.Start.Line.Should().Be(0); c.TextEdit.TextEdit.Range.Start.Character.Should().Be(12); @@ -550,7 +553,10 @@ public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionCo c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.InsertText.Should().BeNull(); c.Detail.Should().Be("description2"); - c.Documentation!.MarkupContent!.Value.Should().Be("[View Documentation](contoso.com/help2)"); + c.Documentation!.MarkupContent!.Value.Should().Contain($"**Display name:** {expectedLabel2}"); + c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** bicep/app/dapr-cntrapp2"); + c.Documentation.MarkupContent.Value.Should().Contain("**Description:** description2"); + c.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help2)"); c.TextEdit!.TextEdit!.NewText.Should().Be(expectedCompletionText2); c.TextEdit.TextEdit.Range.Start.Line.Should().Be(0); c.TextEdit.TextEdit.Range.Start.Character.Should().Be(12); @@ -664,7 +670,10 @@ public async Task GetFilteredCompletions_WithPrivateModulePathCompletions_Return c.LabelDetails!.Description.Should().Be(expectedLabelDescription); c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.Detail.Should().Be("d1"); - c.Documentation!.MarkupContent!.Value.Should().Be("[View Documentation](contoso.com/help1)"); + c.Documentation!.MarkupContent!.Value.Should().Contain("**Display name:** bar"); + c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** bicep/whatever/abc/foo/bar"); + c.Documentation.MarkupContent.Value.Should().Contain("**Description:** d1"); + c.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help1)"); c.TextEdit!.TextEdit!.NewText.Should().Be(expectedCompletionText); }); } @@ -774,7 +783,11 @@ public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_Returns x.InsertText == null && x.SortText == expectedSortText1 && x.Detail == null && - x.Documentation == null && + x.Documentation!.MarkupContent!.Value.Contains("**Display name:** 1.0.2") && + x.Documentation.MarkupContent.Value.Contains("**Full module path:** bicep/app/dapr-containerapp") && + x.Documentation.MarkupContent.Value.Contains("**Version:** 1.0.2") && + x.Documentation.MarkupContent.Value.Contains("**Description:** N/A") && + x.Documentation.MarkupContent.Value.Contains("**Documentation:** N/A") && x.TextEdit!.TextEdit!.NewText == expectedCompletionText1 && x.TextEdit!.TextEdit!.Range.Start.Line == 0 && x.TextEdit!.TextEdit!.Range.Start.Character == 12 && @@ -787,7 +800,11 @@ public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_Returns x.InsertText == null && x.SortText == expectedSortText2 && x.Detail == "d2" && - x.Documentation!.MarkupContent!.Value == "[View Documentation](contoso.com/help%20page.html)" && + x.Documentation!.MarkupContent!.Value.Contains("**Display name:** 1.0.1") && + x.Documentation.MarkupContent.Value.Contains("**Full module path:** bicep/app/dapr-containerapp") && + x.Documentation.MarkupContent.Value.Contains("**Version:** 1.0.1") && + x.Documentation.MarkupContent.Value.Contains("**Description:** d2") && + x.Documentation.MarkupContent.Value.Contains("[View Documentation](contoso.com/help%20page.html)") && x.TextEdit!.TextEdit!.NewText == expectedCompletionText2 && x.TextEdit!.TextEdit!.Range.Start.Line == 0 && x.TextEdit!.TextEdit!.Range.Start.Character == 12 && @@ -870,8 +887,8 @@ public async Task GetFilteredCompletions_WithPublicAliasOverriddenInBicepConfigA [DataTestMethod] [DataRow("module test 'br/test1:|'", "dapr-containerapp", "'br/test1:dapr-containerapp:$0'", 0, 12, 0, 23)] [DataRow("module test 'br/test1:|", "dapr-containerapp", "'br/test1:dapr-containerapp:$0'", 0, 12, 0, 22)] - [DataRow("module test 'br/test2:|'", "bicep/app/dapr-containerapp", "'br/test2:bicep/app/dapr-containerapp:$0'", 0, 12, 0, 23)] - [DataRow("module test 'br/test2:|", "bicep/app/dapr-containerapp", "'br/test2:bicep/app/dapr-containerapp:$0'", 0, 12, 0, 22)] + [DataRow("module test 'br/test2:|'", "dapr-containerapp", "'br/test2:bicep/app/dapr-containerapp:$0'", 0, 12, 0, 23)] + [DataRow("module test 'br/test2:|", "dapr-containerapp", "'br/test2:bicep/app/dapr-containerapp:$0'", 0, 12, 0, 22)] public async Task GetFilteredCompletions_WithAliasForMCRInBicepConfigAndModulePath_ReturnsCompletionItems( string inputWithCursors, string expectedLabel, @@ -917,7 +934,10 @@ public async Task GetFilteredCompletions_WithAliasForMCRInBicepConfigAndModulePa x.Kind.Should().Be(CompletionItemKind.Snippet); x.InsertText.Should().BeNull(); x.Detail.Should().Be("dapr description"); - x.Documentation!.MarkupContent!.Value.Should().Be("[View Documentation](contoso.com/help)"); + x.Documentation!.MarkupContent!.Value.Should().Contain("**Display name:** dapr-containerapp"); + x.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** bicep/app/dapr-containerapp"); + x.Documentation.MarkupContent.Value.Should().Contain("**Description:** dapr description"); + x.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help)"); var actualTextEdit = x.TextEdit!.TextEdit; actualTextEdit.Should().NotBeNull(); diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 60bd3514461..4e203c9824d 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -680,7 +680,7 @@ private async Task ResolveVersionCompletionItem(CompletionItem c { Detail = metadata.Details.Description, }) - .WithDocumentation(MarkdownHelper.GetDocumentationLink(metadata.Details.DocumentationUri)); + .WithDocumentation(GetVersionCompletionDocumentation(completionItem.Label, modulePath, version, metadata.Details)); } return completionItem; @@ -702,7 +702,7 @@ private async Task ResolveModuleCompletionItem(CompletionItem co return (completionItem with { Detail = details.Description, - }).WithDocumentation(MarkdownHelper.GetDocumentationLink(details.DocumentationUri)); + }).WithDocumentation(GetModuleCompletionDocumentation(completionItem.Label, modulePath, details)); } return completionItem; @@ -770,5 +770,48 @@ private static string GetModuleSortKey(string label, string displayPath) // Prefer matches by suffix while keeping a stable, deterministic order. return $"{label}|{displayPath}"; } + + private static string GetModuleCompletionDocumentation(string displayName, string modulePath, RegistryMetadataDetails details) + { + var sections = new List + { + $"**Display name:** {displayName}", + $"**Full module path:** {modulePath}", + $"**Description:** {details.Description ?? "N/A"}", + }; + + if (MarkdownHelper.GetDocumentationLink(details.DocumentationUri) is { } docLink) + { + sections.Add(docLink); + } + else + { + sections.Add("**Documentation:** N/A"); + } + + return MarkdownHelper.JoinWithNewlines(sections); + } + + private static string GetVersionCompletionDocumentation(string displayName, string modulePath, string version, RegistryMetadataDetails details) + { + var sections = new List + { + $"**Display name:** {displayName}", + $"**Full module path:** {modulePath}", + $"**Version:** {version}", + $"**Description:** {details.Description ?? "N/A"}", + }; + + if (MarkdownHelper.GetDocumentationLink(details.DocumentationUri) is { } docLink) + { + sections.Add(docLink); + } + else + { + sections.Add("**Documentation:** N/A"); + } + + return MarkdownHelper.JoinWithNewlines(sections); + } } } From 1e250f0e33572e4ef2d273cc163fd8aea28a137c Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:34:08 +0000 Subject: [PATCH 04/19] refactor completion item handling for improved readability and maintainability --- .../ModuleReferenceCompletionProviderTests.cs | 78 +++---------------- .../ModuleReferenceCompletionProvider.cs | 44 ++--------- 2 files changed, 18 insertions(+), 104 deletions(-) diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index c3d098a3bac..d13bba02d49 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -500,10 +500,10 @@ public async Task GetFilteredCompletions_WithACRCompletionsSettingSetToTrue_AndN } [DataTestMethod] - [DataRow("module test 'br:mcr.microsoft.com/bicep/|'", "dapr-cntrapp1", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp1:$0'", "dapr-cntrapp2", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp2:$0'", 41)] - [DataRow("module test 'br:mcr.microsoft.com/bicep/|", "dapr-cntrapp1", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp1:$0'", "dapr-cntrapp2", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp2:$0'", 40)] - [DataRow("module test 'br/public:|'", "dapr-cntrapp1", "'br/public:app/dapr-cntrapp1:$0'", "dapr-cntrapp2", "'br/public:app/dapr-cntrapp2:$0'", 24)] - [DataRow("module test 'br/public:|", "dapr-cntrapp1", "'br/public:app/dapr-cntrapp1:$0'", "dapr-cntrapp2", "'br/public:app/dapr-cntrapp2:$0'", 23)] + [DataRow("module test 'br:mcr.microsoft.com/bicep/|'", "bicep/app/dapr-cntrapp1", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp1:$0'", "bicep/app/dapr-cntrapp2", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp2:$0'", 41)] + [DataRow("module test 'br:mcr.microsoft.com/bicep/|", "bicep/app/dapr-cntrapp1", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp1:$0'", "bicep/app/dapr-cntrapp2", "'br:mcr.microsoft.com/bicep/app/dapr-cntrapp2:$0'", 40)] + [DataRow("module test 'br/public:|'", "app/dapr-cntrapp1", "'br/public:app/dapr-cntrapp1:$0'", "app/dapr-cntrapp2", "'br/public:app/dapr-cntrapp2:$0'", 24)] + [DataRow("module test 'br/public:|", "app/dapr-cntrapp1", "'br/public:app/dapr-cntrapp1:$0'", "app/dapr-cntrapp2", "'br/public:app/dapr-cntrapp2:$0'", 23)] public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionContext_ReturnsCompletionItems( string inputWithCursors, string expectedLabel1, @@ -565,66 +565,14 @@ public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionCo }); } - [TestMethod] - public async Task GetFilteredCompletions_WithAvmModulePath_UsesSuffixLabelAndPrefixDescription() - { - var catalog = RegistryCatalogMocks.CreateCatalogWithMocks( - RegistryCatalogMocks.MockPublicMetadataProvider([ - new("bicep/avm/ptn/ai-ml/ai-foundry", null, null, []), - new("bicep/avm/ptn/ai-ml/ai-platform", null, null, []), - ]) - ); - - var (completionContext, sourceFile) = GetBicepCompletionContext("module test 'br/public:|'"); - var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( - azureContainerRegistriesProvider, - catalog, - settingsProvider, - BicepTestConstants.CreateMockTelemetryProvider().Object); - var completions = await GetAndResolveCompletionItems(sourceFile, completionContext, moduleReferenceCompletionProvider); - - completions.Should().Contain( - c => c.Label == "ai-foundry" && - c.LabelDetails != null && - c.LabelDetails.Description == "avm/ptn/ai-ml/" && - c.TextEdit!.TextEdit!.NewText == "'br/public:avm/ptn/ai-ml/ai-foundry:$0'"); - } - - [TestMethod] - public async Task GetFilteredCompletions_WithAvmPathPrefix_ReturnsMatchingCompletion() - { - var catalog = RegistryCatalogMocks.CreateCatalogWithMocks( - RegistryCatalogMocks.MockPublicMetadataProvider([ - new("bicep/avm/ptn/ai-ml/ai-foundry", null, null, []), - new("bicep/avm/ptn/ai-ml/ai-platform", null, null, []), - new("bicep/avm/ptn/ai-platform/baseline", null, null, []), - ]) - ); - - var (completionContext, sourceFile) = GetBicepCompletionContext("module test 'br/public:avm/ptn/ai-ml/|'"); - var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( - azureContainerRegistriesProvider, - catalog, - settingsProvider, - BicepTestConstants.CreateMockTelemetryProvider().Object); - var completions = await GetAndResolveCompletionItems(sourceFile, completionContext, moduleReferenceCompletionProvider); - - completions.Should().Contain( - c => c.Label == "ai-foundry" && - c.TextEdit!.TextEdit!.NewText == "'br/public:avm/ptn/ai-ml/ai-foundry:$0'"); - completions.Should().NotContain( - c => c.TextEdit!.TextEdit!.NewText == "'br/public:avm/ptn/ai-platform/baseline:$0'"); - } - [DataTestMethod] - [DataRow("module test 'br:registry.contoso.io/bicep/|'", "bar", "bicep/whatever/abc/foo/", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] - [DataRow("module test 'br:registry.contoso.io/bicep/|", "bar", "bicep/whatever/abc/foo/", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] - [DataRow("module test 'br/myRegistry:|'", "bar", "abc/foo/", "'br/myRegistry:abc/foo/bar:$0'")] - [DataRow("module test 'br/myRegistry_noPath:|'", "bar", "bicep/whatever/abc/foo/", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'")] + [DataRow("module test 'br:registry.contoso.io/bicep/|'", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] + [DataRow("module test 'br:registry.contoso.io/bicep/|", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] + [DataRow("module test 'br/myRegistry:|'", "abc/foo/bar", "'br/myRegistry:abc/foo/bar:$0'")] + [DataRow("module test 'br/myRegistry_noPath:|'", "bicep/whatever/abc/foo/bar", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'")] public async Task GetFilteredCompletions_WithPrivateModulePathCompletions_ReturnsCompletionItems( string inputWithCursors, string expectedLabel, - string expectedLabelDescription, string expectedCompletionText) { var catalog = RegistryCatalogMocks.CreateCatalogWithMocks( @@ -666,11 +614,9 @@ public async Task GetFilteredCompletions_WithPrivateModulePathCompletions_Return c => { c.Label.Should().Be(expectedLabel); - c.LabelDetails.Should().NotBeNull(); - c.LabelDetails!.Description.Should().Be(expectedLabelDescription); c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.Detail.Should().Be("d1"); - c.Documentation!.MarkupContent!.Value.Should().Contain("**Display name:** bar"); + c.Documentation!.MarkupContent!.Value.Should().Contain($"**Display name:** {expectedLabel}"); c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** bicep/whatever/abc/foo/bar"); c.Documentation.MarkupContent.Value.Should().Contain("**Description:** d1"); c.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help1)"); @@ -887,8 +833,8 @@ public async Task GetFilteredCompletions_WithPublicAliasOverriddenInBicepConfigA [DataTestMethod] [DataRow("module test 'br/test1:|'", "dapr-containerapp", "'br/test1:dapr-containerapp:$0'", 0, 12, 0, 23)] [DataRow("module test 'br/test1:|", "dapr-containerapp", "'br/test1:dapr-containerapp:$0'", 0, 12, 0, 22)] - [DataRow("module test 'br/test2:|'", "dapr-containerapp", "'br/test2:bicep/app/dapr-containerapp:$0'", 0, 12, 0, 23)] - [DataRow("module test 'br/test2:|", "dapr-containerapp", "'br/test2:bicep/app/dapr-containerapp:$0'", 0, 12, 0, 22)] + [DataRow("module test 'br/test2:|'", "bicep/app/dapr-containerapp", "'br/test2:bicep/app/dapr-containerapp:$0'", 0, 12, 0, 23)] + [DataRow("module test 'br/test2:|", "bicep/app/dapr-containerapp", "'br/test2:bicep/app/dapr-containerapp:$0'", 0, 12, 0, 22)] public async Task GetFilteredCompletions_WithAliasForMCRInBicepConfigAndModulePath_ReturnsCompletionItems( string inputWithCursors, string expectedLabel, @@ -934,7 +880,7 @@ public async Task GetFilteredCompletions_WithAliasForMCRInBicepConfigAndModulePa x.Kind.Should().Be(CompletionItemKind.Snippet); x.InsertText.Should().BeNull(); x.Detail.Should().Be("dapr description"); - x.Documentation!.MarkupContent!.Value.Should().Contain("**Display name:** dapr-containerapp"); + x.Documentation!.MarkupContent!.Value.Should().Contain($"**Display name:** {expectedLabel}"); x.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** bicep/app/dapr-containerapp"); x.Documentation.MarkupContent.Value.Should().Contain("**Description:** dapr description"); x.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help)"); diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 4e203c9824d..eeb72eb9983 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -497,29 +497,21 @@ private async Task> GetModuleCompletions(string trim string insertText = $"'{parts.WithModulePath(moduleName).ToNotation()}:$0'"; - // Remove the base path prefix from the display path if we're dealing with a module alias - var displayPath = !string.IsNullOrWhiteSpace(parts.SpecifiedAlias) && !string.IsNullOrWhiteSpace(parts.ModulePathPrefix) + // Remove the base path prefix from the label if we're dealing with a module alias + var label = !string.IsNullOrWhiteSpace(parts.SpecifiedAlias) && !string.IsNullOrWhiteSpace(parts.ModulePathPrefix) ? moduleName.Substring(parts.ModulePathPrefixWithSeparator.Length) : moduleName; - var (label, labelPrefix) = GetSuffixLabel(displayPath); - - var completionItemBuilder = CompletionItemBuilder.Create( + var completionItem = CompletionItemBuilder.Create( CompletionItemKind.Snippet, label) .WithSnippetEdit(context.ReplacementRange, insertText) .WithFilterText(insertText) - .WithSortText(GetSortText(GetModuleSortKey(label, displayPath))) + .WithSortText(GetSortText(moduleName)) .WithResolveData( ModuleResolutionKey, new { Registry = module.Registry, Module = moduleName }) - .WithFollowupCompletion("module version completion"); - - if (!string.IsNullOrWhiteSpace(labelPrefix)) - { - completionItemBuilder.WithLabelDetails(string.Empty, labelPrefix); - } - - var completionItem = completionItemBuilder.Build(); + .WithFollowupCompletion("module version completion") + .Build(); completions.Add(completionItem); @@ -747,30 +739,6 @@ private static string GetSortText(string label, ModuleCompletionPriority priorit return $"9{(int)priority}_{label}"; } - private static (string Label, string? Prefix) GetSuffixLabel(string displayPath) - { - if (string.IsNullOrWhiteSpace(displayPath)) - { - return (displayPath, null); - } - - var lastSlashIndex = displayPath.LastIndexOf('/'); - if (lastSlashIndex < 0 || lastSlashIndex == displayPath.Length - 1) - { - return (displayPath, null); - } - - var prefix = displayPath.Substring(0, lastSlashIndex + 1); - var label = displayPath.Substring(lastSlashIndex + 1); - return (label, prefix); - } - - private static string GetModuleSortKey(string label, string displayPath) - { - // Prefer matches by suffix while keeping a stable, deterministic order. - return $"{label}|{displayPath}"; - } - private static string GetModuleCompletionDocumentation(string displayName, string modulePath, RegistryMetadataDetails details) { var sections = new List From d29a6c097f8e7667798cc6e2794bec8ea619008a Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:17:22 +0000 Subject: [PATCH 05/19] reverting tests --- .../CompletionTests.cs | 56 ++++++++----------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index eeb35782411..3894e8680eb 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -4547,15 +4547,15 @@ public async Task Private_module_version_completions(string inputWithCursors, Bi } [TestMethod] - [DataRow("module test 'br:mcr.microsoft.com/bicep/abc/foo|'", "bar", "bicep/abc/foo/", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br:mcr.microsoft.com/bicep/abc/foo|", "bar", "bicep/abc/foo/", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br/public:abc/foo|'", "bar", "abc/foo/", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br/public:abc/foo|", "bar", "abc/foo/", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("using 'br:mcr.microsoft.com/bicep/abc/foo|'", "bar", "bicep/abc/foo/", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - [DataRow("using 'br:mcr.microsoft.com/bicep/abc/foo|", "bar", "bicep/abc/foo/", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - [DataRow("using 'br/public:abc/foo|'", "bar", "abc/foo/", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - [DataRow("using 'br/public:abc/foo|", "bar", "abc/foo/", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - public async Task Public_registry_module_completions_support_prefix_matching(string text, string expectedLabelForFoo, string expectedLabelDescriptionForFoo, string expectedInsertTextForFoo, BicepSourceFileKind kind) + [DataRow("module test 'br:mcr.microsoft.com/bicep/abc/foo|'", "bicep/abc/foo/bar", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br:mcr.microsoft.com/bicep/abc/foo|", "bicep/abc/foo/bar", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br/public:abc/foo|'", "abc/foo/bar", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br/public:abc/foo|", "abc/foo/bar", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("using 'br:mcr.microsoft.com/bicep/abc/foo|'", "bicep/abc/foo/bar", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + [DataRow("using 'br:mcr.microsoft.com/bicep/abc/foo|", "bicep/abc/foo/bar", "'br:mcr.microsoft.com/bicep/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + [DataRow("using 'br/public:abc/foo|'", "abc/foo/bar", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + [DataRow("using 'br/public:abc/foo|", "abc/foo/bar", "'br/public:abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + public async Task Public_registry_module_completions_support_prefix_matching(string text, string expectedLabelForFoo, string expectedInsertTextForFoo, BicepSourceFileKind kind) { var extension = kind == BicepSourceFileKind.ParamsFile ? "bicepparam" : "bicep"; var (fileText, cursor) = ParserHelper.GetFileWithSingleCursor(text, '|'); @@ -4580,32 +4580,28 @@ public async Task Public_registry_module_completions_support_prefix_matching(str var completions = await file.RequestAndResolveCompletions(cursor); completions.Count().Should().Be(2); - completions.Select(x => (Label: x.Label, LabelDetails: x.LabelDetails, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( + completions.Select(x => (Label: x.Label, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( c => { c.Label.Should().Be(expectedLabelForFoo); - c.LabelDetails.Should().NotBeNull(); - c.LabelDetails!.Description.Should().Be(expectedLabelDescriptionForFoo); c.InsertText.Should().Be(expectedInsertTextForFoo); }, c => { c.Label.Should().Be(expectedLabelForFoo.Replace("foo/", "food/")); - c.LabelDetails.Should().NotBeNull(); - c.LabelDetails!.Description.Should().Be(expectedLabelDescriptionForFoo.Replace("foo/", "food/")); c.InsertText.Should().Be(expectedInsertTextForFoo.Replace("foo/", "food/")); } ); } [TestMethod] - [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|'", "bar", "bicep/whatever/abc/foo/", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|", "bar", "bicep/whatever/abc/foo/", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br/myRegistry:abc/foo|'", "bar", "abc/foo/", "'br/myRegistry:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br/myRegistry_noPath:bicep/whatever/abc/foo|", "bar", "bicep/whatever/abc/foo/", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] - [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|'", "bar", "bicep/whatever/abc/foo/", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - [DataRow("module test 'br/myRegistry_noPath:bicep/whatever/abc/foo|", "bar", "bicep/whatever/abc/foo/", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] - public async Task Private_registry_completions_support_prefix_matching(string text, string expectedLabelForFoo, string expectedLabelDescriptionForFoo, string expectedInsertTextForFoo, BicepSourceFileKind kind) + [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|'", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br/myRegistry:abc/foo|'", "abc/foo/bar", "'br/myRegistry:abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br/myRegistry_noPath:bicep/whatever/abc/foo|", "bicep/whatever/abc/foo/bar", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br:registry.contoso.io/bicep/whatever/abc/foo|'", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + [DataRow("module test 'br/myRegistry_noPath:bicep/whatever/abc/foo|", "bicep/whatever/abc/foo/bar", "'br/myRegistry_noPath:bicep/whatever/abc/foo/bar:$0'", BicepSourceFileKind.ParamsFile)] + public async Task Private_registry_completions_support_prefix_matching(string text, string expectedLabelForFoo, string expectedInsertTextForFoo, BicepSourceFileKind kind) { var extension = kind == BicepSourceFileKind.ParamsFile ? "bicepparam" : "bicep"; var (fileText, cursor) = ParserHelper.GetFileWithSingleCursor(text, '|'); @@ -4658,29 +4654,25 @@ public async Task Private_registry_completions_support_prefix_matching(string te var completions = await file.RequestAndResolveCompletions(cursor); completions.Count().Should().Be(2); - completions.Select(x => (Label: x.Label, LabelDetails: x.LabelDetails, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( + completions.Select(x => (Label: x.Label, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( c => { c.Label.Should().Be(expectedLabelForFoo); - c.LabelDetails.Should().NotBeNull(); - c.LabelDetails!.Description.Should().Be(expectedLabelDescriptionForFoo); c.InsertText.Should().Be(expectedInsertTextForFoo); }, c => { c.Label.Should().Be(expectedLabelForFoo.Replace("foo/", "food/")); - c.LabelDetails.Should().NotBeNull(); - c.LabelDetails!.Description.Should().Be(expectedLabelDescriptionForFoo.Replace("foo/", "food/")); c.InsertText.Should().Be(expectedInsertTextForFoo.Replace("foo/", "food/")); } ); } [TestMethod] - [DataRow("module test 'br/ms:bicep/app/|'", "dapr-containerapp", "bicep/app/", "'br/ms:bicep/app/dapr-containerapp:$0'")] - [DataRow("module test 'br/ms_empty:bicep/app/|'", "dapr-containerapp", "bicep/app/", "'br/ms_empty:bicep/app/dapr-containerapp:$0'")] - [DataRow("module test 'br/ms_bicep:app/|'", "dapr-containerapp", "app/", "'br/ms_bicep:app/dapr-containerapp:$0'")] - public async Task Public_registry_via_alias_supports_completions(string text, string expectedLabel, string expectedLabelDescription, string expectedInsertText) + [DataRow("module test 'br/ms:bicep/app/|'", "bicep/app/dapr-containerapp", "'br/ms:bicep/app/dapr-containerapp:$0'")] + [DataRow("module test 'br/ms_empty:bicep/app/|'", "bicep/app/dapr-containerapp", "'br/ms_empty:bicep/app/dapr-containerapp:$0'")] + [DataRow("module test 'br/ms_bicep:app/|'", "app/dapr-containerapp", "'br/ms_bicep:app/dapr-containerapp:$0'")] + public async Task Public_registry_via_alias_supports_completions(string text, string expectedLabel, string expectedInsertText) { var (fileText, cursor) = ParserHelper.GetFileWithSingleCursor(text, '|'); var baseFolder = $"{Guid.NewGuid():D}"; @@ -4732,12 +4724,10 @@ public async Task Public_registry_via_alias_supports_completions(string text, st var completions = await file.RequestAndResolveCompletions(cursor); completions.Count().Should().Be(1); - completions.Select(x => (Label: x.Label, LabelDetails: x.LabelDetails, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( + completions.Select(x => (Label: x.Label, InsertText: x.TextEdit!.TextEdit!.NewText)).Should().SatisfyRespectively( c => { c.Label.Should().Be(expectedLabel); - c.LabelDetails.Should().NotBeNull(); - c.LabelDetails!.Description.Should().Be(expectedLabelDescription); c.InsertText.Should().Be(expectedInsertText); } ); From e3d01a07465c9bd90ca468a60d451fc3319e6e9d Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:54:43 +0000 Subject: [PATCH 06/19] improve completion item documentation with detailed display names, paths, versions, and descriptions --- src/Bicep.LangServer.IntegrationTests/CompletionTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index 3894e8680eb..66e0f61ea2a 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -4460,7 +4460,7 @@ public async Task Public_module_version_completions(string inputWithCursors, Bic first.SortText.Should().Be("0000"); first.Kind.Should().Be(CompletionItemKind.Snippet); first.Detail.Should().Be("d1"); - first.Documentation!.MarkupContent!.Value.Should().Be("[View Documentation](contoso.com/help1)"); + first.Documentation!.MarkupContent!.Value.Should().Be("**Display name:** 1.0.2 \n**Full module path:** bicep/app/dapr-containerapp \n**Version:** 1.0.2 \n**Description:** d1 \n[View Documentation](contoso.com/help1)"); }, second => { @@ -4468,7 +4468,7 @@ public async Task Public_module_version_completions(string inputWithCursors, Bic second.SortText.Should().Be("0001"); second.Kind.Should().Be(CompletionItemKind.Snippet); second.Detail.Should().BeNull(); - second.Documentation.Should().BeNull(); + second.Documentation!.MarkupContent!.Value.Should().Be("**Display name:** 1.0.1 \n**Full module path:** bicep/app/dapr-containerapp \n**Version:** 1.0.1 \n**Description:** N/A \n**Documentation:** N/A"); } ); } @@ -4533,7 +4533,7 @@ public async Task Private_module_version_completions(string inputWithCursors, Bi first.SortText.Should().Be("0000"); first.Kind.Should().Be(CompletionItemKind.Snippet); first.Detail.Should().Be("d101"); - first.Documentation!.MarkupContent!.Value.Should().Be("[View Documentation](contoso.com/help/d101.html)"); + first.Documentation!.MarkupContent!.Value.Should().Be("**Display name:** v101 \n**Full module path:** app/private-app \n**Version:** v101 \n**Description:** d101 \n[View Documentation](contoso.com/help/d101.html)"); }, second => { @@ -4541,7 +4541,7 @@ public async Task Private_module_version_completions(string inputWithCursors, Bi second.SortText.Should().Be("0001"); second.Kind.Should().Be(CompletionItemKind.Snippet); second.Detail.Should().Be("d100"); - second.Documentation!.MarkupContent!.Value.Should().Be("[View Documentation](contoso.com/help/d100.html)"); + second.Documentation!.MarkupContent!.Value.Should().Be("**Display name:** v100 \n**Full module path:** app/private-app \n**Version:** v100 \n**Description:** d100 \n[View Documentation](contoso.com/help/d100.html)"); } ); } From 9c2af8f54688068a163d9196c313b7dc05de1b19 Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:43:15 +0000 Subject: [PATCH 07/19] add AVM module display name provider and integrate into completion services --- .../AvmModuleDisplayNameProvider.cs | 236 ++++++++++++++++++ .../ModuleReferenceCompletionProvider.cs | 47 +++- .../IServiceCollectionExtensions.cs | 17 +- src/Bicep.LangServer/Server.cs | 4 + 4 files changed, 292 insertions(+), 12 deletions(-) create mode 100644 src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs diff --git a/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs b/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs new file mode 100644 index 00000000000..5a3ac0df4e7 --- /dev/null +++ b/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using Bicep.Core.Features; +using Microsoft.VisualBasic.FileIO; + +namespace Bicep.LanguageServer.Completions +{ + public interface IAvmModuleDisplayNameProvider + { + void StartCache(); + + bool TryGetModuleDisplayName(string modulePath, [NotNullWhen(true)] out string? displayName); + } + + public class AvmModuleDisplayNameProvider : IAvmModuleDisplayNameProvider + { + private static readonly Uri UtilityModulesCsvUri = new("https://azure.github.io/Azure-Verified-Modules/module-indexes/BicepUtilityModules.csv"); + private static readonly Uri PatternModulesCsvUri = new("https://azure.github.io/Azure-Verified-Modules/module-indexes/BicepPatternModules.csv"); + private static readonly Uri ResourceModulesCsvUri = new("https://azure.github.io/Azure-Verified-Modules/module-indexes/BicepResourceModules.csv"); + + private static readonly AvmDisplayNameLookup EmptyLookup = new([], [], []); + + private readonly IAvmModuleCsvIndexHttpClient client; + private readonly object startLock = new(); + + private volatile AvmDisplayNameLookup lookup = EmptyLookup; + private Task? loadTask; + + public AvmModuleDisplayNameProvider(IAvmModuleCsvIndexHttpClient client) + { + this.client = client; + } + + public void StartCache() + { + lock (startLock) + { + loadTask ??= Task.Run(LoadAsync); + } + } + + public bool TryGetModuleDisplayName(string modulePath, [NotNullWhen(true)] out string? displayName) + { + displayName = null; + + if (TryGetNormalizedModuleName(modulePath) is not { } normalizedModuleName) + { + return false; + } + + var currentLookup = lookup; + if (normalizedModuleName.StartsWith("avm/utl/", StringComparison.Ordinal)) + { + return currentLookup.Utility.TryGetValue(normalizedModuleName, out displayName); + } + + if (normalizedModuleName.StartsWith("avm/ptn/", StringComparison.Ordinal)) + { + return currentLookup.Pattern.TryGetValue(normalizedModuleName, out displayName); + } + + if (normalizedModuleName.StartsWith("avm/res/", StringComparison.Ordinal)) + { + return currentLookup.Resource.TryGetValue(normalizedModuleName, out displayName); + } + + return false; + } + + private async Task LoadAsync() + { + try + { + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + + var utilityTask = LoadCsvAsync(UtilityModulesCsvUri, timeoutCts.Token); + var patternTask = LoadCsvAsync(PatternModulesCsvUri, timeoutCts.Token); + var resourceTask = LoadCsvAsync(ResourceModulesCsvUri, timeoutCts.Token); + + await Task.WhenAll(utilityTask, patternTask, resourceTask); + + var utility = await utilityTask; + var pattern = await patternTask; + var resource = await resourceTask; + + lookup = new AvmDisplayNameLookup( + Utility: utility, + Pattern: pattern, + Resource: resource); + + Trace.WriteLineIf( + FeatureProvider.TracingEnabled, + string.Format( + "{0}: AVM display-name cache loaded (utility={1}, pattern={2}, resource={3}).", + nameof(AvmModuleDisplayNameProvider), + utility.Count, + pattern.Count, + resource.Count)); + } + catch (Exception exception) + { + lookup = EmptyLookup; + + Trace.WriteLineIf( + FeatureProvider.TracingEnabled, + string.Format( + "{0}: Failed to preload AVM display-name cache. Falling back to empty cache. Error: {1}", + nameof(AvmModuleDisplayNameProvider), + exception.Message)); + } + } + + private async Task> LoadCsvAsync(Uri csvUri, CancellationToken cancellationToken) + { + var csvContent = await client.GetCsvAsync(csvUri, cancellationToken); + using var reader = new StringReader(csvContent); + using var parser = new TextFieldParser(reader) + { + TextFieldType = FieldType.Delimited, + HasFieldsEnclosedInQuotes = true, + TrimWhiteSpace = false, + }; + + parser.SetDelimiters(","); + + if (parser.EndOfData) + { + return []; + } + + var headers = parser.ReadFields(); + if (headers is null) + { + return []; + } + + var moduleNameIndex = Array.FindIndex(headers, x => string.Equals(x, "ModuleName", StringComparison.Ordinal)); + var moduleDisplayNameIndex = Array.FindIndex(headers, x => string.Equals(x, "ModuleDisplayName", StringComparison.Ordinal)); + if (moduleNameIndex == -1 || moduleDisplayNameIndex == -1) + { + return []; + } + + var entries = new Dictionary(StringComparer.Ordinal); + while (!parser.EndOfData) + { + var fields = parser.ReadFields(); + if (fields is null || + moduleNameIndex >= fields.Length || + moduleDisplayNameIndex >= fields.Length) + { + continue; + } + + if (TryGetNormalizedModuleName(fields[moduleNameIndex]) is not { } normalizedModuleName) + { + continue; + } + + var moduleDisplayName = fields[moduleDisplayNameIndex]?.Trim(); + if (string.IsNullOrWhiteSpace(moduleDisplayName)) + { + continue; + } + + entries.TryAdd(normalizedModuleName, moduleDisplayName); + } + + return entries.ToImmutableDictionary(StringComparer.Ordinal); + } + + private static string? TryGetNormalizedModuleName(string? modulePath) + { + if (string.IsNullOrWhiteSpace(modulePath)) + { + return null; + } + + var normalized = modulePath.Trim().Replace('\\', '/'); + if (normalized.StartsWith("bicep/", StringComparison.Ordinal)) + { + normalized = normalized["bicep/".Length..]; + } + + return normalized; + } + + private sealed record AvmDisplayNameLookup( + ImmutableDictionary Utility, + ImmutableDictionary Pattern, + ImmutableDictionary Resource); + } + + public interface IAvmModuleCsvIndexHttpClient + { + Task GetCsvAsync(Uri csvUri, CancellationToken cancellationToken); + } + + public class AvmModuleCsvIndexHttpClient : IAvmModuleCsvIndexHttpClient + { + private readonly HttpClient client; + + public AvmModuleCsvIndexHttpClient(HttpClient client) + { + this.client = client; + } + + public async Task GetCsvAsync(Uri csvUri, CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(HttpMethod.Get, csvUri); + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStringAsync(cancellationToken); + } + } + + public class NullAvmModuleDisplayNameProvider : IAvmModuleDisplayNameProvider + { + public static readonly NullAvmModuleDisplayNameProvider Instance = new(); + + public void StartCache() + { + } + + public bool TryGetModuleDisplayName(string modulePath, [NotNullWhen(true)] out string? displayName) + { + displayName = null; + return false; + } + } +} diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index eeb72eb9983..3b3a293ccf5 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -39,6 +39,7 @@ public partial class ModuleReferenceCompletionProvider : IModuleReferenceComplet private readonly ISettingsProvider settingsProvider; private readonly ITelemetryProvider telemetryProvider; private readonly RegistryConfiguration registryConfiguration; + private readonly IAvmModuleDisplayNameProvider avmModuleDisplayNameProvider; private enum ModuleCompletionPriority { @@ -146,13 +147,15 @@ public ModuleReferenceCompletionProvider( IRegistryModuleCatalog registryModuleCatalog, ISettingsProvider settingsProvider, ITelemetryProvider telemetryProvider, - RegistryConfiguration registryConfiguration) + RegistryConfiguration registryConfiguration, + IAvmModuleDisplayNameProvider? avmModuleDisplayNameProvider = null) { this.azureContainerRegistriesProvider = azureContainerRegistriesProvider; this.registryModuleCatalog = registryModuleCatalog; this.settingsProvider = settingsProvider; this.telemetryProvider = telemetryProvider; this.registryConfiguration = registryConfiguration; + this.avmModuleDisplayNameProvider = avmModuleDisplayNameProvider ?? NullAvmModuleDisplayNameProvider.Instance; } public async Task> GetFilteredCompletions(BicepSourceFile sourceFile, BicepCompletionContext context, CancellationToken cancellationToken) @@ -670,9 +673,9 @@ private async Task ResolveVersionCompletionItem(CompletionItem c return (completionItem with { - Detail = metadata.Details.Description, + Detail = GetCompletionTitle(registry, modulePath, metadata.Details.Description), }) - .WithDocumentation(GetVersionCompletionDocumentation(completionItem.Label, modulePath, version, metadata.Details)); + .WithDocumentation(GetVersionCompletionDocumentation(modulePath, version, metadata.Details)); } return completionItem; @@ -693,8 +696,8 @@ private async Task ResolveModuleCompletionItem(CompletionItem co return (completionItem with { - Detail = details.Description, - }).WithDocumentation(GetModuleCompletionDocumentation(completionItem.Label, modulePath, details)); + Detail = GetCompletionTitle(registry, modulePath, details.Description), + }).WithDocumentation(GetModuleCompletionDocumentation(modulePath, details)); } return completionItem; @@ -739,12 +742,24 @@ private static string GetSortText(string label, ModuleCompletionPriority priorit return $"9{(int)priority}_{label}"; } - private static string GetModuleCompletionDocumentation(string displayName, string modulePath, RegistryMetadataDetails details) + private string? GetCompletionTitle(string registry, string modulePath, string? defaultTitle) { + if (registry.Equals(LanguageConstants.BicepPublicMcrRegistry, StringComparison.Ordinal) + && avmModuleDisplayNameProvider.TryGetModuleDisplayName(modulePath, out var moduleDisplayName)) + { + return moduleDisplayName; + } + + return defaultTitle; + } + + private static string GetModuleCompletionDocumentation(string modulePath, RegistryMetadataDetails details) + { + var displayModulePath = GetDisplayModulePath(modulePath); + var sections = new List { - $"**Display name:** {displayName}", - $"**Full module path:** {modulePath}", + $"**Full module path:** {displayModulePath}", $"**Description:** {details.Description ?? "N/A"}", }; @@ -760,12 +775,13 @@ private static string GetModuleCompletionDocumentation(string displayName, strin return MarkdownHelper.JoinWithNewlines(sections); } - private static string GetVersionCompletionDocumentation(string displayName, string modulePath, string version, RegistryMetadataDetails details) + private static string GetVersionCompletionDocumentation(string modulePath, string version, RegistryMetadataDetails details) { + var displayModulePath = GetDisplayModulePath(modulePath); + var sections = new List { - $"**Display name:** {displayName}", - $"**Full module path:** {modulePath}", + $"**Full module path:** {displayModulePath}", $"**Version:** {version}", $"**Description:** {details.Description ?? "N/A"}", }; @@ -781,5 +797,14 @@ private static string GetVersionCompletionDocumentation(string displayName, stri return MarkdownHelper.JoinWithNewlines(sections); } + + private static string GetDisplayModulePath(string modulePath) + { + const string publicRegistryPrefix = LanguageConstants.BicepPublicMcrPathPrefix; + + return modulePath.StartsWith(publicRegistryPrefix, StringComparison.Ordinal) + ? modulePath[publicRegistryPrefix.Length..] + : modulePath; + } } } diff --git a/src/Bicep.LangServer/IServiceCollectionExtensions.cs b/src/Bicep.LangServer/IServiceCollectionExtensions.cs index f73cd521706..2ae9214a217 100644 --- a/src/Bicep.LangServer/IServiceCollectionExtensions.cs +++ b/src/Bicep.LangServer/IServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.IO.Abstractions; +using System.Net; using Bicep.Core; using Bicep.Core.Analyzers.Interfaces; using Bicep.Core.Analyzers.Linter; @@ -58,9 +59,23 @@ BicepLangServerOptions bicepLangServerOptions .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(bicepLangServerOptions) - .AddSingleton(); + .AddSingleton() + .AddAvmModuleDisplayNameServices(); + + private static IServiceCollection AddAvmModuleDisplayNameServices(this IServiceCollection services) + { + services + .AddHttpClient() + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + }); + + return services; + } } diff --git a/src/Bicep.LangServer/Server.cs b/src/Bicep.LangServer/Server.cs index 4bb26e13f74..aa95053bb00 100644 --- a/src/Bicep.LangServer/Server.cs +++ b/src/Bicep.LangServer/Server.cs @@ -9,6 +9,7 @@ using Bicep.Core.Tracing; using Bicep.Core.Utils; using Bicep.LanguageServer.Features.Custom.Visualization; +using Bicep.LanguageServer.Completions; using Bicep.LanguageServer.Handlers; using Bicep.LanguageServer.Options; using Bicep.LanguageServer.Providers; @@ -97,6 +98,9 @@ public async Task RunAsync(CancellationToken cancellationToken) var scheduler = server.GetRequiredService(); scheduler.Start(); + var avmDisplayNameProvider = server.GetRequiredService(); + avmDisplayNameProvider.StartCache(); + #pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks await server.WaitForExit; #pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks From 4387a1d3d158770593af7831a8473ac327ad35bf Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:43:29 +0000 Subject: [PATCH 08/19] refactor completion item documentation to improve readability by removing display names and simplifying paths --- .../CompletionTests.cs | 8 +-- .../ModuleReferenceCompletionProviderTests.cs | 61 +++++++++++++++---- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index 66e0f61ea2a..cacc03d8fce 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -4460,7 +4460,7 @@ public async Task Public_module_version_completions(string inputWithCursors, Bic first.SortText.Should().Be("0000"); first.Kind.Should().Be(CompletionItemKind.Snippet); first.Detail.Should().Be("d1"); - first.Documentation!.MarkupContent!.Value.Should().Be("**Display name:** 1.0.2 \n**Full module path:** bicep/app/dapr-containerapp \n**Version:** 1.0.2 \n**Description:** d1 \n[View Documentation](contoso.com/help1)"); + first.Documentation!.MarkupContent!.Value.Should().Be("**Full module path:** app/dapr-containerapp \n**Version:** 1.0.2 \n**Description:** d1 \n[View Documentation](contoso.com/help1)"); }, second => { @@ -4468,7 +4468,7 @@ public async Task Public_module_version_completions(string inputWithCursors, Bic second.SortText.Should().Be("0001"); second.Kind.Should().Be(CompletionItemKind.Snippet); second.Detail.Should().BeNull(); - second.Documentation!.MarkupContent!.Value.Should().Be("**Display name:** 1.0.1 \n**Full module path:** bicep/app/dapr-containerapp \n**Version:** 1.0.1 \n**Description:** N/A \n**Documentation:** N/A"); + second.Documentation!.MarkupContent!.Value.Should().Be("**Full module path:** app/dapr-containerapp \n**Version:** 1.0.1 \n**Description:** N/A \n**Documentation:** N/A"); } ); } @@ -4533,7 +4533,7 @@ public async Task Private_module_version_completions(string inputWithCursors, Bi first.SortText.Should().Be("0000"); first.Kind.Should().Be(CompletionItemKind.Snippet); first.Detail.Should().Be("d101"); - first.Documentation!.MarkupContent!.Value.Should().Be("**Display name:** v101 \n**Full module path:** app/private-app \n**Version:** v101 \n**Description:** d101 \n[View Documentation](contoso.com/help/d101.html)"); + first.Documentation!.MarkupContent!.Value.Should().Be("**Full module path:** app/private-app \n**Version:** v101 \n**Description:** d101 \n[View Documentation](contoso.com/help/d101.html)"); }, second => { @@ -4541,7 +4541,7 @@ public async Task Private_module_version_completions(string inputWithCursors, Bi second.SortText.Should().Be("0001"); second.Kind.Should().Be(CompletionItemKind.Snippet); second.Detail.Should().Be("d100"); - second.Documentation!.MarkupContent!.Value.Should().Be("**Display name:** v100 \n**Full module path:** app/private-app \n**Version:** v100 \n**Description:** d100 \n[View Documentation](contoso.com/help/d100.html)"); + second.Documentation!.MarkupContent!.Value.Should().Be("**Full module path:** app/private-app \n**Version:** v100 \n**Description:** d100 \n[View Documentation](contoso.com/help/d100.html)"); } ); } diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index d13bba02d49..912eddfbeee 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -536,8 +536,8 @@ public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionCo c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.InsertText.Should().BeNull(); c.Detail.Should().BeNull(); - c.Documentation!.MarkupContent!.Value.Should().Contain($"**Display name:** {expectedLabel1}"); - c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** bicep/app/dapr-cntrapp1"); + c.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); + c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** app/dapr-cntrapp1"); c.Documentation.MarkupContent.Value.Should().Contain("**Description:** N/A"); c.Documentation.MarkupContent.Value.Should().Contain("**Documentation:** N/A"); c.TextEdit!.TextEdit!.NewText.Should().Be(expectedCompletionText1); @@ -553,8 +553,8 @@ public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionCo c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.InsertText.Should().BeNull(); c.Detail.Should().Be("description2"); - c.Documentation!.MarkupContent!.Value.Should().Contain($"**Display name:** {expectedLabel2}"); - c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** bicep/app/dapr-cntrapp2"); + c.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); + c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** app/dapr-cntrapp2"); c.Documentation.MarkupContent.Value.Should().Contain("**Description:** description2"); c.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help2)"); c.TextEdit!.TextEdit!.NewText.Should().Be(expectedCompletionText2); @@ -565,6 +565,43 @@ public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionCo }); } + [TestMethod] + public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_UsesModuleDisplayNameForTooltipTitle() + { + var catalog = RegistryCatalogMocks.CreateCatalogWithMocks( + RegistryCatalogMocks.MockPublicMetadataProvider([ + new("bicep/avm/ptn/ai-platform/baseline", "module description", "contoso.com/help", []), + ]) + ); + + var avmDisplayNameProviderMock = StrictMock.Of(); + string? moduleDisplayName = "AI platform baseline"; + avmDisplayNameProviderMock + .Setup(x => x.TryGetModuleDisplayName("bicep/avm/ptn/ai-platform/baseline", out moduleDisplayName)) + .Returns(true); + + var (completionContext, sourceFile) = GetBicepCompletionContext("module test 'br/public:avm/ptn/ai-platform/|'"); + var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( + azureContainerRegistriesProvider, + catalog, + settingsProvider, + BicepTestConstants.CreateMockTelemetryProvider().Object, + avmDisplayNameProviderMock.Object); + var completions = await GetAndResolveCompletionItems(sourceFile, completionContext, moduleReferenceCompletionProvider); + + completions.Should().SatisfyRespectively( + x => + { + x.Label.Should().Be("avm/ptn/ai-platform/baseline"); + x.Detail.Should().Be(moduleDisplayName); + x.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); + x.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** avm/ptn/ai-platform/baseline"); + x.Documentation.MarkupContent.Value.Should().Contain("**Description:** module description"); + }); + + avmDisplayNameProviderMock.VerifyAll(); + } + [DataTestMethod] [DataRow("module test 'br:registry.contoso.io/bicep/|'", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] [DataRow("module test 'br:registry.contoso.io/bicep/|", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] @@ -616,8 +653,8 @@ public async Task GetFilteredCompletions_WithPrivateModulePathCompletions_Return c.Label.Should().Be(expectedLabel); c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.Detail.Should().Be("d1"); - c.Documentation!.MarkupContent!.Value.Should().Contain($"**Display name:** {expectedLabel}"); - c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** bicep/whatever/abc/foo/bar"); + c.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); + c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** whatever/abc/foo/bar"); c.Documentation.MarkupContent.Value.Should().Contain("**Description:** d1"); c.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help1)"); c.TextEdit!.TextEdit!.NewText.Should().Be(expectedCompletionText); @@ -729,8 +766,8 @@ public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_Returns x.InsertText == null && x.SortText == expectedSortText1 && x.Detail == null && - x.Documentation!.MarkupContent!.Value.Contains("**Display name:** 1.0.2") && - x.Documentation.MarkupContent.Value.Contains("**Full module path:** bicep/app/dapr-containerapp") && + !x.Documentation!.MarkupContent!.Value.Contains("**Display name:**") && + x.Documentation.MarkupContent.Value.Contains("**Full module path:** app/dapr-containerapp") && x.Documentation.MarkupContent.Value.Contains("**Version:** 1.0.2") && x.Documentation.MarkupContent.Value.Contains("**Description:** N/A") && x.Documentation.MarkupContent.Value.Contains("**Documentation:** N/A") && @@ -746,8 +783,8 @@ public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_Returns x.InsertText == null && x.SortText == expectedSortText2 && x.Detail == "d2" && - x.Documentation!.MarkupContent!.Value.Contains("**Display name:** 1.0.1") && - x.Documentation.MarkupContent.Value.Contains("**Full module path:** bicep/app/dapr-containerapp") && + !x.Documentation!.MarkupContent!.Value.Contains("**Display name:**") && + x.Documentation.MarkupContent.Value.Contains("**Full module path:** app/dapr-containerapp") && x.Documentation.MarkupContent.Value.Contains("**Version:** 1.0.1") && x.Documentation.MarkupContent.Value.Contains("**Description:** d2") && x.Documentation.MarkupContent.Value.Contains("[View Documentation](contoso.com/help%20page.html)") && @@ -880,8 +917,8 @@ public async Task GetFilteredCompletions_WithAliasForMCRInBicepConfigAndModulePa x.Kind.Should().Be(CompletionItemKind.Snippet); x.InsertText.Should().BeNull(); x.Detail.Should().Be("dapr description"); - x.Documentation!.MarkupContent!.Value.Should().Contain($"**Display name:** {expectedLabel}"); - x.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** bicep/app/dapr-containerapp"); + x.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); + x.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** app/dapr-containerapp"); x.Documentation.MarkupContent.Value.Should().Contain("**Description:** dapr description"); x.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help)"); From 06d0ec3a806bf518f9c8ff4533b0b41f8e1b861b Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:05:22 +0000 Subject: [PATCH 09/19] refactor completion item documentation to enhance readability by reordering module path and version information --- src/Bicep.LangServer.IntegrationTests/CompletionTests.cs | 8 ++++---- .../Completions/ModuleReferenceCompletionProviderTests.cs | 4 ++-- .../Completions/ModuleReferenceCompletionProvider.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index cacc03d8fce..2687f624d71 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -4460,7 +4460,7 @@ public async Task Public_module_version_completions(string inputWithCursors, Bic first.SortText.Should().Be("0000"); first.Kind.Should().Be(CompletionItemKind.Snippet); first.Detail.Should().Be("d1"); - first.Documentation!.MarkupContent!.Value.Should().Be("**Full module path:** app/dapr-containerapp \n**Version:** 1.0.2 \n**Description:** d1 \n[View Documentation](contoso.com/help1)"); + first.Documentation!.MarkupContent!.Value.Should().Be("**Version:** 1.0.2 \n**Full module path:** app/dapr-containerapp \n**Description:** d1 \n[View Documentation](contoso.com/help1)"); }, second => { @@ -4468,7 +4468,7 @@ public async Task Public_module_version_completions(string inputWithCursors, Bic second.SortText.Should().Be("0001"); second.Kind.Should().Be(CompletionItemKind.Snippet); second.Detail.Should().BeNull(); - second.Documentation!.MarkupContent!.Value.Should().Be("**Full module path:** app/dapr-containerapp \n**Version:** 1.0.1 \n**Description:** N/A \n**Documentation:** N/A"); + second.Documentation!.MarkupContent!.Value.Should().Be("**Version:** 1.0.1 \n**Full module path:** app/dapr-containerapp \n**Description:** N/A \n**Documentation:** N/A"); } ); } @@ -4533,7 +4533,7 @@ public async Task Private_module_version_completions(string inputWithCursors, Bi first.SortText.Should().Be("0000"); first.Kind.Should().Be(CompletionItemKind.Snippet); first.Detail.Should().Be("d101"); - first.Documentation!.MarkupContent!.Value.Should().Be("**Full module path:** app/private-app \n**Version:** v101 \n**Description:** d101 \n[View Documentation](contoso.com/help/d101.html)"); + first.Documentation!.MarkupContent!.Value.Should().Be("**Version:** v101 \n**Full module path:** app/private-app \n**Description:** d101 \n[View Documentation](contoso.com/help/d101.html)"); }, second => { @@ -4541,7 +4541,7 @@ public async Task Private_module_version_completions(string inputWithCursors, Bi second.SortText.Should().Be("0001"); second.Kind.Should().Be(CompletionItemKind.Snippet); second.Detail.Should().Be("d100"); - second.Documentation!.MarkupContent!.Value.Should().Be("**Full module path:** app/private-app \n**Version:** v100 \n**Description:** d100 \n[View Documentation](contoso.com/help/d100.html)"); + second.Documentation!.MarkupContent!.Value.Should().Be("**Version:** v100 \n**Full module path:** app/private-app \n**Description:** d100 \n[View Documentation](contoso.com/help/d100.html)"); } ); } diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index 912eddfbeee..13185f91ffc 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -767,8 +767,8 @@ public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_Returns x.SortText == expectedSortText1 && x.Detail == null && !x.Documentation!.MarkupContent!.Value.Contains("**Display name:**") && - x.Documentation.MarkupContent.Value.Contains("**Full module path:** app/dapr-containerapp") && x.Documentation.MarkupContent.Value.Contains("**Version:** 1.0.2") && + x.Documentation.MarkupContent.Value.Contains("**Full module path:** app/dapr-containerapp") && x.Documentation.MarkupContent.Value.Contains("**Description:** N/A") && x.Documentation.MarkupContent.Value.Contains("**Documentation:** N/A") && x.TextEdit!.TextEdit!.NewText == expectedCompletionText1 && @@ -784,8 +784,8 @@ public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_Returns x.SortText == expectedSortText2 && x.Detail == "d2" && !x.Documentation!.MarkupContent!.Value.Contains("**Display name:**") && - x.Documentation.MarkupContent.Value.Contains("**Full module path:** app/dapr-containerapp") && x.Documentation.MarkupContent.Value.Contains("**Version:** 1.0.1") && + x.Documentation.MarkupContent.Value.Contains("**Full module path:** app/dapr-containerapp") && x.Documentation.MarkupContent.Value.Contains("**Description:** d2") && x.Documentation.MarkupContent.Value.Contains("[View Documentation](contoso.com/help%20page.html)") && x.TextEdit!.TextEdit!.NewText == expectedCompletionText2 && diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 3b3a293ccf5..92241bd4ef5 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -781,8 +781,8 @@ private static string GetVersionCompletionDocumentation(string modulePath, strin var sections = new List { - $"**Full module path:** {displayModulePath}", $"**Version:** {version}", + $"**Full module path:** {displayModulePath}", $"**Description:** {details.Description ?? "N/A"}", }; From 41f504d8bbca852c53c3bcfb23be583270c2d8be Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:10:50 +0000 Subject: [PATCH 10/19] refactor completion item documentation to improve readability by removing detail fields and enhancing section formatting --- .../CompletionTests.cs | 12 ++--- .../ModuleReferenceCompletionProviderTests.cs | 17 +++++-- .../ModuleReferenceCompletionProvider.cs | 47 +++++++++++-------- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index 2687f624d71..510466c2ce3 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -4459,8 +4459,8 @@ public async Task Public_module_version_completions(string inputWithCursors, Bic first.Label.Should().Be("1.0.2"); first.SortText.Should().Be("0000"); first.Kind.Should().Be(CompletionItemKind.Snippet); - first.Detail.Should().Be("d1"); - first.Documentation!.MarkupContent!.Value.Should().Be("**Version:** 1.0.2 \n**Full module path:** app/dapr-containerapp \n**Description:** d1 \n[View Documentation](contoso.com/help1)"); + first.Detail.Should().BeNull(); + first.Documentation!.MarkupContent!.Value.Should().Be("### d1 \n**Version:** 1.0.2 \n**Full module path:** app/dapr-containerapp \n**Description:** d1 \n[View Documentation](contoso.com/help1)"); }, second => { @@ -4532,16 +4532,16 @@ public async Task Private_module_version_completions(string inputWithCursors, Bi first.Label.Should().Be("v101"); first.SortText.Should().Be("0000"); first.Kind.Should().Be(CompletionItemKind.Snippet); - first.Detail.Should().Be("d101"); - first.Documentation!.MarkupContent!.Value.Should().Be("**Version:** v101 \n**Full module path:** app/private-app \n**Description:** d101 \n[View Documentation](contoso.com/help/d101.html)"); + first.Detail.Should().BeNull(); + first.Documentation!.MarkupContent!.Value.Should().Be("### d101 \n**Version:** v101 \n**Full module path:** app/private-app \n**Description:** d101 \n[View Documentation](contoso.com/help/d101.html)"); }, second => { second.Label.Should().Be("v100"); second.SortText.Should().Be("0001"); second.Kind.Should().Be(CompletionItemKind.Snippet); - second.Detail.Should().Be("d100"); - second.Documentation!.MarkupContent!.Value.Should().Be("**Version:** v100 \n**Full module path:** app/private-app \n**Description:** d100 \n[View Documentation](contoso.com/help/d100.html)"); + second.Detail.Should().BeNull(); + second.Documentation!.MarkupContent!.Value.Should().Be("### d100 \n**Version:** v100 \n**Full module path:** app/private-app \n**Description:** d100 \n[View Documentation](contoso.com/help/d100.html)"); } ); } diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index 13185f91ffc..040118911f6 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -537,6 +537,7 @@ public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionCo c.InsertText.Should().BeNull(); c.Detail.Should().BeNull(); c.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); + c.Documentation.MarkupContent.Value.Should().NotContain("### "); c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** app/dapr-cntrapp1"); c.Documentation.MarkupContent.Value.Should().Contain("**Description:** N/A"); c.Documentation.MarkupContent.Value.Should().Contain("**Documentation:** N/A"); @@ -552,8 +553,9 @@ public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionCo c.Kind.Should().Be(CompletionItemKind.Snippet); c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.InsertText.Should().BeNull(); - c.Detail.Should().Be("description2"); + c.Detail.Should().BeNull(); c.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); + c.Documentation.MarkupContent.Value.Should().Contain("### description2"); c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** app/dapr-cntrapp2"); c.Documentation.MarkupContent.Value.Should().Contain("**Description:** description2"); c.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help2)"); @@ -593,8 +595,9 @@ public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_Uses x => { x.Label.Should().Be("avm/ptn/ai-platform/baseline"); - x.Detail.Should().Be(moduleDisplayName); + x.Detail.Should().BeNull(); x.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); + x.Documentation.MarkupContent.Value.Should().Contain("### AI platform baseline"); x.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** avm/ptn/ai-platform/baseline"); x.Documentation.MarkupContent.Value.Should().Contain("**Description:** module description"); }); @@ -652,8 +655,9 @@ public async Task GetFilteredCompletions_WithPrivateModulePathCompletions_Return { c.Label.Should().Be(expectedLabel); c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); - c.Detail.Should().Be("d1"); + c.Detail.Should().BeNull(); c.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); + c.Documentation.MarkupContent.Value.Should().Contain("### d1"); c.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** whatever/abc/foo/bar"); c.Documentation.MarkupContent.Value.Should().Contain("**Description:** d1"); c.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help1)"); @@ -767,6 +771,7 @@ public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_Returns x.SortText == expectedSortText1 && x.Detail == null && !x.Documentation!.MarkupContent!.Value.Contains("**Display name:**") && + !x.Documentation.MarkupContent.Value.Contains("### ") && x.Documentation.MarkupContent.Value.Contains("**Version:** 1.0.2") && x.Documentation.MarkupContent.Value.Contains("**Full module path:** app/dapr-containerapp") && x.Documentation.MarkupContent.Value.Contains("**Description:** N/A") && @@ -782,8 +787,9 @@ public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_Returns x.Kind == CompletionItemKind.Snippet && x.InsertText == null && x.SortText == expectedSortText2 && - x.Detail == "d2" && + x.Detail == null && !x.Documentation!.MarkupContent!.Value.Contains("**Display name:**") && + x.Documentation.MarkupContent.Value.Contains("### d2") && x.Documentation.MarkupContent.Value.Contains("**Version:** 1.0.1") && x.Documentation.MarkupContent.Value.Contains("**Full module path:** app/dapr-containerapp") && x.Documentation.MarkupContent.Value.Contains("**Description:** d2") && @@ -916,8 +922,9 @@ public async Task GetFilteredCompletions_WithAliasForMCRInBicepConfigAndModulePa x.Label.Should().Be(expectedLabel); x.Kind.Should().Be(CompletionItemKind.Snippet); x.InsertText.Should().BeNull(); - x.Detail.Should().Be("dapr description"); + x.Detail.Should().BeNull(); x.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); + x.Documentation.MarkupContent.Value.Should().Contain("### dapr description"); x.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** app/dapr-containerapp"); x.Documentation.MarkupContent.Value.Should().Contain("**Description:** dapr description"); x.Documentation.MarkupContent.Value.Should().Contain("[View Documentation](contoso.com/help)"); diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 92241bd4ef5..7f47f00ab85 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -671,11 +671,10 @@ private async Task ResolveVersionCompletionItem(CompletionItem c telemetryProvider.PostEvent(BicepTelemetryEvent.ModuleRegistryResolution(ModuleRegistryResolutionType.AcrVersion)); } - return (completionItem with - { - Detail = GetCompletionTitle(registry, modulePath, metadata.Details.Description), - }) - .WithDocumentation(GetVersionCompletionDocumentation(modulePath, version, metadata.Details)); + var title = GetCompletionTitle(registry, modulePath, metadata.Details.Description); + + return completionItem + .WithDocumentation(GetVersionCompletionDocumentation(title, modulePath, version, metadata.Details)); } return completionItem; @@ -694,10 +693,10 @@ private async Task ResolveModuleCompletionItem(CompletionItem co telemetryProvider.PostEvent(BicepTelemetryEvent.ModuleRegistryResolution(ModuleRegistryResolutionType.AcrModulePath)); } - return (completionItem with - { - Detail = GetCompletionTitle(registry, modulePath, details.Description), - }).WithDocumentation(GetModuleCompletionDocumentation(modulePath, details)); + var title = GetCompletionTitle(registry, modulePath, details.Description); + + return completionItem + .WithDocumentation(GetModuleCompletionDocumentation(title, modulePath, details)); } return completionItem; @@ -753,15 +752,19 @@ private static string GetSortText(string label, ModuleCompletionPriority priorit return defaultTitle; } - private static string GetModuleCompletionDocumentation(string modulePath, RegistryMetadataDetails details) + private static string GetModuleCompletionDocumentation(string? title, string modulePath, RegistryMetadataDetails details) { var displayModulePath = GetDisplayModulePath(modulePath); - var sections = new List + var sections = new List(); + + if (title is not null) { - $"**Full module path:** {displayModulePath}", - $"**Description:** {details.Description ?? "N/A"}", - }; + sections.Add($"### {title}"); + } + + sections.Add($"**Full module path:** {displayModulePath}"); + sections.Add($"**Description:** {details.Description ?? "N/A"}"); if (MarkdownHelper.GetDocumentationLink(details.DocumentationUri) is { } docLink) { @@ -775,16 +778,20 @@ private static string GetModuleCompletionDocumentation(string modulePath, Regist return MarkdownHelper.JoinWithNewlines(sections); } - private static string GetVersionCompletionDocumentation(string modulePath, string version, RegistryMetadataDetails details) + private static string GetVersionCompletionDocumentation(string? title, string modulePath, string version, RegistryMetadataDetails details) { var displayModulePath = GetDisplayModulePath(modulePath); - var sections = new List + var sections = new List(); + + if (title is not null) { - $"**Version:** {version}", - $"**Full module path:** {displayModulePath}", - $"**Description:** {details.Description ?? "N/A"}", - }; + sections.Add($"### {title}"); + } + + sections.Add($"**Version:** {version}"); + sections.Add($"**Full module path:** {displayModulePath}"); + sections.Add($"**Description:** {details.Description ?? "N/A"}"); if (MarkdownHelper.GetDocumentationLink(details.DocumentationUri) is { } docLink) { From abbe7bc706cf351cfc77097bef31640bfb7655af Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:14:03 +0000 Subject: [PATCH 11/19] refactor completion documentation methods to improve readability and consolidate module details --- .../ModuleReferenceCompletionProvider.cs | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 7f47f00ab85..520ad8def2a 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -674,7 +674,7 @@ private async Task ResolveVersionCompletionItem(CompletionItem c var title = GetCompletionTitle(registry, modulePath, metadata.Details.Description); return completionItem - .WithDocumentation(GetVersionCompletionDocumentation(title, modulePath, version, metadata.Details)); + .WithDocumentation(GetCompletionDocumentation(title, modulePath, metadata.Details, version)); } return completionItem; @@ -696,7 +696,7 @@ private async Task ResolveModuleCompletionItem(CompletionItem co var title = GetCompletionTitle(registry, modulePath, details.Description); return completionItem - .WithDocumentation(GetModuleCompletionDocumentation(title, modulePath, details)); + .WithDocumentation(GetCompletionDocumentation(title, modulePath, details)); } return completionItem; @@ -752,7 +752,7 @@ private static string GetSortText(string label, ModuleCompletionPriority priorit return defaultTitle; } - private static string GetModuleCompletionDocumentation(string? title, string modulePath, RegistryMetadataDetails details) + private static string GetCompletionDocumentation(string? title, string modulePath, RegistryMetadataDetails details, string? version = null) { var displayModulePath = GetDisplayModulePath(modulePath); @@ -763,33 +763,11 @@ private static string GetModuleCompletionDocumentation(string? title, string mod sections.Add($"### {title}"); } - sections.Add($"**Full module path:** {displayModulePath}"); - sections.Add($"**Description:** {details.Description ?? "N/A"}"); - - if (MarkdownHelper.GetDocumentationLink(details.DocumentationUri) is { } docLink) - { - sections.Add(docLink); - } - else - { - sections.Add("**Documentation:** N/A"); - } - - return MarkdownHelper.JoinWithNewlines(sections); - } - - private static string GetVersionCompletionDocumentation(string? title, string modulePath, string version, RegistryMetadataDetails details) - { - var displayModulePath = GetDisplayModulePath(modulePath); - - var sections = new List(); - - if (title is not null) + if (version is not null) { - sections.Add($"### {title}"); + sections.Add($"**Version:** {version}"); } - sections.Add($"**Version:** {version}"); sections.Add($"**Full module path:** {displayModulePath}"); sections.Add($"**Description:** {details.Description ?? "N/A"}"); From 7a48b0bd7d6a80b7385abbf1ac0cb5e7dd8b0a8e Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:21:47 +0000 Subject: [PATCH 12/19] refactor AVM module completion to include module status in documentation --- .../ModuleReferenceCompletionProviderTests.cs | 5 ++ .../AvmModuleDisplayNameProvider.cs | 75 +++++++++++++++---- .../ModuleReferenceCompletionProvider.cs | 36 ++++++++- 3 files changed, 98 insertions(+), 18 deletions(-) diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index 040118911f6..f86e96a3f0f 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -581,6 +581,10 @@ public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_Uses avmDisplayNameProviderMock .Setup(x => x.TryGetModuleDisplayName("bicep/avm/ptn/ai-platform/baseline", out moduleDisplayName)) .Returns(true); + string? moduleStatus = "Available"; + avmDisplayNameProviderMock + .Setup(x => x.TryGetModuleStatus("bicep/avm/ptn/ai-platform/baseline", out moduleStatus)) + .Returns(true); var (completionContext, sourceFile) = GetBicepCompletionContext("module test 'br/public:avm/ptn/ai-platform/|'"); var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( @@ -599,6 +603,7 @@ public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_Uses x.Documentation!.MarkupContent!.Value.Should().NotContain("**Display name:**"); x.Documentation.MarkupContent.Value.Should().Contain("### AI platform baseline"); x.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** avm/ptn/ai-platform/baseline"); + x.Documentation.MarkupContent.Value.Should().Contain("**Status:** \uD83D\uDFE2 Available"); x.Documentation.MarkupContent.Value.Should().Contain("**Description:** module description"); }); diff --git a/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs b/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs index 5a3ac0df4e7..ff55486b22a 100644 --- a/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs +++ b/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs @@ -14,6 +14,8 @@ public interface IAvmModuleDisplayNameProvider void StartCache(); bool TryGetModuleDisplayName(string modulePath, [NotNullWhen(true)] out string? displayName); + + bool TryGetModuleStatus(string modulePath, [NotNullWhen(true)] out string? moduleStatus); } public class AvmModuleDisplayNameProvider : IAvmModuleDisplayNameProvider @@ -22,12 +24,12 @@ public class AvmModuleDisplayNameProvider : IAvmModuleDisplayNameProvider private static readonly Uri PatternModulesCsvUri = new("https://azure.github.io/Azure-Verified-Modules/module-indexes/BicepPatternModules.csv"); private static readonly Uri ResourceModulesCsvUri = new("https://azure.github.io/Azure-Verified-Modules/module-indexes/BicepResourceModules.csv"); - private static readonly AvmDisplayNameLookup EmptyLookup = new([], [], []); + private static readonly AvmModuleInfoLookup EmptyLookup = new([], [], []); private readonly IAvmModuleCsvIndexHttpClient client; private readonly object startLock = new(); - private volatile AvmDisplayNameLookup lookup = EmptyLookup; + private volatile AvmModuleInfoLookup lookup = EmptyLookup; private Task? loadTask; public AvmModuleDisplayNameProvider(IAvmModuleCsvIndexHttpClient client) @@ -47,28 +49,52 @@ public bool TryGetModuleDisplayName(string modulePath, [NotNullWhen(true)] out s { displayName = null; + if (TryGetModuleInfo(modulePath) is { } info) + { + displayName = info.DisplayName; + return true; + } + + return false; + } + + public bool TryGetModuleStatus(string modulePath, [NotNullWhen(true)] out string? moduleStatus) + { + moduleStatus = null; + + if (TryGetModuleInfo(modulePath) is { } info && info.ModuleStatus is not null) + { + moduleStatus = info.ModuleStatus; + return true; + } + + return false; + } + + private AvmModuleInfo? TryGetModuleInfo(string modulePath) + { if (TryGetNormalizedModuleName(modulePath) is not { } normalizedModuleName) { - return false; + return null; } var currentLookup = lookup; if (normalizedModuleName.StartsWith("avm/utl/", StringComparison.Ordinal)) { - return currentLookup.Utility.TryGetValue(normalizedModuleName, out displayName); + return currentLookup.Utility.TryGetValue(normalizedModuleName, out var info) ? info : null; } if (normalizedModuleName.StartsWith("avm/ptn/", StringComparison.Ordinal)) { - return currentLookup.Pattern.TryGetValue(normalizedModuleName, out displayName); + return currentLookup.Pattern.TryGetValue(normalizedModuleName, out var info) ? info : null; } if (normalizedModuleName.StartsWith("avm/res/", StringComparison.Ordinal)) { - return currentLookup.Resource.TryGetValue(normalizedModuleName, out displayName); + return currentLookup.Resource.TryGetValue(normalizedModuleName, out var info) ? info : null; } - return false; + return null; } private async Task LoadAsync() @@ -87,7 +113,7 @@ private async Task LoadAsync() var pattern = await patternTask; var resource = await resourceTask; - lookup = new AvmDisplayNameLookup( + lookup = new AvmModuleInfoLookup( Utility: utility, Pattern: pattern, Resource: resource); @@ -114,7 +140,7 @@ private async Task LoadAsync() } } - private async Task> LoadCsvAsync(Uri csvUri, CancellationToken cancellationToken) + private async Task> LoadCsvAsync(Uri csvUri, CancellationToken cancellationToken) { var csvContent = await client.GetCsvAsync(csvUri, cancellationToken); using var reader = new StringReader(csvContent); @@ -140,12 +166,13 @@ private async Task> LoadCsvAsync(Uri csvUri, var moduleNameIndex = Array.FindIndex(headers, x => string.Equals(x, "ModuleName", StringComparison.Ordinal)); var moduleDisplayNameIndex = Array.FindIndex(headers, x => string.Equals(x, "ModuleDisplayName", StringComparison.Ordinal)); + var moduleStatusIndex = Array.FindIndex(headers, x => string.Equals(x, "ModuleStatus", StringComparison.Ordinal)); if (moduleNameIndex == -1 || moduleDisplayNameIndex == -1) { return []; } - var entries = new Dictionary(StringComparer.Ordinal); + var entries = new Dictionary(StringComparer.Ordinal); while (!parser.EndOfData) { var fields = parser.ReadFields(); @@ -167,7 +194,17 @@ private async Task> LoadCsvAsync(Uri csvUri, continue; } - entries.TryAdd(normalizedModuleName, moduleDisplayName); + string? moduleStatus = null; + if (moduleStatusIndex != -1 && moduleStatusIndex < fields.Length) + { + var statusValue = fields[moduleStatusIndex]?.Trim(); + if (!string.IsNullOrWhiteSpace(statusValue)) + { + moduleStatus = statusValue; + } + } + + entries.TryAdd(normalizedModuleName, new AvmModuleInfo(moduleDisplayName, moduleStatus)); } return entries.ToImmutableDictionary(StringComparer.Ordinal); @@ -189,10 +226,12 @@ private async Task> LoadCsvAsync(Uri csvUri, return normalized; } - private sealed record AvmDisplayNameLookup( - ImmutableDictionary Utility, - ImmutableDictionary Pattern, - ImmutableDictionary Resource); + private sealed record AvmModuleInfo(string DisplayName, string? ModuleStatus); + + private sealed record AvmModuleInfoLookup( + ImmutableDictionary Utility, + ImmutableDictionary Pattern, + ImmutableDictionary Resource); } public interface IAvmModuleCsvIndexHttpClient @@ -232,5 +271,11 @@ public bool TryGetModuleDisplayName(string modulePath, [NotNullWhen(true)] out s displayName = null; return false; } + + public bool TryGetModuleStatus(string modulePath, [NotNullWhen(true)] out string? moduleStatus) + { + moduleStatus = null; + return false; + } } } diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 520ad8def2a..39572ce6697 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -672,9 +672,10 @@ private async Task ResolveVersionCompletionItem(CompletionItem c } var title = GetCompletionTitle(registry, modulePath, metadata.Details.Description); + var status = GetModuleStatus(registry, modulePath); return completionItem - .WithDocumentation(GetCompletionDocumentation(title, modulePath, metadata.Details, version)); + .WithDocumentation(GetCompletionDocumentation(title, modulePath, metadata.Details, status, version)); } return completionItem; @@ -694,9 +695,10 @@ private async Task ResolveModuleCompletionItem(CompletionItem co } var title = GetCompletionTitle(registry, modulePath, details.Description); + var status = GetModuleStatus(registry, modulePath); return completionItem - .WithDocumentation(GetCompletionDocumentation(title, modulePath, details)); + .WithDocumentation(GetCompletionDocumentation(title, modulePath, details, status)); } return completionItem; @@ -752,7 +754,18 @@ private static string GetSortText(string label, ModuleCompletionPriority priorit return defaultTitle; } - private static string GetCompletionDocumentation(string? title, string modulePath, RegistryMetadataDetails details, string? version = null) + private string? GetModuleStatus(string registry, string modulePath) + { + if (registry.Equals(LanguageConstants.BicepPublicMcrRegistry, StringComparison.Ordinal) + && avmModuleDisplayNameProvider.TryGetModuleStatus(modulePath, out var moduleStatus)) + { + return moduleStatus; + } + + return null; + } + + private static string GetCompletionDocumentation(string? title, string modulePath, RegistryMetadataDetails details, string? status = null, string? version = null) { var displayModulePath = GetDisplayModulePath(modulePath); @@ -769,6 +782,23 @@ private static string GetCompletionDocumentation(string? title, string modulePat } sections.Add($"**Full module path:** {displayModulePath}"); + + if (status is not null) + { + var emoji = status switch + { + "Proposed" => "⚪", + "Available" => "🟢", + "Orphaned" => "🟡", + "Deprecated" => "🔴", + _ => null, + }; + + sections.Add(emoji is not null + ? $"**Status:** {emoji} {status}" + : $"**Status:** {status}"); + } + sections.Add($"**Description:** {details.Description ?? "N/A"}"); if (MarkdownHelper.GetDocumentationLink(details.DocumentationUri) is { } docLink) From f2bd7934036ef89170402c03730b35a50303004d Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Mon, 18 May 2026 16:08:45 +0000 Subject: [PATCH 13/19] Enhance AVM module completion by adding mock MS Graph extensions and improving readability for long paths --- .../ExamplesTests.cs | 36 +++++- .../Utils/ExtensionResourceTypeHelper.cs | 106 ++++++++++++++++++ .../ModuleReferenceCompletionProviderTests.cs | 1 + 3 files changed, 142 insertions(+), 1 deletion(-) diff --git a/src/Bicep.Core.IntegrationTests/ExamplesTests.cs b/src/Bicep.Core.IntegrationTests/ExamplesTests.cs index 49822d71d35..35d10b4cb41 100644 --- a/src/Bicep.Core.IntegrationTests/ExamplesTests.cs +++ b/src/Bicep.Core.IntegrationTests/ExamplesTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Diagnostics.CodeAnalysis; +using Bicep.Core.Configuration; using Bicep.Core.Diagnostics; using Bicep.Core.Emit; using Bicep.Core.Extensions; @@ -20,6 +21,10 @@ namespace Bicep.Core.IntegrationTests [TestClass] public class ExamplesTests { + private const string MsGraphSampleExtensionVersion = "0.1.8-preview"; + private const string MsGraphBetaRepository = "bicep/extensions/microsoftgraph/beta"; + private const string MsGraphV1Repository = "bicep/extensions/microsoftgraph/v1.0"; + private static ServiceBuilder Services => new ServiceBuilder().WithDisabledAnalyzersConfiguration(); [NotNull] @@ -33,7 +38,13 @@ public static async Task RunExampleTest(TestContext testContext, EmbeddedFile em var bicepFile = baselineFolder.EntryFile; var jsonFile = baselineFolder.GetFileOrEnsureCheckedIn(Path.ChangeExtension(embeddedBicep.FileName, jsonFileExtension)); - var compiler = Services.WithFeatureOverrides(features).Build().GetCompiler(); + var services = Services.WithFeatureOverrides(features); + if (RequiresMockMsGraphExtensions(embeddedBicep)) + { + services = await AddMockMsGraphExtensions(services); + } + + var compiler = services.Build().GetCompiler(); var compilation = await compiler.CreateCompilation(bicepFile.OutputFileUri.ToIOUri()); var model = compilation.GetEntrypointSemanticModel(); @@ -107,6 +118,29 @@ public void ExampleData_should_return_a_number_of_records() private static IEnumerable GetAllExampleData() => ExampleData.GetAllExampleData().Select(x => new object[] { x.BicepFile }); + private static bool RequiresMockMsGraphExtensions(EmbeddedFile embeddedBicep) + => embeddedBicep.StreamPath.StartsWith("Files/user_submitted/extensibility/microsoftGraph/", StringComparison.Ordinal); + + private static async Task AddMockMsGraphExtensions(ServiceBuilder services) + { + services = services.WithContainerRegistryClientFactory(RegistryHelper.CreateMockRegistryClient( + new RegistryHelper.RepoDescriptor(LanguageConstants.BicepPublicMcrRegistry, MsGraphBetaRepository, [MsGraphSampleExtensionVersion]), + new RegistryHelper.RepoDescriptor(LanguageConstants.BicepPublicMcrRegistry, MsGraphV1Repository, [MsGraphSampleExtensionVersion]))); + + var serviceProvider = services.Build(); + await RegistryHelper.PublishExtensionToRegistryAsync( + serviceProvider, + $"br:{LanguageConstants.BicepPublicMcrRegistry}/{MsGraphBetaRepository}:{MsGraphSampleExtensionVersion}", + ExtensionResourceTypeHelper.GetMockMsGraphTypesTgz("MicrosoftGraphBeta", MsGraphSampleExtensionVersion, "beta")); + + await RegistryHelper.PublishExtensionToRegistryAsync( + serviceProvider, + $"br:{LanguageConstants.BicepPublicMcrRegistry}/{MsGraphV1Repository}:{MsGraphSampleExtensionVersion}", + ExtensionResourceTypeHelper.GetMockMsGraphTypesTgz("MicrosoftGraph", MsGraphSampleExtensionVersion, "v1.0")); + + return services; + } + private static bool IsPermittedMissingTypeDiagnostic(IDiagnostic diagnostic) { if (diagnostic.Code != "BCP081") diff --git a/src/Bicep.Core.UnitTests/Utils/ExtensionResourceTypeHelper.cs b/src/Bicep.Core.UnitTests/Utils/ExtensionResourceTypeHelper.cs index f582e86553d..25555e1037d 100644 --- a/src/Bicep.Core.UnitTests/Utils/ExtensionResourceTypeHelper.cs +++ b/src/Bicep.Core.UnitTests/Utils/ExtensionResourceTypeHelper.cs @@ -340,6 +340,112 @@ public static BinaryData GetMockDesiredStateConfigurationTypesTgz() ("types.json", StreamHelper.GetString(stream => TypeSerializer.Serialize(stream, factory.GetTypes())))); } + public static BinaryData GetMockMsGraphTypesTgz(string extensionName, string extensionVersion, string apiVersion) + { + var factory = new TypeFactory([]); + + var stringType = factory.Create(() => new StringType()); + var booleanType = factory.Create(() => new BooleanType()); + var anyType = factory.Create(() => new AnyType()); + var stringArrayType = factory.Create(() => new ArrayType(factory.GetReference(stringType))); + var anyArrayType = factory.Create(() => new ArrayType(factory.GetReference(anyType))); + + var stringTypeRef = factory.GetReference(stringType); + var booleanTypeRef = factory.GetReference(booleanType); + var anyTypeRef = factory.GetReference(anyType); + var stringArrayTypeRef = factory.GetReference(stringArrayType); + var anyArrayTypeRef = factory.GetReference(anyArrayType); + + ObjectTypeProperty Property(ITypeReference typeRef, ObjectTypePropertyFlags flags = ObjectTypePropertyFlags.None) => new(typeRef, flags, null); + + ResourceType Resource(string name, Dictionary properties) + { + var bodyType = factory.Create(() => new ObjectType(name, properties, null)); + + return factory.Create(() => new ResourceType( + name, + factory.GetReference(bodyType), + null, + writableScopes_in: ScopeType.All, + readableScopes_in: ScopeType.All)); + } + + Dictionary ApplicationProperties() => new() + { + ["uniqueName"] = Property(stringTypeRef, ObjectTypePropertyFlags.Required), + ["displayName"] = Property(stringTypeRef), + ["appId"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly), + ["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly), + ["appRoles"] = Property(anyArrayTypeRef), + ["api"] = Property(anyTypeRef), + }; + + ResourceType[] resourceTypes = apiVersion switch + { + "beta" => + [ + Resource("Microsoft.Graph/applications@beta", ApplicationProperties()), + Resource("Microsoft.Graph/servicePrincipals@beta", new() + { + ["appId"] = Property(stringTypeRef, ObjectTypePropertyFlags.Required), + ["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly), + }), + Resource("Microsoft.Graph/oauth2PermissionGrants@beta", new() + { + ["clientId"] = Property(stringTypeRef), + ["consentType"] = Property(stringTypeRef), + ["resourceId"] = Property(stringTypeRef), + ["scope"] = Property(stringTypeRef), + ["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly), + }), + Resource("Microsoft.Graph/appRoleAssignedTo@beta", new() + { + ["appRoleId"] = Property(stringTypeRef), + ["principalId"] = Property(stringTypeRef), + ["resourceId"] = Property(stringTypeRef), + ["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly), + }), + Resource("Microsoft.Graph/groups@beta", new() + { + ["uniqueName"] = Property(stringTypeRef, ObjectTypePropertyFlags.Required), + ["displayName"] = Property(stringTypeRef), + ["mailEnabled"] = Property(booleanTypeRef), + ["mailNickname"] = Property(stringTypeRef), + ["securityEnabled"] = Property(booleanTypeRef), + ["groupTypes"] = Property(stringArrayTypeRef), + ["owners"] = Property(stringArrayTypeRef), + ["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly), + }), + ], + "v1.0" => + [ + Resource("Microsoft.Graph/applications@v1.0", ApplicationProperties()), + Resource("Microsoft.Graph/applications/federatedIdentityCredentials@v1.0", new() + { + ["name"] = Property(stringTypeRef, ObjectTypePropertyFlags.Required), + ["audiences"] = Property(stringArrayTypeRef), + ["description"] = Property(stringTypeRef), + ["issuer"] = Property(stringTypeRef), + ["subject"] = Property(stringTypeRef), + ["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly), + }), + ], + _ => throw new ArgumentException($"Unsupported Microsoft Graph API version '{apiVersion}'.", nameof(apiVersion)), + }; + + var settings = new TypeSettings(name: extensionName, version: extensionVersion, isSingleton: false, configurationType: null!); + var index = new TypeIndex( + resourceTypes.ToDictionary(x => x.Name, x => new CrossFileTypeReference("types.json", factory.GetIndex(x))), + new Dictionary>>(), + [], + settings, + null); + + return GetTypesTgzBytesFromFiles( + ("index.json", StreamHelper.GetString(stream => TypeSerializer.SerializeIndex(stream, index))), + ("types.json", StreamHelper.GetString(stream => TypeSerializer.Serialize(stream, factory.GetTypes())))); + } + public static BinaryData GetTypesTgzBytesFromFiles(params (string filePath, string contents)[] files) { var stream = new MemoryStream(); diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index f86e96a3f0f..8af3a229d9e 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -592,6 +592,7 @@ public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_Uses catalog, settingsProvider, BicepTestConstants.CreateMockTelemetryProvider().Object, + BicepTestConstants.TestRegistryConfiguration, avmDisplayNameProviderMock.Object); var completions = await GetAndResolveCompletionItems(sourceFile, completionContext, moduleReferenceCompletionProvider); From e772b978b498ffadb9d9517955625b4c7a460445 Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Mon, 18 May 2026 17:02:23 +0000 Subject: [PATCH 14/19] Add unit test for filtered completions using catalog description when display name cache is empty --- .../AvmModuleDisplayNameProviderTests.cs | 111 ++++++++++++++++++ .../ModuleReferenceCompletionProviderTests.cs | 43 +++++++ 2 files changed, 154 insertions(+) create mode 100644 src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs diff --git a/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs new file mode 100644 index 00000000000..28b624fc842 --- /dev/null +++ b/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Reflection; +using System.Net.Http; +using Bicep.LanguageServer.Completions; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.LangServer.UnitTests.Completions +{ + [TestClass] + public class AvmModuleDisplayNameProviderTests + { + [TestMethod] + public async Task StartCache_WhenCsvFilesAreAvailable_LoadsDisplayNamesAndStatuses() + { + const string csvContent = """ + ModuleName,ModuleDisplayName,ModuleStatus + bicep/avm/ptn/ai-platform/baseline,AI platform baseline,Available + bicep/avm/res/compute/virtual-machine,Virtual machine,Deprecated + """; + + var client = new TestAvmModuleCsvIndexHttpClient((_, _) => Task.FromResult(csvContent)); + var provider = new AvmModuleDisplayNameProvider(client); + + provider.StartCache(); + await WaitForLoadTaskAsync(provider); + + provider.TryGetModuleDisplayName("bicep/avm/ptn/ai-platform/baseline", out var patternDisplayName).Should().BeTrue(); + patternDisplayName.Should().Be("AI platform baseline"); + + provider.TryGetModuleStatus("bicep/avm/ptn/ai-platform/baseline", out var patternModuleStatus).Should().BeTrue(); + patternModuleStatus.Should().Be("Available"); + + provider.TryGetModuleDisplayName("avm/res/compute/virtual-machine", out var resourceDisplayName).Should().BeTrue(); + resourceDisplayName.Should().Be("Virtual machine"); + + provider.TryGetModuleStatus("avm/res/compute/virtual-machine", out var resourceModuleStatus).Should().BeTrue(); + resourceModuleStatus.Should().Be("Deprecated"); + + client.RequestCount.Should().Be(3); + } + + [TestMethod] + public async Task StartCache_WhenCsvFetchFails_CompletesAndUsesEmptyLookup() + { + var client = new TestAvmModuleCsvIndexHttpClient((_, _) => + Task.FromException(new HttpRequestException("CSV unavailable"))); + var provider = new AvmModuleDisplayNameProvider(client); + + var startCache = provider.StartCache; + startCache.Should().NotThrow(); + + await WaitForLoadTaskAsync(provider); + + provider.TryGetModuleDisplayName("bicep/avm/ptn/ai-platform/baseline", out var displayName).Should().BeFalse(); + displayName.Should().BeNull(); + + provider.TryGetModuleStatus("bicep/avm/ptn/ai-platform/baseline", out var moduleStatus).Should().BeFalse(); + moduleStatus.Should().BeNull(); + + client.RequestCount.Should().Be(3); + } + + private static async Task WaitForLoadTaskAsync(AvmModuleDisplayNameProvider provider) + { + var loadTask = GetLoadTask(provider); + var completedTask = await Task.WhenAny(loadTask, Task.Delay(TimeSpan.FromSeconds(5))); + + completedTask.Should().Be(loadTask); + await loadTask; + } + + private static Task GetLoadTask(AvmModuleDisplayNameProvider provider) + { + var loadTaskField = typeof(AvmModuleDisplayNameProvider).GetField("loadTask", BindingFlags.NonPublic | BindingFlags.Instance); + loadTaskField.Should().NotBeNull(); + + var loadTask = loadTaskField?.GetValue(provider) as Task; + loadTask.Should().NotBeNull(); + + return loadTask ?? Task.CompletedTask; + } + + private sealed class TestAvmModuleCsvIndexHttpClient : IAvmModuleCsvIndexHttpClient + { + private readonly Func> getCsvAsync; + private int requestCount; + + public TestAvmModuleCsvIndexHttpClient(Func> getCsvAsync) + { + this.getCsvAsync = getCsvAsync; + } + + public int RequestCount => Volatile.Read(ref requestCount); + + public async Task GetCsvAsync(Uri csvUri, CancellationToken cancellationToken) + { + try + { + return await getCsvAsync(csvUri, cancellationToken); + } + finally + { + Interlocked.Increment(ref requestCount); + } + } + } + } +} diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index 8af3a229d9e..991e145592c 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -611,6 +611,49 @@ public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_Uses avmDisplayNameProviderMock.VerifyAll(); } + [TestMethod] + public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_WhenDisplayNameCacheIsEmpty_UsesCatalogDescriptionForTooltipTitle() + { + var catalog = RegistryCatalogMocks.CreateCatalogWithMocks( + RegistryCatalogMocks.MockPublicMetadataProvider([ + new("bicep/avm/ptn/ai-platform/baseline", "module description", "contoso.com/help", []), + ]) + ); + + var avmDisplayNameProviderMock = StrictMock.Of(); + string? moduleDisplayName = null; + avmDisplayNameProviderMock + .Setup(provider => provider.TryGetModuleDisplayName("bicep/avm/ptn/ai-platform/baseline", out moduleDisplayName)) + .Returns(false); + string? moduleStatus = null; + avmDisplayNameProviderMock + .Setup(provider => provider.TryGetModuleStatus("bicep/avm/ptn/ai-platform/baseline", out moduleStatus)) + .Returns(false); + + var (completionContext, sourceFile) = GetBicepCompletionContext("module test 'br/public:avm/ptn/ai-platform/|'"); + var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( + azureContainerRegistriesProvider, + catalog, + settingsProvider, + BicepTestConstants.CreateMockTelemetryProvider().Object, + BicepTestConstants.TestRegistryConfiguration, + avmDisplayNameProviderMock.Object); + var completions = await GetAndResolveCompletionItems(sourceFile, completionContext, moduleReferenceCompletionProvider); + + completions.Should().SatisfyRespectively( + completion => + { + completion.Label.Should().Be("avm/ptn/ai-platform/baseline"); + completion.Detail.Should().BeNull(); + completion.Documentation!.MarkupContent!.Value.Should().Contain("### module description"); + completion.Documentation.MarkupContent.Value.Should().Contain("**Full module path:** avm/ptn/ai-platform/baseline"); + completion.Documentation.MarkupContent.Value.Should().NotContain("**Status:**"); + completion.Documentation.MarkupContent.Value.Should().Contain("**Description:** module description"); + }); + + avmDisplayNameProviderMock.VerifyAll(); + } + [DataTestMethod] [DataRow("module test 'br:registry.contoso.io/bicep/|'", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] [DataRow("module test 'br:registry.contoso.io/bicep/|", "bicep/whatever/abc/foo/bar", "'br:registry.contoso.io/bicep/whatever/abc/foo/bar:$0'")] From a080b55f2a6ccada8dab88c784c2ca4fe20d50bc Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Mon, 18 May 2026 18:23:40 +0000 Subject: [PATCH 15/19] Refactor using directives for consistency in AvmModuleDisplayNameProvider tests and implementation --- .../Completions/AvmModuleDisplayNameProviderTests.cs | 2 +- .../Completions/AvmModuleDisplayNameProvider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs index 28b624fc842..96d80ca13a6 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Reflection; using System.Net.Http; +using System.Reflection; using Bicep.LanguageServer.Completions; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs b/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs index ff55486b22a..995853d5d5b 100644 --- a/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs +++ b/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs @@ -2,8 +2,8 @@ // Licensed under the MIT License. using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Bicep.Core.Features; using Microsoft.VisualBasic.FileIO; From 56eb43868ba7994d8801a13b660475018f2706c7 Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:29:38 +0000 Subject: [PATCH 16/19] Skip completions for proposed modules in AVM module reference provider --- .../Completions/ModuleReferenceCompletionProvider.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 39572ce6697..b160b0bda80 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -498,6 +498,13 @@ private async Task> GetModuleCompletions(string trim continue; } + if (parts.ResolvedRegistry.Equals(LanguageConstants.BicepPublicMcrRegistry, StringComparison.Ordinal) + && avmModuleDisplayNameProvider.TryGetModuleStatus(moduleName, out var moduleStatus) + && moduleStatus.Equals("Proposed", StringComparison.Ordinal)) + { + continue; + } + string insertText = $"'{parts.WithModulePath(moduleName).ToNotation()}:$0'"; // Remove the base path prefix from the label if we're dealing with a module alias From 4bafba39f10cc75ca892d088f01966134074b0d6 Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:29:57 +0000 Subject: [PATCH 17/19] Add tests to filter AVM module completions based on module status --- .../ModuleReferenceCompletionProviderTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index 991e145592c..c3037c5f44d 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -611,6 +611,71 @@ public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_Uses avmDisplayNameProviderMock.VerifyAll(); } + [TestMethod] + public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_ExcludesProposedModules() + { + var catalog = RegistryCatalogMocks.CreateCatalogWithMocks( + RegistryCatalogMocks.MockPublicMetadataProvider([ + new("bicep/avm/res/compute/virtual-machine", "Virtual machine", "contoso.com/available", []), + new("bicep/avm/res/compute/proposed-service", "Proposed service", "contoso.com/proposed", []), + ]) + ); + + var avmDisplayNameProviderMock = StrictMock.Of(); + string? availableModuleStatus = "Available"; + avmDisplayNameProviderMock + .Setup(provider => provider.TryGetModuleStatus("bicep/avm/res/compute/virtual-machine", out availableModuleStatus)) + .Returns(true); + string? proposedModuleStatus = "Proposed"; + avmDisplayNameProviderMock + .Setup(provider => provider.TryGetModuleStatus("bicep/avm/res/compute/proposed-service", out proposedModuleStatus)) + .Returns(true); + + var (completionContext, sourceFile) = GetBicepCompletionContext("module test 'br/public:avm/res/compute/|'"); + var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( + azureContainerRegistriesProvider, + catalog, + settingsProvider, + BicepTestConstants.CreateMockTelemetryProvider().Object, + BicepTestConstants.TestRegistryConfiguration, + avmDisplayNameProviderMock.Object); + var completions = await moduleReferenceCompletionProvider.GetFilteredCompletions(sourceFile, completionContext, CancellationToken.None); + + completions.Select(completion => completion.Label).Should().Equal("avm/res/compute/virtual-machine"); + + avmDisplayNameProviderMock.VerifyAll(); + } + + [TestMethod] + public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_WhenModuleStatusIsUnavailable_IncludesModule() + { + var catalog = RegistryCatalogMocks.CreateCatalogWithMocks( + RegistryCatalogMocks.MockPublicMetadataProvider([ + new("bicep/avm/res/compute/virtual-machine", "Virtual machine", "contoso.com/help", []), + ]) + ); + + var avmDisplayNameProviderMock = StrictMock.Of(); + string? moduleStatus = null; + avmDisplayNameProviderMock + .Setup(provider => provider.TryGetModuleStatus("bicep/avm/res/compute/virtual-machine", out moduleStatus)) + .Returns(false); + + var (completionContext, sourceFile) = GetBicepCompletionContext("module test 'br/public:avm/res/compute/|'"); + var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( + azureContainerRegistriesProvider, + catalog, + settingsProvider, + BicepTestConstants.CreateMockTelemetryProvider().Object, + BicepTestConstants.TestRegistryConfiguration, + avmDisplayNameProviderMock.Object); + var completions = await moduleReferenceCompletionProvider.GetFilteredCompletions(sourceFile, completionContext, CancellationToken.None); + + completions.Select(completion => completion.Label).Should().Equal("avm/res/compute/virtual-machine"); + + avmDisplayNameProviderMock.VerifyAll(); + } + [TestMethod] public async Task GetFilteredCompletions_WithPublicAvmModulePathCompletions_WhenDisplayNameCacheIsEmpty_UsesCatalogDescriptionForTooltipTitle() { From f52740dd1fffdf01ef980340d1c1baf8a3706e0f Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:39:12 +0000 Subject: [PATCH 18/19] Update AVM module CSV URIs to use the latest links for improved accuracy --- .../Completions/AvmModuleDisplayNameProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs b/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs index 995853d5d5b..ef7866c4c64 100644 --- a/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs +++ b/src/Bicep.LangServer/Completions/AvmModuleDisplayNameProvider.cs @@ -20,9 +20,9 @@ public interface IAvmModuleDisplayNameProvider public class AvmModuleDisplayNameProvider : IAvmModuleDisplayNameProvider { - private static readonly Uri UtilityModulesCsvUri = new("https://azure.github.io/Azure-Verified-Modules/module-indexes/BicepUtilityModules.csv"); - private static readonly Uri PatternModulesCsvUri = new("https://azure.github.io/Azure-Verified-Modules/module-indexes/BicepPatternModules.csv"); - private static readonly Uri ResourceModulesCsvUri = new("https://azure.github.io/Azure-Verified-Modules/module-indexes/BicepResourceModules.csv"); + private static readonly Uri UtilityModulesCsvUri = new("https://aka.ms/avm/index/bicep/utl/csv"); + private static readonly Uri PatternModulesCsvUri = new("https://aka.ms/avm/index/bicep/ptn/csv"); + private static readonly Uri ResourceModulesCsvUri = new("https://aka.ms/avm/index/bicep/res/csv"); private static readonly AvmModuleInfoLookup EmptyLookup = new([], [], []); From e132b262c26a2bfa09a1cca0cd6f544b82cbaf7d Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:39:52 +0000 Subject: [PATCH 19/19] Enhance AVM module display name tests to verify requested CSV URIs --- .../Completions/AvmModuleDisplayNameProviderTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs index 96d80ca13a6..6f06e3ffd5e 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/AvmModuleDisplayNameProviderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Concurrent; using System.Net.Http; using System.Reflection; using Bicep.LanguageServer.Completions; @@ -40,6 +41,11 @@ public async Task StartCache_WhenCsvFilesAreAvailable_LoadsDisplayNamesAndStatus resourceModuleStatus.Should().Be("Deprecated"); client.RequestCount.Should().Be(3); + client.RequestedUris.Select(uri => uri.ToString()).Should().BeEquivalentTo([ + "https://aka.ms/avm/index/bicep/utl/csv", + "https://aka.ms/avm/index/bicep/ptn/csv", + "https://aka.ms/avm/index/bicep/res/csv", + ]); } [TestMethod] @@ -86,6 +92,7 @@ private static Task GetLoadTask(AvmModuleDisplayNameProvider provider) private sealed class TestAvmModuleCsvIndexHttpClient : IAvmModuleCsvIndexHttpClient { private readonly Func> getCsvAsync; + private readonly ConcurrentQueue requestedUris = new(); private int requestCount; public TestAvmModuleCsvIndexHttpClient(Func> getCsvAsync) @@ -95,8 +102,12 @@ public TestAvmModuleCsvIndexHttpClient(Func public int RequestCount => Volatile.Read(ref requestCount); + public IEnumerable RequestedUris => requestedUris; + public async Task GetCsvAsync(Uri csvUri, CancellationToken cancellationToken) { + requestedUris.Enqueue(csvUri); + try { return await getCsvAsync(csvUri, cancellationToken);