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
+4
View File
@@ -13,3 +13,7 @@
## [0.2.0] ## [0.2.0]
- Add DEF File Support - Add DEF File Support
## [2026.6.100]
- Fix several bugs
-70
View File
@@ -1,70 +0,0 @@
import * as vscode from "vscode"
/**
* Computes folding ranges for a TCL document based on brace matching.
*
* Handles same-line opening braces (`... {`), standalone `{`, `} elseif`/`} else`
* branch continuations and line continuations (`\`).
*/
export function computeTclFoldingRanges(document: vscode.TextDocument): vscode.FoldingRange[] {
const ranges: vscode.FoldingRange[] = []
const stack: number[] = []
let pendingStartLine: number | null = null
for (let i = 0; i < document.lineCount; i += 1) {
const line = document.lineAt(i).text
const trimmed = line.trim()
const branchContinuation = /^\}\s*(elseif|else)\b/.test(trimmed)
const sameLineOpen = trimmed.endsWith("{")
if (!trimmed || trimmed.startsWith("#")) {
continue
}
if (trimmed.startsWith("}")) {
const startLine = stack.pop()
const endLine = branchContinuation ? i - 1 : i
if (typeof startLine === "number" && startLine < endLine) {
ranges.push(
new vscode.FoldingRange(startLine, endLine, vscode.FoldingRangeKind.Region)
)
}
if (branchContinuation && sameLineOpen) {
stack.push(i)
pendingStartLine = null
} else if (trimmed !== "}" && trimmed.endsWith("\\")) {
pendingStartLine = i
} else {
pendingStartLine = null
}
continue
}
if (trimmed === "{") {
const startLine = pendingStartLine ?? i
if (startLine < i) {
stack.push(startLine)
} else {
stack.push(i)
}
pendingStartLine = null
continue
}
if (trimmed.endsWith("{")) {
stack.push(i)
pendingStartLine = null
continue
}
if (trimmed.endsWith("\\")) {
pendingStartLine = i
continue
}
pendingStartLine = null
}
return ranges
}
+18 -15
View File
@@ -15,7 +15,6 @@ import {
cdlDocumentSymbolProvider, cdlDocumentSymbolProvider,
defDocumentSymbolProvider defDocumentSymbolProvider
} from "./common/handlers" } from "./common/handlers"
import { computeTclFoldingRanges } from "./common/folding"
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging" import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
import { import {
checkVersion, checkVersion,
@@ -30,7 +29,7 @@ import { loadServerDefaults } from "./common/setup"
import { getLSClientTraceLevel } from "./common/utilities" import { getLSClientTraceLevel } from "./common/utilities"
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi" import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
let client: LanguageClient let client: LanguageClient | undefined
export async function activate(context: vscode.ExtensionContext) { export async function activate(context: vscode.ExtensionContext) {
// This is required to get server name and module. This should be // This is required to get server name and module. This should be
@@ -62,7 +61,7 @@ export async function activate(context: vscode.ExtensionContext) {
traceLog(`Module: ${serverInfo.module}`) traceLog(`Module: ${serverInfo.module}`)
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`) traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`)
const runServer = async () => { const runServerImpl = async () => {
const interpreter = getInterpreterFromSetting(serverId) const interpreter = getInterpreterFromSetting(serverId)
if (interpreter && interpreter.length > 0) { if (interpreter && interpreter.length > 0) {
if (checkVersion(await resolveInterpreter(interpreter))) { if (checkVersion(await resolveInterpreter(interpreter))) {
@@ -91,6 +90,16 @@ export async function activate(context: vscode.ExtensionContext) {
) )
} }
// Serialize server (re)starts. Overlapping triggers (interpreter change,
// config change, restart command, initial activation) would otherwise each
// read the stale module-level `client`, start a new server and leave the
// previous one running orphaned -> hints/hover shown multiple times.
let runServerQueue: Promise<void> = Promise.resolve()
const runServer = () => {
runServerQueue = runServerQueue.catch(() => undefined).then(() => runServerImpl())
return runServerQueue
}
context.subscriptions.push( context.subscriptions.push(
onDidChangePythonInterpreter(async () => { onDidChangePythonInterpreter(async () => {
await runServer() await runServer()
@@ -116,16 +125,9 @@ export async function activate(context: vscode.ExtensionContext) {
} }
}) })
// Folding ranges for TCL (brace based) // Folding ranges for TCL are provided by the language server
const tclFoldingProvider = vscode.languages.registerFoldingRangeProvider( // (folding_range_provider in lsp_server.py). No client-side provider here to
[{ language: "tcl" }], // avoid duplicate folding regions.
{
provideFoldingRanges(document: vscode.TextDocument) {
return computeTclFoldingRanges(document)
}
}
)
context.subscriptions.push(tclFoldingProvider)
// //
const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider( const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider(
@@ -219,6 +221,7 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef) context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef)
// Check if the first line of the CDL file contains "MACHINE" // Check if the first line of the CDL file contains "MACHINE"
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((document) => { vscode.workspace.onDidOpenTextDocument((document) => {
if (document.languageId === "cdl" || document.languageId === "def") { if (document.languageId === "cdl" || document.languageId === "def") {
if (document.languageId === "cdl") { if (document.languageId === "cdl") {
@@ -227,8 +230,7 @@ export async function activate(context: vscode.ExtensionContext) {
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document)) diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
} }
} }
}) }),
vscode.workspace.onDidChangeTextDocument((event) => { vscode.workspace.onDidChangeTextDocument((event) => {
const document = event.document const document = event.document
if (document.languageId === "cdl" || document.languageId === "def") { if (document.languageId === "cdl" || document.languageId === "def") {
@@ -239,6 +241,7 @@ export async function activate(context: vscode.ExtensionContext) {
} }
} }
}) })
)
} }
export function deactivate(): Thenable<void> | undefined { export function deactivate(): Thenable<void> | undefined {
-5
View File
@@ -134,10 +134,5 @@
"esbuild": "^0.25.6", "esbuild": "^0.25.6",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "^5.7.2" "typescript": "^5.7.2"
},
"__metadata": {
"installedTimestamp": 1776513675781,
"targetPlatform": "undefined",
"size": 3267242
} }
} }
+6 -3
View File
@@ -4,9 +4,12 @@ version = "0.1.0"
description = "Python language server for NX Postprocessor Support" description = "Python language server for NX Postprocessor Support"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = [ dependencies = [
"pygls", # Upper bounds guard against breaking API changes in bundled deps. The code
"packaging", # is written against tclint 0.8.x (see tools/semantic_tokens.py); pin it so a
"tclint", # re-bundle can't silently pull an incompatible major/minor.
"pygls>=1.3,<2",
"packaging>=24,<27",
"tclint>=0.8,<0.9",
] ]
[dependency-groups] [dependency-groups]
+10
View File
@@ -27,7 +27,17 @@ if debugger_path:
# 5678 is the default port, If you need to change it update it here # 5678 is the default port, If you need to change it update it here
# and in launch.json. # and in launch.json.
# 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) 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 # 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 # 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 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): class TokenModifier(enum.IntFlag):
deprecated = enum.auto() deprecated = enum.auto()
readonly = enum.auto() readonly = enum.auto()
@@ -47,7 +64,7 @@ TOKEN_TYPES = [
class _Highlighter(Visitor): class _Highlighter(Visitor):
def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]): 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._tokens = []
self.custom_functions = custom_functions self.custom_functions = custom_functions