Files
iLSP/tests/test_local_features.py
Henrik Jess Nielsen d7db53a482
All checks were successful
Build and Deploy iLSP / test (push) Successful in 22s
Build and Deploy iLSP / build-and-deploy (push) Successful in 1m26s
feat(bicep): implement signatureHelp, foldingRange, workspace/symbol locally
Bicep.LangServer 0.46.1 (latest release) does not implement these three LSP
features, so iLSP now answers them itself directly in the proxy instead of
forwarding to the underlying LS:

- textDocument/signatureHelp: curated database of ~50 commonly used Bicep
  built-in functions (resourceId, guid, concat, take, format, ...) with a
  simple bracket-depth call-context detector (ilsp/bicep_lsp/bicep_functions.py,
  ilsp/bicep_lsp/local_features.py:compute_signature_help).
- textDocument/foldingRange: stack-based bracket matcher over {}/[]/() plus
  contiguous // comment-block folding (compute_folding_ranges).
- workspace/symbol: best-effort search over documents currently open in the
  editor session (iLSP has no full-workspace index, only what's been opened),
  matching top-level resource/module/var/param/output/func/type declarations
  (compute_workspace_symbols).

The proxy now advertises these in the initialize response capabilities
(signatureHelpProvider, foldingRangeProvider, workspaceSymbolProvider) so
editors actually send the requests, and intercepts them in _client_to_ls
before they'd otherwise be silently dropped/errored by the real Bicep LS.
A send lock (_LockedConn) prevents interleaved frames now that both
_client_to_ls and _ls_to_client can write responses to the client socket.

13 new unit tests in tests/test_local_features.py. Closes the last 3 gaps
found in the full LSP capability audit.
2026-08-14 17:54:34 +02:00

133 lines
4.4 KiB
Python

import json
from ilsp.bicep_lsp.local_features import (
compute_folding_ranges,
compute_signature_help,
compute_workspace_symbols,
)
# ── signatureHelp ────────────────────────────────────────────────────────────
def test_signature_help_simple_call():
lines = ["var id = resourceId('Microsoft.Storage/storageAccounts', "]
result = compute_signature_help(lines, 0, len(lines[0]))
assert result is not None
sig = result["signatures"][0]
assert sig["label"].startswith("resourceId(")
assert result["activeParameter"] == 1
def test_signature_help_first_arg():
lines = ["var g = guid("]
result = compute_signature_help(lines, 0, len(lines[0]))
assert result is not None
assert result["activeParameter"] == 0
assert result["signatures"][0]["label"].startswith("guid(")
def test_signature_help_unknown_function_returns_none():
lines = ["var x = notARealFunction("]
result = compute_signature_help(lines, 0, len(lines[0]))
assert result is None
def test_signature_help_outside_call_returns_none():
lines = ["var x = 'hello world'"]
result = compute_signature_help(lines, 0, len(lines[0]))
assert result is None
def test_signature_help_nested_call_uses_innermost():
lines = ["var x = concat(toLower("]
result = compute_signature_help(lines, 0, len(lines[0]))
assert result is not None
assert result["signatures"][0]["label"].startswith("toLower(")
# ── foldingRange ─────────────────────────────────────────────────────────────
def test_folding_range_multiline_object():
lines = [
"resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {",
" name: 'demo'",
" sku: {",
" name: 'Standard_LRS'",
" }",
"}",
]
ranges = compute_folding_ranges(lines)
assert {"startLine": 0, "endLine": 5, "kind": "region"} in ranges
assert {"startLine": 2, "endLine": 4, "kind": "region"} in ranges
def test_folding_range_ignores_single_line_brackets():
lines = ["var x = { a: 1 }"]
ranges = compute_folding_ranges(lines)
assert ranges == []
def test_folding_range_comment_block():
lines = [
"// line one",
"// line two",
"// line three",
"var x = 1",
]
ranges = compute_folding_ranges(lines)
assert {"startLine": 0, "endLine": 2, "kind": "comment"} in ranges
def test_folding_range_short_comment_block_not_folded():
lines = ["// only one line", "var x = 1"]
ranges = compute_folding_ranges(lines)
assert not any(r["kind"] == "comment" for r in ranges)
# ── workspace/symbol ─────────────────────────────────────────────────────────
def test_workspace_symbol_finds_declarations():
docs = {
"file:///a.bicep": [
"param location string = 'westeurope'",
"var storageName = 'demo'",
"resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {",
" name: storageName",
"}",
"module roleAssignment 'br/modules:modules/roleassignments:2.0.x' = {",
" name: 'ra'",
"}",
],
}
results = compute_workspace_symbols(docs, "")
names = {r["name"] for r in results}
assert names == {"location", "storageName", "sa", "roleAssignment"}
def test_workspace_symbol_filters_by_query():
docs = {
"file:///a.bicep": [
"var storageName = 'demo'",
"var otherThing = 'x'",
],
}
results = compute_workspace_symbols(docs, "storage")
assert len(results) == 1
assert results[0]["name"] == "storageName"
def test_workspace_symbol_query_is_case_insensitive():
docs = {"file:///a.bicep": ["var StorageName = 'demo'"]}
results = compute_workspace_symbols(docs, "storagename")
assert len(results) == 1
def test_workspace_symbol_searches_across_multiple_documents():
docs = {
"file:///a.bicep": ["var fromA = 1"],
"file:///b.bicep": ["var fromB = 2"],
}
results = compute_workspace_symbols(docs, "")
names = {r["name"] for r in results}
assert names == {"fromA", "fromB"}