feat: context-aware Bicep completions (version + param injection)
- ProxySession tracks open documents per TCP connection - _detect_context() identifies version, param, and module_path contexts - version context: autocomplete versions for 'br/modules:NAME:' cursor positions - param context: autocomplete params for specific module+version (with version fallback) - modules.py: added get_module_by_name(), version_completion_items(), param_completion_items() - 28/28 tests passing
This commit is contained in:
@@ -4,8 +4,10 @@ LRU Bicep module catalog — loaded from the bundled catalog file at startup.
|
||||
The catalog (bicep_modules_catalog.json) is baked into the Docker image at build time.
|
||||
No runtime dependency on DevOpsMCP or any external service.
|
||||
|
||||
Provides completion items for LRU-internal Bicep modules with
|
||||
higher sort priority than standard Azure modules.
|
||||
Provides completion items for:
|
||||
- LRU module names (when typing a module reference string)
|
||||
- Module versions per module (when cursor is after 'br/modules:NAME:')
|
||||
- Module params per version (when cursor is inside a params {} block)
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -24,7 +26,7 @@ _CATALOG_PATHS = [
|
||||
|
||||
|
||||
def _load_catalog() -> list[dict[str, Any]]:
|
||||
"""Load modules from the bundled catalog file."""
|
||||
"""Load modules from the bundled catalog file, preserving per-version schema."""
|
||||
for path in _CATALOG_PATHS:
|
||||
if path.exists():
|
||||
try:
|
||||
@@ -32,7 +34,6 @@ def _load_catalog() -> list[dict[str, Any]]:
|
||||
modules_raw = data.get("modules", {})
|
||||
registry = data.get("registry", "iactemplatereg.azurecr.io")
|
||||
modules = []
|
||||
# modules is a dict: { "bicep/modules/appservice": { versions: [...], ... }, ... }
|
||||
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
|
||||
@@ -42,6 +43,7 @@ def _load_catalog() -> list[dict[str, Any]]:
|
||||
"versions": versions,
|
||||
"latest": versions[-1] if versions else "latest",
|
||||
"registry": registry,
|
||||
"schema": info.get("schema", {}), # per-version → params
|
||||
})
|
||||
logger.info("Bicep catalog loaded from %s: %d modules", path, len(modules))
|
||||
return modules
|
||||
@@ -65,8 +67,16 @@ class BicepModuleCatalog:
|
||||
def get_modules(cls) -> list[dict[str, Any]]:
|
||||
return cls._modules
|
||||
|
||||
@classmethod
|
||||
def get_module_by_name(cls, name: str) -> dict[str, Any] | None:
|
||||
for mod in cls._modules:
|
||||
if mod["name"] == name:
|
||||
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']}"
|
||||
@@ -88,3 +98,80 @@ class BicepModuleCatalog:
|
||||
},
|
||||
})
|
||||
return items
|
||||
|
||||
@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)
|
||||
if not mod:
|
||||
return []
|
||||
schema = mod.get("schema", {})
|
||||
items = []
|
||||
for ver in reversed(mod["versions"]):
|
||||
ver_schema = schema.get(ver, {})
|
||||
params = list(ver_schema.get("parameters", {}).keys())
|
||||
param_summary = (
|
||||
f"{len(params)} params: {', '.join(params[:3])}{'...' if len(params) > 3 else ''}"
|
||||
if params else "no params"
|
||||
)
|
||||
items.append({
|
||||
"label": ver,
|
||||
"kind": 12, # Value
|
||||
"detail": param_summary,
|
||||
"insertText": ver,
|
||||
"sortText": f"0_lru_ver_{ver}",
|
||||
"documentation": {
|
||||
"kind": "markdown",
|
||||
"value": (
|
||||
f"**{module_name} `{ver}`**\n\n"
|
||||
+ (
|
||||
"Params: " + ", ".join(f"`{p}`" for p in params)
|
||||
if params else "_No params_"
|
||||
)
|
||||
),
|
||||
},
|
||||
})
|
||||
return items
|
||||
|
||||
@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)
|
||||
if not mod:
|
||||
return []
|
||||
schema = mod.get("schema", {})
|
||||
|
||||
# Exact version match first, then fall back to closest available
|
||||
ver_params = schema.get(version, {}).get("parameters", {})
|
||||
if not ver_params:
|
||||
for v in reversed(list(schema.keys())):
|
||||
candidate = schema[v].get("parameters", {})
|
||||
if candidate:
|
||||
ver_params = candidate
|
||||
logger.debug("Param fallback: %s %s→%s", module_name, version, v)
|
||||
break
|
||||
|
||||
items = []
|
||||
for param_name, param_info in ver_params.items():
|
||||
ptype = param_info.get("type", "any")
|
||||
description = param_info.get("description", "").strip()
|
||||
allowed = param_info.get("allowed", [])
|
||||
doc_lines = [f"**{param_name}** (`{ptype}`)"]
|
||||
if description:
|
||||
doc_lines.append(f"\n{description}")
|
||||
if allowed:
|
||||
shown = allowed[:5]
|
||||
more = f" + {len(allowed) - 5} more" if len(allowed) > 5 else ""
|
||||
doc_lines.append(f"\nAllowed: {', '.join(f'`{a}`' for a in shown)}{more}")
|
||||
items.append({
|
||||
"label": param_name,
|
||||
"kind": 5, # Field
|
||||
"detail": ptype,
|
||||
"insertText": f"{param_name}: ",
|
||||
"sortText": f"0_lru_param_{param_name}",
|
||||
"documentation": {
|
||||
"kind": "markdown",
|
||||
"value": "\n".join(doc_lines),
|
||||
},
|
||||
})
|
||||
return items
|
||||
|
||||
Reference in New Issue
Block a user