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:
+5
-1
@@ -12,4 +12,8 @@
|
|||||||
|
|
||||||
## [0.2.0]
|
## [0.2.0]
|
||||||
|
|
||||||
- Add DEF File Support
|
- Add DEF File Support
|
||||||
|
|
||||||
|
## [2026.6.100]
|
||||||
|
|
||||||
|
- Fix several bugs
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
+34
-31
@@ -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,26 +221,27 @@ 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"
|
||||||
vscode.workspace.onDidOpenTextDocument((document) => {
|
context.subscriptions.push(
|
||||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
vscode.workspace.onDidOpenTextDocument((document) => {
|
||||||
if (document.languageId === "cdl") {
|
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||||
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
|
if (document.languageId === "cdl") {
|
||||||
} else if (document.languageId === "def") {
|
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
|
||||||
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
|
} else if (document.languageId === "def") {
|
||||||
|
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
})
|
vscode.workspace.onDidChangeTextDocument((event) => {
|
||||||
|
const document = event.document
|
||||||
vscode.workspace.onDidChangeTextDocument((event) => {
|
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||||
const document = event.document
|
if (document.languageId === "cdl") {
|
||||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
|
||||||
if (document.languageId === "cdl") {
|
} else if (document.languageId === "def") {
|
||||||
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
|
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
|
||||||
} else if (document.languageId === "def") {
|
}
|
||||||
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate(): Thenable<void> | undefined {
|
export function deactivate(): Thenable<void> | undefined {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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.
|
||||||
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
|
# 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user