fix(bicep): support multi-line array completion for roles and other enums
All checks were successful
Build and Deploy iLSP / test (push) Successful in 22s
Build and Deploy iLSP / build-and-deploy (push) Successful in 1m35s

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:
Henrik Jess Nielsen
2026-05-19 13:51:17 +02:00
parent d4cb9ae8fb
commit 578f88a0e8
5 changed files with 109 additions and 12 deletions

View File

@@ -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.

View File

@@ -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,

View File

@@ -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)
asyncio.run(main(sys.argv[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
View 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"

View 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(