feat(bicep): implement signatureHelp, foldingRange, workspace/symbol locally
All checks were successful
Build and Deploy iLSP / test (push) Successful in 22s
Build and Deploy iLSP / build-and-deploy (push) Successful in 1m26s

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:
2026-08-14 17:54:34 +02:00
parent dcdf22dac3
commit d7db53a482
4 changed files with 632 additions and 8 deletions

View File

@@ -0,0 +1,249 @@
"""
Curated signature database for Bicep's built-in functions.
Bicep.LangServer (as of 0.46.1) does not implement textDocument/signatureHelp,
so iLSP provides its own for the most commonly used built-in functions.
Not exhaustive — covers the functions people actually type day to day.
"""
from typing import Any
# name -> list of parameter labels (one entry per overload, simplest form first)
BUILTIN_FUNCTIONS: dict[str, dict[str, Any]] = {
"resourceId": {
"params": ["resourceType", "resourceName1", "resourceName2..."],
"doc": "Returns the unique identifier of a resource. Also usable with a subscriptionId/resourceGroupName prefix.",
},
"subscriptionResourceId": {
"params": ["subscriptionId", "resourceType", "resourceName1", "resourceName2..."],
"doc": "Returns the unique identifier for a resource deployed at the subscription level.",
},
"tenantResourceId": {
"params": ["resourceType", "resourceName1", "resourceName2..."],
"doc": "Returns the unique identifier for a resource deployed at the tenant level.",
},
"extensionResourceId": {
"params": ["resourceId", "resourceType", "resourceName1", "resourceName2..."],
"doc": "Returns the resource ID for an extension resource, which is applied to another resource.",
},
"guid": {
"params": ["baseString1", "baseString2..."],
"doc": "Creates a deterministic GUID based on the values passed as parameters.",
},
"uniqueString": {
"params": ["baseString1", "baseString2..."],
"doc": "Creates a deterministic hash string based on the values passed as parameters (13 chars).",
},
"take": {
"params": ["originalValue", "count"],
"doc": "Returns a string or array with the specified number of elements from the start.",
},
"skip": {
"params": ["originalValue", "count"],
"doc": "Returns a string or array with all the elements after the specified number skipped.",
},
"first": {
"params": ["arg1"],
"doc": "Returns the first element of an array, or first character of a string.",
},
"last": {
"params": ["arg1"],
"doc": "Returns the last element of an array, or last character of a string.",
},
"length": {
"params": ["arg1"],
"doc": "Returns the number of elements in an array, characters in a string, or properties in an object.",
},
"concat": {
"params": ["arg1", "arg2..."],
"doc": "Combines multiple arrays or strings.",
},
"union": {
"params": ["arg1", "arg2..."],
"doc": "Returns a single array/object with all elements from the parameters, merging objects.",
},
"contains": {
"params": ["container", "itemToFind"],
"doc": "Checks whether an array contains a value, an object contains a key, or a string contains a substring.",
},
"empty": {
"params": ["itemToTest"],
"doc": "Determines if an array, object, or string is empty.",
},
"format": {
"params": ["formatString", "arg1..."],
"doc": "Creates a formatted string using .NET-style {0} placeholders.",
},
"join": {
"params": ["inputArray", "delimiter"],
"doc": "Joins multiple strings from an array into a single string, separated by delimiter.",
},
"split": {
"params": ["inputString", "delimiter"],
"doc": "Splits a string into an array using a delimiter.",
},
"replace": {
"params": ["originalString", "oldString", "newString"],
"doc": "Returns a new string with all instances of oldString replaced by newString.",
},
"toLower": {
"params": ["stringToChange"],
"doc": "Converts a string to lowercase.",
},
"toUpper": {
"params": ["stringToChange"],
"doc": "Converts a string to uppercase.",
},
"trim": {
"params": ["stringToTrim"],
"doc": "Removes leading/trailing whitespace from a string.",
},
"substring": {
"params": ["stringToParse", "startIndex", "length"],
"doc": "Returns a substring starting at the specified position.",
},
"padLeft": {
"params": ["valueToPad", "totalLength", "paddingCharacter"],
"doc": "Pads a string to a fixed total length by adding characters to the left.",
},
"startsWith": {
"params": ["stringToSearch", "stringToFind"],
"doc": "Checks whether a string starts with a value (case-insensitive).",
},
"endsWith": {
"params": ["stringToSearch", "stringToFind"],
"doc": "Checks whether a string ends with a value (case-insensitive).",
},
"indexOf": {
"params": ["stringToSearch", "stringToFind"],
"doc": "Returns the first position of a value within a string (case-insensitive).",
},
"reference": {
"params": ["resourceNameOrIdentifier", "apiVersion", "'Full'"],
"doc": "Returns an object representing a resource's runtime state.",
},
"listKeys": {
"params": ["resourceNameOrIdentifier", "apiVersion"],
"doc": "Returns the keys for a resource (e.g. storage account access keys).",
},
"listSecrets": {
"params": ["resourceNameOrIdentifier", "apiVersion"],
"doc": "Returns the secrets for a resource.",
},
"resourceGroup": {
"params": [],
"doc": "Returns an object representing the current resource group.",
},
"subscription": {
"params": [],
"doc": "Returns an object representing the current subscription.",
},
"deployment": {
"params": [],
"doc": "Returns an object representing the current deployment operation.",
},
"environment": {
"params": [],
"doc": "Returns an object describing the Azure cloud environment.",
},
"az": {
"params": [],
"doc": "Namespace for Azure Bicep built-in functions.",
},
"json": {
"params": ["jsonString"],
"doc": "Converts a valid JSON string into a JSON object / array.",
},
"string": {
"params": ["valueToConvert"],
"doc": "Converts the specified value to a string.",
},
"int": {
"params": ["valueToConvert"],
"doc": "Converts the specified value to an integer.",
},
"bool": {
"params": ["value"],
"doc": "Converts the specified value to a boolean.",
},
"array": {
"params": ["valueToConvert"],
"doc": "Converts the specified value to an array.",
},
"base64": {
"params": ["inputString"],
"doc": "Returns the base64 representation of the input string.",
},
"base64ToString": {
"params": ["base64Value"],
"doc": "Converts a base64 representation to a string.",
},
"dateTimeAdd": {
"params": ["base", "duration", "format"],
"doc": "Adds a time duration to a base UTC datetime value.",
},
"utcNow": {
"params": ["format"],
"doc": "Returns the current UTC datetime in the specified format.",
},
"loadTextContent": {
"params": ["filePath", "encoding"],
"doc": "Loads the content of the specified file as a string at compile time.",
},
"loadJsonContent": {
"params": ["filePath", "jsonPath", "encoding"],
"doc": "Loads the content of the specified JSON file at compile time.",
},
"loadFileAsBase64": {
"params": ["filePath"],
"doc": "Loads the specified file as a base64 string at compile time.",
},
"managementGroup": {
"params": ["name"],
"doc": "Returns an object representing the current or specified management group.",
},
"newGuid": {
"params": [],
"doc": "Returns a new (random, non-deterministic) GUID. Only valid for param defaults.",
},
"filter": {
"params": ["array", "lambdaExpression"],
"doc": "Filters an array using a custom filtering lambda expression.",
},
"map": {
"params": ["array", "lambdaExpression"],
"doc": "Applies a custom mapping lambda expression to each element of an array.",
},
"reduce": {
"params": ["array", "initialValue", "lambdaExpression"],
"doc": "Reduces an array with a custom lambda expression.",
},
"sort": {
"params": ["array", "lambdaExpression"],
"doc": "Sorts an array with a custom lambda expression comparator.",
},
"min": {
"params": ["arg1", "arg2..."],
"doc": "Returns the minimum value from an array of integers, or a comma-separated list.",
},
"max": {
"params": ["arg1", "arg2..."],
"doc": "Returns the maximum value from an array of integers, or a comma-separated list.",
},
"range": {
"params": ["startIndex", "count"],
"doc": "Creates an array of integers from startIndex to startIndex+count-1.",
},
"items": {
"params": ["object"],
"doc": "Returns an array of key/value pair objects that represent the properties of an object.",
},
"pickZones": {
"params": ["providerNamespace", "resourceType", "location", "numberOfZones", "offset"],
"doc": "Determines which availability zones are recommended for a resource type/location.",
},
}
def get_signature(name: str) -> dict[str, Any] | None:
return BUILTIN_FUNCTIONS.get(name)