Delegate TCL folding to server, fix bugs, and enhance stability

- Move TCL folding range computation from client to language server, avoiding duplicate regions.
- Serialize language server restarts to prevent multiple server instances running concurrently.
- Pin server-side Python dependencies to specific minor versions for improved stability and predictability.
- Make debug server connection non-fatal, allowing the language server to start even if the debugger isn't attached.
- Optimize semantic token generation by caching Tclint plugin commands.
This commit is contained in:
Christoph Brandau
2026-06-18 20:38:47 +02:00
parent 3ad3847045
commit f60c4563e4
7 changed files with 74 additions and 112 deletions
+6 -3
View File
@@ -4,9 +4,12 @@ version = "0.1.0"
description = "Python language server for NX Postprocessor Support"
requires-python = ">=3.8"
dependencies = [
"pygls",
"packaging",
"tclint",
# Upper bounds guard against breaking API changes in bundled deps. The code
# is written against tclint 0.8.x (see tools/semantic_tokens.py); pin it so a
# re-bundle can't silently pull an incompatible major/minor.
"pygls>=1.3,<2",
"packaging>=24,<27",
"tclint>=0.8,<0.9",
]
[dependency-groups]
+11 -1
View File
@@ -27,7 +27,17 @@ if debugger_path:
# 5678 is the default port, If you need to change it update it here
# and in launch.json.
debugpy.connect(5678)
# Connecting requires the "Python debug server" listener (launch.json) to be
# up first. If it isn't (e.g. wrong launch config was used), don't crash the
# whole language server - just continue running without the debugger attached.
try:
debugpy.connect(5678)
except (ConnectionRefusedError, OSError) as exc:
print(
f"debugpy: could not connect to debug adapter on port 5678 "
f"({exc}); continuing without debugging.",
file=sys.stderr,
)
# This will ensure that execution is paused as soon as the debugger
# connects to VS Code. If you don't want to pause here comment this
+18 -1
View File
@@ -7,6 +7,23 @@ from common.load_data import standard_items
import lsprotocol.types as lsp
# Constructing a PluginManager scans entry points, and get_commands() rebuilds
# the builtin command set on every call. Semantic tokens are requested often, so
# cache the manager and the resolved commands per plugin set.
_PLUGIN_MANAGER = None
_COMMANDS_CACHE = {}
def _load_commands(plugins):
global _PLUGIN_MANAGER
if _PLUGIN_MANAGER is None:
_PLUGIN_MANAGER = PluginManager()
key = tuple(plugins)
if key not in _COMMANDS_CACHE:
_COMMANDS_CACHE[key] = _PLUGIN_MANAGER.get_commands(list(plugins))
return _COMMANDS_CACHE[key]
class TokenModifier(enum.IntFlag):
deprecated = enum.auto()
readonly = enum.auto()
@@ -47,7 +64,7 @@ TOKEN_TYPES = [
class _Highlighter(Visitor):
def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]):
self._commands = PluginManager().get_commands(plugins)
self._commands = _load_commands(plugins)
self._tokens = []
self.custom_functions = custom_functions