fix(bicep): support multi-line array completion for roles and other enums
Adds support for autocomplete in multi-line array syntax like:
roles: [
'KEY_VAULT_ ← cursor triggers completion here
]
Previously only worked on same line as opening bracket:
roles: ['KEY_VAULT_ ← only this worked
Changes:
- Walk backwards up to 10 lines to find array opening (e.g. "roles: [")
- Detect if cursor is inside array based on indentation and quotes
- Stop lookback if closing bracket found (not in array anymore)
- Add test case for nested multi-line array completion
- Improve lsp_bridge.py error handling with traceback logging
- Add lsp_bridge_debug.sh wrapper for easier IntelliJ debugging
- Update EDITOR_SETUP.md with correct IntelliJ LSP4IJ config
Fixes autocomplete for deeply nested structures like:
assignments: [{ roles: ['APP_CONFIGURATION_...'] }]
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,21 +14,49 @@ supports LSP WebSocket transport.
|
||||
|
||||
## IntelliJ IDEA setup (LSP4IJ)
|
||||
|
||||
Install the [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, then:
|
||||
Install the [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, then add language servers:
|
||||
|
||||
### Bicep
|
||||
|
||||
1. **Settings → Languages & Frameworks → LSP → Language Servers → +**
|
||||
- Name: `iLSP Bicep`
|
||||
- Server type: `Command` (not WebSocket — see note below)
|
||||
- Command: `python3`
|
||||
- Arguments: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge.py wss://ilsp.i80.dk/bicep`
|
||||
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/bicep`
|
||||
- Arguments: (leave blank)
|
||||
- File pattern: `*.bicep;*.bicepparam`
|
||||
|
||||
### YAML (Azure DevOps + GitHub Actions)
|
||||
|
||||
2. **Settings → Languages & Frameworks → LSP → Language Servers → +**
|
||||
- Name: `iLSP YAML`
|
||||
- Server type: `Command`
|
||||
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/yaml`
|
||||
- Arguments: (leave blank)
|
||||
- File pattern: `*.yaml;*.yml;azure-pipelines.yml`
|
||||
|
||||
### Python (optional)
|
||||
|
||||
3. **Settings → Languages & Frameworks → LSP → Language Servers → +**
|
||||
- Name: `iLSP Python`
|
||||
- Server type: `Command`
|
||||
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/python`
|
||||
- Arguments: (leave blank)
|
||||
- File pattern: `*.py`
|
||||
|
||||
> **Note**: LSP4IJ's WebSocket mode doesn't handle LSP Content-Length framing correctly,
|
||||
> causing "starting..." to hang or "Stream closed" errors. Use `lsp_bridge.py` instead.
|
||||
> causing "starting..." to hang or "Stream closed" errors. Use `lsp_bridge_debug.sh` wrapper
|
||||
> which calls `lsp_bridge.py` and logs errors to `/tmp/lsp_bridge_debug.log` for debugging.
|
||||
|
||||
2. **Add a file-type mapping** under the new server entry:
|
||||
- File pattern: `*.bicep;*.bicepparam` (include both Bicep file types)
|
||||
### Troubleshooting
|
||||
|
||||
3. **Disable Azure Toolkit Bicep completions** — this is critical if you have the
|
||||
If the language server doesn't start, check the debug log:
|
||||
```bash
|
||||
tail -f /tmp/lsp_bridge_debug.log
|
||||
```
|
||||
|
||||
### Disable conflicting plugins
|
||||
|
||||
**Disable Azure Toolkit Bicep completions** — this is critical if you have the
|
||||
Azure Toolkit plugin installed. Without this step, IDEA merges completions from
|
||||
both LSP4IJ and Azure Toolkit, resulting in noisy suggestions (`resource`,
|
||||
`projectName`, Bicep schema types, etc.) appearing alongside iLSP results.
|
||||
|
||||
@@ -154,6 +154,31 @@ class _ProxySession:
|
||||
"has_open_quote": bool(array_m.group(2)),
|
||||
}
|
||||
|
||||
# Check if cursor is inside a multi-line array element
|
||||
# e.g. " 'APP_CONFIG" on line after "roles: ["
|
||||
# Walk backwards to find the array opening
|
||||
for lookback_idx in range(line_idx - 1, max(0, line_idx - 10), -1):
|
||||
prev_line = lines[lookback_idx]
|
||||
# Found array opening like "roles: [" or " roles: ["
|
||||
array_open_m = re.match(r"^\s*(\w+):\s*\[$", prev_line.rstrip())
|
||||
if array_open_m:
|
||||
param_name = array_open_m.group(1)
|
||||
if param_name not in {"params", "name", "module", "resource"}:
|
||||
# Check if current line is inside the array (has quote or is indented)
|
||||
if re.match(r"^\s+('?)([^',\]]*)\s*$", current):
|
||||
has_quote = bool(re.match(r"^\s+'", current))
|
||||
return {
|
||||
"type": "param_value",
|
||||
"module": mod_name,
|
||||
"version": mod_ver,
|
||||
"param": param_name,
|
||||
"has_open_quote": has_quote,
|
||||
}
|
||||
break
|
||||
# Stop if we hit a closing bracket (we're outside the array)
|
||||
if "]" in prev_line:
|
||||
break
|
||||
|
||||
return {
|
||||
"type": "param",
|
||||
"module": mod_name,
|
||||
|
||||
@@ -17,6 +17,7 @@ frames straight to stdout.
|
||||
import asyncio
|
||||
import ssl
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import websockets
|
||||
|
||||
@@ -71,8 +72,9 @@ async def main(uri: str) -> None:
|
||||
if msg is None:
|
||||
break
|
||||
await ws.send(msg)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"stdin_to_ws error: {e}", file=sys.stderr)
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
|
||||
async def ws_to_stdout() -> None:
|
||||
try:
|
||||
@@ -80,8 +82,9 @@ async def main(uri: str) -> None:
|
||||
data = frame if isinstance(frame, bytes) else frame.encode()
|
||||
sys.stdout.buffer.write(data)
|
||||
sys.stdout.buffer.flush()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"ws_to_stdout error: {e}", file=sys.stderr)
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
|
||||
await asyncio.gather(stdin_to_ws(), ws_to_stdout())
|
||||
|
||||
@@ -90,4 +93,9 @@ if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
try:
|
||||
asyncio.run(main(sys.argv[1]))
|
||||
except Exception as e:
|
||||
print(f"Fatal error: {e}", file=sys.stderr)
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
12
scripts/lsp_bridge_debug.sh
Executable file
12
scripts/lsp_bridge_debug.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Wrapper for lsp_bridge.py that logs to a file for debugging IntelliJ issues
|
||||
|
||||
LOG_FILE="/tmp/lsp_bridge_debug.log"
|
||||
|
||||
echo "=== LSP Bridge started at $(date) ===" >> "$LOG_FILE"
|
||||
echo "Args: $@" >> "$LOG_FILE"
|
||||
echo "PWD: $(pwd)" >> "$LOG_FILE"
|
||||
echo "Python: $(which python3)" >> "$LOG_FILE"
|
||||
echo "" >> "$LOG_FILE"
|
||||
|
||||
exec /opt/homebrew/bin/python3 /Users/lrihni/Projects/iLSP/scripts/lsp_bridge.py "$@" 2>> "$LOG_FILE"
|
||||
@@ -329,6 +329,30 @@ def test_detect_param_value_context_in_array():
|
||||
assert ctx["has_open_quote"] is True
|
||||
|
||||
|
||||
def test_detect_param_value_context_in_multiline_array():
|
||||
"""Cursor in multi-line array element → param_value context."""
|
||||
lines = [
|
||||
"module myMod 'br/modules:roleassignments:2.0.x' = {",
|
||||
" params: {",
|
||||
" assignments: [",
|
||||
" {",
|
||||
" roles: [",
|
||||
" 'APP_CONFIGURATION_", # ← cursor on separate line
|
||||
" ]",
|
||||
" }",
|
||||
" ]",
|
||||
" }",
|
||||
"}",
|
||||
]
|
||||
session = _make_session_with_doc(URI, lines)
|
||||
# character = len(" 'APP_CONFIGURATION_") = 27
|
||||
ctx = session._detect_context(URI, {"line": 5, "character": 27})
|
||||
assert ctx["type"] == "param_value"
|
||||
assert ctx["module"] == "roleassignments"
|
||||
assert ctx["param"] == "roles"
|
||||
assert ctx["has_open_quote"] is True
|
||||
|
||||
|
||||
def test_param_value_items_from_catalog_allowed():
|
||||
"""environmentType completions come from catalog 'allowed' field."""
|
||||
BicepModuleCatalog._modules = [_make_module(
|
||||
|
||||
Reference in New Issue
Block a user