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:
249
ilsp/bicep_lsp/bicep_functions.py
Normal file
249
ilsp/bicep_lsp/bicep_functions.py
Normal 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)
|
||||
177
ilsp/bicep_lsp/local_features.py
Normal file
177
ilsp/bicep_lsp/local_features.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
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
|
||||
@@ -27,6 +27,11 @@ import subprocess
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
from .local_features import (
|
||||
compute_folding_ranges,
|
||||
compute_signature_help,
|
||||
compute_workspace_symbols,
|
||||
)
|
||||
from .modules import BicepModuleCatalog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -265,8 +270,26 @@ def _inject_completions(msg: dict[str, Any], context: dict | None = None) -> byt
|
||||
return json.dumps(msg).encode()
|
||||
|
||||
|
||||
def _local_response(msg_id, result: Any) -> bytes:
|
||||
return json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": result}).encode()
|
||||
|
||||
|
||||
class _LockedConn:
|
||||
"""Wraps a socket so concurrent writers (locally-answered responses vs.
|
||||
forwarded LS responses) never interleave partial frames on the wire."""
|
||||
|
||||
def __init__(self, conn: socket.socket, lock: threading.Lock) -> None:
|
||||
self._conn = conn
|
||||
self._lock = lock
|
||||
|
||||
def sendall(self, data: bytes) -> None:
|
||||
with self._lock:
|
||||
self._conn.sendall(data)
|
||||
|
||||
|
||||
def _client_to_ls(
|
||||
conn_file,
|
||||
conn: socket.socket,
|
||||
proc_stdin,
|
||||
session: _ProxySession,
|
||||
) -> None:
|
||||
@@ -276,6 +299,7 @@ def _client_to_ls(
|
||||
logger.debug("Client→LS: %d bytes", len(body))
|
||||
|
||||
# Track document state and completion context (never block forwarding)
|
||||
forward = True
|
||||
try:
|
||||
msg = json.loads(body)
|
||||
method = msg.get("method", "")
|
||||
@@ -291,13 +315,37 @@ def _client_to_ls(
|
||||
session.update_doc(uri, changes[-1].get("text", ""))
|
||||
elif method == "textDocument/completion":
|
||||
session.record_completion_request(msg)
|
||||
elif method == "textDocument/signatureHelp":
|
||||
# Bicep.LangServer doesn't implement this — answer locally.
|
||||
forward = False
|
||||
params = msg.get("params", {})
|
||||
uri = params.get("textDocument", {}).get("uri", "")
|
||||
pos = params.get("position", {})
|
||||
lines = session.docs.get(uri, [])
|
||||
result = compute_signature_help(lines, pos.get("line", 0), pos.get("character", 0))
|
||||
conn.sendall(_frame(_local_response(msg.get("id"), result)))
|
||||
elif method == "textDocument/foldingRange":
|
||||
# Bicep.LangServer doesn't implement this — answer locally.
|
||||
forward = False
|
||||
uri = msg.get("params", {}).get("textDocument", {}).get("uri", "")
|
||||
lines = session.docs.get(uri, [])
|
||||
result = compute_folding_ranges(lines)
|
||||
conn.sendall(_frame(_local_response(msg.get("id"), result)))
|
||||
elif method == "workspace/symbol":
|
||||
# Bicep.LangServer doesn't implement this — answer locally,
|
||||
# scoped to documents open in this editor session.
|
||||
forward = False
|
||||
query = msg.get("params", {}).get("query", "")
|
||||
result = compute_workspace_symbols(session.docs, query)
|
||||
conn.sendall(_frame(_local_response(msg.get("id"), result)))
|
||||
except Exception:
|
||||
pass # parsing errors must never block forwarding
|
||||
forward = True # parsing/handling errors must never block forwarding
|
||||
|
||||
framed = _frame(body)
|
||||
proc_stdin.write(framed)
|
||||
proc_stdin.flush()
|
||||
logger.debug("Client→LS: flushed")
|
||||
if forward:
|
||||
framed = _frame(body)
|
||||
proc_stdin.write(framed)
|
||||
proc_stdin.flush()
|
||||
logger.debug("Client→LS: flushed")
|
||||
except EOFError:
|
||||
logger.debug("Client write side closed — signalling EOF to LS")
|
||||
except Exception as exc:
|
||||
@@ -327,7 +375,19 @@ def _ls_to_client(
|
||||
# textDocument/completion request we tracked — any other
|
||||
# response (hover, definition, references, documentSymbol,
|
||||
# formatting, codeAction, ...) must pass through untouched.
|
||||
out = _inject_completions(msg, context) if context is not None else body
|
||||
if context is not None:
|
||||
out = _inject_completions(msg, context)
|
||||
elif msg.get("method") is None and isinstance(msg.get("result"), dict) and "capabilities" in msg.get("result", {}):
|
||||
# This is the initialize response — advertise the extra
|
||||
# capabilities iLSP answers locally (Bicep.LangServer
|
||||
# itself doesn't implement these).
|
||||
caps = msg["result"]["capabilities"]
|
||||
caps["signatureHelpProvider"] = {"triggerCharacters": ["(", ","]}
|
||||
caps["foldingRangeProvider"] = True
|
||||
caps["workspaceSymbolProvider"] = True
|
||||
out = json.dumps(msg).encode()
|
||||
else:
|
||||
out = body
|
||||
except json.JSONDecodeError:
|
||||
out = body
|
||||
conn.sendall(_frame(out))
|
||||
@@ -351,16 +411,22 @@ def _handle_client(conn: socket.socket, addr: tuple) -> None:
|
||||
# Unbuffered read from the socket — critical for correct LSP framing
|
||||
conn_file = conn.makefile("rb", buffering=0)
|
||||
|
||||
# t1 may write locally-answered responses (signatureHelp/foldingRange/
|
||||
# workspace-symbol) to conn concurrently with t2's LS responses — a lock
|
||||
# prevents interleaved/corrupted frames on the wire.
|
||||
send_lock = threading.Lock()
|
||||
locked_conn = _LockedConn(conn, send_lock)
|
||||
|
||||
# t1: client → LS (finishes when client closes write side)
|
||||
t1 = threading.Thread(
|
||||
target=_client_to_ls,
|
||||
args=(conn_file, proc.stdin, session),
|
||||
args=(conn_file, locked_conn, proc.stdin, session),
|
||||
daemon=True,
|
||||
)
|
||||
# t2: LS → client (finishes when LS closes stdout)
|
||||
t2 = threading.Thread(
|
||||
target=_ls_to_client,
|
||||
args=(proc.stdout, conn, session),
|
||||
args=(proc.stdout, locked_conn, session),
|
||||
daemon=True,
|
||||
)
|
||||
t1.start()
|
||||
|
||||
Reference in New Issue
Block a user