""" Local (non-forwarded) implementations of LSP features that Bicep.LangServer (as of 0.46.1) does not implement itself: textDocument/signatureHelp, textDocument/foldingRange, and workspace/symbol. These are intentionally simple, regex/bracket-based implementations — good enough for real editor usage, not a full Bicep parser. They run entirely in the iLSP proxy and never touch the underlying Bicep LS process. """ from __future__ import annotations import re from typing import Any from .bicep_functions import get_signature _DECL_RE = re.compile( r"^\s*(resource|module|var|param|output|func|type)\s+([A-Za-z_][\w]*)", ) _SYMBOL_KIND = { "resource": 6, # Method-ish; LSP doesn't have a great "resource" kind, use Variable-family "module": 2, # Module "var": 13, # Variable "param": 13, # Variable "output": 13, # Variable "func": 12, # Function "type": 5, # Class } # ── signatureHelp ─────────────────────────────────────────────────────────── def _find_enclosing_call(current: str) -> tuple[str, int] | None: """ Walk backwards from the cursor to find the nearest unmatched '(' and the identifier immediately preceding it, returning (function_name, arg_index). Returns None if the cursor isn't inside a recognizable function call. """ depth = 0 comma_count = 0 i = len(current) - 1 in_string = False while i >= 0: ch = current[i] if ch == "'" and (i == 0 or current[i - 1] != "\\"): in_string = not in_string elif not in_string: if ch in ")]}": depth += 1 elif ch in "(": if depth == 0: # Found the opening paren for the innermost call name_m = re.search(r"([A-Za-z_][\w]*)\s*$", current[:i]) if name_m: return name_m.group(1), comma_count return None depth -= 1 elif ch in "[{": depth -= 1 elif ch == "," and depth == 0: comma_count += 1 i -= 1 return None def compute_signature_help(lines: list[str], line: int, character: int) -> dict[str, Any] | None: if line >= len(lines): return None current = lines[line][:character] found = _find_enclosing_call(current) if not found: return None func_name, arg_index = found sig = get_signature(func_name) if not sig: return None params = sig["params"] parameters = [{"label": p} for p in params] if params else [] active_param = min(arg_index, max(len(parameters) - 1, 0)) if parameters else 0 label = f"{func_name}({', '.join(params)})" return { "signatures": [ { "label": label, "documentation": {"kind": "markdown", "value": sig["doc"]}, "parameters": parameters, } ], "activeSignature": 0, "activeParameter": active_param, } # ── foldingRange ──────────────────────────────────────────────────────────── def compute_folding_ranges(lines: list[str]) -> list[dict[str, Any]]: """ Simple stack-based bracket matcher over {}, [], () — collapses any multi-line bracket pair into a folding range. Also folds contiguous leading '//' comment blocks. """ ranges: list[dict[str, Any]] = [] stack: list[tuple[str, int]] = [] in_string = False for lineno, text in enumerate(lines): i = 0 while i < len(text): ch = text[i] if ch == "'" and (i == 0 or text[i - 1] != "\\"): in_string = not in_string elif not in_string: if ch in "{[(": stack.append((ch, lineno)) elif ch in "}])": if stack: _open_ch, open_line = stack.pop() if lineno > open_line: ranges.append({"startLine": open_line, "endLine": lineno, "kind": "region"}) i += 1 # Fold contiguous line-comment blocks (3+ lines) i = 0 while i < len(lines): if lines[i].strip().startswith("//"): start = i while i < len(lines) and lines[i].strip().startswith("//"): i += 1 if i - start >= 3: ranges.append({"startLine": start, "endLine": i - 1, "kind": "comment"}) else: i += 1 ranges.sort(key=lambda r: (r["startLine"], r["endLine"])) return ranges # ── workspace/symbol ──────────────────────────────────────────────────────── def compute_workspace_symbols(docs: dict[str, list[str]], query: str) -> list[dict[str, Any]]: """ Best-effort workspace symbol search over documents currently open in this editor session (iLSP has no full-workspace index — only what the client has opened). Matches top-level resource/module/var/param/output/func/type declarations whose name contains the query (case-insensitive). """ query_l = query.lower() results: list[dict[str, Any]] = [] for uri, lines in docs.items(): for lineno, text in enumerate(lines): m = _DECL_RE.match(text) if not m: continue kind_word, name = m.group(1), m.group(2) if query_l and query_l not in name.lower(): continue start_char = text.index(name, m.end(1)) results.append({ "name": name, "kind": _SYMBOL_KIND.get(kind_word, 13), "location": { "uri": uri, "range": { "start": {"line": lineno, "character": start_char}, "end": {"line": lineno, "character": start_char + len(name)}, }, }, "containerName": kind_word, }) return results