From 578f88a0e86ad70756c3c54f51097bb67fcc41d1 Mon Sep 17 00:00:00 2001 From: Henrik Jess Nielsen Date: Tue, 19 May 2026 13:51:17 +0200 Subject: [PATCH] fix(bicep): support multi-line array completion for roles and other enums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- EDITOR_SETUP.md | 42 ++++++++++++++++++++++++++++++------- ilsp/bicep_lsp/proxy.py | 25 ++++++++++++++++++++++ scripts/lsp_bridge.py | 18 +++++++++++----- scripts/lsp_bridge_debug.sh | 12 +++++++++++ tests/test_proxy.py | 24 +++++++++++++++++++++ 5 files changed, 109 insertions(+), 12 deletions(-) create mode 100755 scripts/lsp_bridge_debug.sh diff --git a/EDITOR_SETUP.md b/EDITOR_SETUP.md index 9a8b7e7..7b7c526 100644 --- a/EDITOR_SETUP.md +++ b/EDITOR_SETUP.md @@ -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. diff --git a/ilsp/bicep_lsp/proxy.py b/ilsp/bicep_lsp/proxy.py index aa19181..c618dc8 100644 --- a/ilsp/bicep_lsp/proxy.py +++ b/ilsp/bicep_lsp/proxy.py @@ -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, diff --git a/scripts/lsp_bridge.py b/scripts/lsp_bridge.py index f68b2ed..413859e 100755 --- a/scripts/lsp_bridge.py +++ b/scripts/lsp_bridge.py @@ -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 ", 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) diff --git a/scripts/lsp_bridge_debug.sh b/scripts/lsp_bridge_debug.sh new file mode 100755 index 0000000..e52a3ea --- /dev/null +++ b/scripts/lsp_bridge_debug.sh @@ -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" diff --git a/tests/test_proxy.py b/tests/test_proxy.py index a1d762b..091bde8 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -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(