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) ## 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 → +** 1. **Settings → Languages & Frameworks → LSP → Language Servers → +**
- Name: `iLSP Bicep` - Name: `iLSP Bicep`
- Server type: `Command` (not WebSocket — see note below) - Server type: `Command` (not WebSocket — see note below)
- Command: `python3` - Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/bicep`
- Arguments: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge.py 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, > **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: ### Troubleshooting
- File pattern: `*.bicep;*.bicepparam` (include both Bicep file types)
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 Azure Toolkit plugin installed. Without this step, IDEA merges completions from
both LSP4IJ and Azure Toolkit, resulting in noisy suggestions (`resource`, both LSP4IJ and Azure Toolkit, resulting in noisy suggestions (`resource`,
`projectName`, Bicep schema types, etc.) appearing alongside iLSP results. `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)), "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 { return {
"type": "param", "type": "param",
"module": mod_name, "module": mod_name,

View File

@@ -17,6 +17,7 @@ frames straight to stdout.
import asyncio import asyncio
import ssl import ssl
import sys import sys
import traceback
import websockets import websockets
@@ -71,8 +72,9 @@ async def main(uri: str) -> None:
if msg is None: if msg is None:
break break
await ws.send(msg) await ws.send(msg)
except Exception: except Exception as e:
pass print(f"stdin_to_ws error: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
async def ws_to_stdout() -> None: async def ws_to_stdout() -> None:
try: try:
@@ -80,8 +82,9 @@ async def main(uri: str) -> None:
data = frame if isinstance(frame, bytes) else frame.encode() data = frame if isinstance(frame, bytes) else frame.encode()
sys.stdout.buffer.write(data) sys.stdout.buffer.write(data)
sys.stdout.buffer.flush() sys.stdout.buffer.flush()
except Exception: except Exception as e:
pass 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()) await asyncio.gather(stdin_to_ws(), ws_to_stdout())
@@ -90,4 +93,9 @@ if __name__ == "__main__":
if len(sys.argv) < 2: if len(sys.argv) < 2:
print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr) print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr)
sys.exit(1) 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 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(): def test_param_value_items_from_catalog_allowed():
"""environmentType completions come from catalog 'allowed' field.""" """environmentType completions come from catalog 'allowed' field."""
BicepModuleCatalog._modules = [_make_module( BicepModuleCatalog._modules = [_make_module(