diff --git a/ilsp/bicep_lsp/modules.py b/ilsp/bicep_lsp/modules.py index 920591a..b6b0e88 100644 --- a/ilsp/bicep_lsp/modules.py +++ b/ilsp/bicep_lsp/modules.py @@ -67,9 +67,14 @@ def _load_catalog() -> list[dict[str, Any]]: for mod_path, info in modules_raw.items(): versions = info.get("versions", ["latest"]) name = mod_path.split("/")[-1] if "/" in mod_path else mod_path + # ref_path: what the user writes after 'br/modules:' in a .bicep file. + # bicepconfig modulePath "bicep" means Bicep prepends "bicep/" to the + # registry path automatically, so strip that prefix here. + ref_path = mod_path.removeprefix("bicep/") modules.append({ "name": name, "path": mod_path, + "ref_path": ref_path, # e.g. "modules/appservice", "util/types" "versions": versions, "latest": versions[-1] if versions else "latest", "registry": registry, @@ -97,8 +102,13 @@ class BicepModuleCatalog: @classmethod def _iac_param_map(cls, module_name: str) -> dict[str, dict[str, Any]]: - """Return {param_name: {description, required, type}} from IAC source catalog.""" - iac_mod = cls._iac.get(module_name, {}) + """Return {param_name: {description, required, type}} from IAC source catalog. + + IAC catalog is keyed by bare module name (last path segment), so strip any + path prefix (e.g. 'modules/appservice' → 'appservice'). + """ + bare_name = module_name.split("/")[-1] + iac_mod = cls._iac.get(bare_name, {}) return {p["name"]: p for p in iac_mod.get("params", [])} @classmethod @@ -112,12 +122,24 @@ class BicepModuleCatalog: return mod return None + @classmethod + def get_module_by_ref(cls, ref_path: str) -> dict[str, Any] | None: + """Look up a module by the path that appears after 'br/modules:' in a .bicep file. + + Matches both new-style ('modules/appservice', 'util/types') and old-style + bare names ('appservice') so completions work regardless of module version. + """ + for mod in cls._modules: + if mod["ref_path"] == ref_path or mod["name"] == ref_path: + return mod + return None + @classmethod def as_completion_items(cls) -> list[dict[str, Any]]: """Module name completions — shown when typing a module reference string.""" items = [] for mod in cls._modules: - ref = f"br/modules:{mod['path']}:{mod['latest']}" + ref = f"br/modules:{mod['ref_path']}:{mod['latest']}" items.append({ "label": mod["name"], "kind": 9, # Module @@ -140,7 +162,7 @@ class BicepModuleCatalog: @classmethod def version_completion_items(cls, module_name: str) -> list[dict[str, Any]]: """Version completions for a specific module (newest first).""" - mod = cls.get_module_by_name(module_name) + mod = cls.get_module_by_ref(module_name) if not mod: return [] schema = mod.get("schema", {}) @@ -180,7 +202,7 @@ class BicepModuleCatalog: has_open_quote: bool = False, ) -> list[dict[str, Any]]: """Enum/allowed-value completions for a specific param (e.g. principalType, environmentType).""" - mod = cls.get_module_by_name(module_name) + mod = cls.get_module_by_ref(module_name) allowed: list[str] = [] if mod: @@ -224,7 +246,7 @@ class BicepModuleCatalog: @classmethod def param_completion_items(cls, module_name: str, version: str) -> list[dict[str, Any]]: """Param completions for a specific module+version combination.""" - mod = cls.get_module_by_name(module_name) + mod = cls.get_module_by_ref(module_name) if not mod: return [] schema = mod.get("schema", {}) diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 5220b1d..fff9e32 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -31,9 +31,11 @@ def _completion_response(items: list) -> dict: def _make_module(name, versions=None, schema=None): versions = versions or ["1.0.0", "latest"] + path = f"bicep/modules/{name}" return { "name": name, - "path": f"bicep/modules/{name}", + "path": path, + "ref_path": f"modules/{name}", # what appears after 'br/modules:' in bicep files "versions": versions, "latest": versions[-1], "registry": "iactemplatereg.azurecr.io",