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.
This commit is contained in:
132
tests/test_local_features.py
Normal file
132
tests/test_local_features.py
Normal file
@@ -0,0 +1,132 @@
|
||||
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"}
|
||||
Reference in New Issue
Block a user