Compare commits
62
Commits
0.4.14
..
2025.8.400
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068cf2d1c3 | ||
|
|
ad18a3dcf3 | ||
|
|
bd4b17825c | ||
|
|
5d6182e91f | ||
|
|
63e9c429bd | ||
|
|
7fdafe484e | ||
|
|
5cba6763f1 | ||
|
|
e20bb85ac6 | ||
|
|
98a0b2197d | ||
|
|
579847b56d | ||
|
|
618d6b478b | ||
|
|
480978d6bb | ||
|
|
04798328bd | ||
|
|
ae835089b6 | ||
|
|
0ed1e887fc | ||
|
|
12b7b62aa5 | ||
|
|
7263b6f530 | ||
|
|
c0635e48ea | ||
|
|
8fb98d2f40 | ||
|
|
f24b280edf | ||
|
|
b61cff3b3b | ||
|
|
cf62044d3e | ||
|
|
feba88212f | ||
|
|
aeebb5c964 | ||
|
|
b141818251 | ||
|
|
72f894b05d | ||
|
|
0a26cb5c0f | ||
|
|
b3cac5c292 | ||
|
|
ef558c3aa3 | ||
|
|
9d5ef23162 | ||
|
|
f659db1147 | ||
|
|
c25bf79427 | ||
|
|
b059a75205 | ||
|
|
a90e15514b | ||
|
|
2c2579319d | ||
|
|
b38fab1428 | ||
|
|
bc745622b9 | ||
|
|
0bb4ef731e | ||
|
|
27e2b3bf6e | ||
|
|
f55c3ba904 | ||
|
|
cb430a5553 | ||
|
|
637a28ad82 | ||
|
|
499df9b51d | ||
|
|
31ccae9a40 | ||
|
|
9b373e85b7 | ||
|
|
0c5f4ea6fb | ||
|
|
793b35f39f | ||
|
|
3780fc27dc | ||
|
|
6541830ef6 | ||
|
|
509b13e99f | ||
|
|
396cfdc4ec | ||
|
|
ccd22e4343 | ||
|
|
7d83ae1012 | ||
|
|
029eaebd57 | ||
|
|
2a2db8f217 | ||
|
|
8be9aae05d | ||
|
|
6bded783a1 | ||
|
|
f5c61267e3 | ||
|
|
b680c01989 | ||
|
|
1abf89fe56 | ||
|
|
6a051a6dee | ||
|
|
d22d897d23 |
@@ -1,13 +1,15 @@
|
|||||||
on:
|
on:
|
||||||
push:
|
release:
|
||||||
tags:
|
types: [published]
|
||||||
- "*"
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_publish:
|
build_and_publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout branch from release
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.release.target_commitish }}
|
||||||
|
fetch-depth: 0
|
||||||
- name: GitHub Tag Name example
|
- name: GitHub Tag Name example
|
||||||
run: |
|
run: |
|
||||||
echo "Tag name from GITHUB_REF_NAME: $GITHUB_REF_NAME"
|
echo "Tag name from GITHUB_REF_NAME: $GITHUB_REF_NAME"
|
||||||
@@ -33,9 +35,32 @@ jobs:
|
|||||||
git config user.name "${{ vars.USERNAME_GIT }}"
|
git config user.name "${{ vars.USERNAME_GIT }}"
|
||||||
git config user.email "${{ vars.EMAIL_GIT }}"
|
git config user.email "${{ vars.EMAIL_GIT }}"
|
||||||
git add package.json
|
git add package.json
|
||||||
git commit -m "Update version to ${{ github.ref_name }}"
|
git commit -m "Update version to ${{ github.ref_name }}" || echo "No changes to commit."
|
||||||
git push origin HEAD:main
|
|
||||||
- name: Publish to Visual Studio Marketplace
|
# Get current branch; if detached, resolve to a remote branch that contains this commit
|
||||||
|
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [[ "$BRANCH" == "HEAD" || -z "$BRANCH" ]]; then
|
||||||
|
# If target_commitish is a branch, use it; if it’s a SHA, find a branch that contains it
|
||||||
|
CANDIDATE='${{ github.event.release.target_commitish }}'
|
||||||
|
if [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]]; then
|
||||||
|
BRANCH="$(git branch -r --contains "$GITHUB_SHA" | sed -n 's|.*origin/||p' | head -n1)"
|
||||||
|
else
|
||||||
|
BRANCH="$CANDIDATE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$BRANCH" ]]; then
|
||||||
|
echo "Could not determine branch for push."; git branch -a; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git push origin HEAD:refs/heads/$BRANCH
|
||||||
|
- name: Publish to Visual Studio Marketplace (Pre-Release)
|
||||||
|
if: ${{ github.event.release.prerelease }}
|
||||||
|
run: vsce publish --pre-release
|
||||||
|
env:
|
||||||
|
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
|
||||||
|
- name: Publish to Visual Studio Marketplace (Stable)
|
||||||
|
if: ${{ !github.event.release.prerelease }}
|
||||||
run: vsce publish
|
run: vsce publish
|
||||||
env:
|
env:
|
||||||
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
|
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
|
||||||
|
|||||||
Vendored
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
"ruff.configuration": {
|
"ruff.configuration": {
|
||||||
"lint": {
|
"lint": {
|
||||||
"extend-ignore": ["F821", "E402"]
|
"extend-ignore": ["F821", "E402"]
|
||||||
}
|
},
|
||||||
|
"line-length": 200
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# NX Postprocessor Support
|
||||||
|
|
||||||
|
A comprehensive VS Code extension providing language support for NX CAM postprocessor development, including CDL, TCL, and DEF files.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Syntax Highlighting** - Full syntax highlighting for CDL, TCL, and DEF files
|
||||||
|
- **Code Formatting** - Automatic code formatting for all supported file types
|
||||||
|
- **Multi-language Support** - Supports NX CDL, TCL, and DEF file formats
|
||||||
|
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
|
||||||
|
- **Auto-completion** - Context-aware code completion for faster development
|
||||||
|
|
||||||
|
## Supported File Types
|
||||||
|
|
||||||
|
- `.cdl` - NX User Defined Event File
|
||||||
|
- `.tcl` - NX TCL postprocessor files
|
||||||
|
- `.def` - NX Definition files
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Install from the VS Code Marketplace
|
||||||
|
2. Open any `.cdl`, `.tcl`, or `.def` file
|
||||||
|
3. The extension will automatically activate and provide language support
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The extension can be configured through VS Code settings:
|
||||||
|
|
||||||
|
- `nx-post-support.interpreter` - Specify custom Python interpreter path for the language server
|
||||||
|
- `nx-post-support.formatter` - Enable/disable the TCL formatter (default: false)
|
||||||
|
- `nx-post-support.inlayHint` - Enable/disable inlay Hints (default: true)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Simply open any supported file type and enjoy:
|
||||||
|
|
||||||
|
- Syntax highlighting
|
||||||
|
- Error detection and linting
|
||||||
|
- Code completion
|
||||||
|
- Code formatting (Format Document command)
|
||||||
|
- Hover information
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
This extension is actively maintained. For issues or feature requests, please visit our [repository](https://git.cbsk-tech.de/Christoph/nx_post_support.git).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see LICENSE file for details.
|
||||||
|
|||||||
@@ -271,3 +271,180 @@ export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode
|
|||||||
|
|
||||||
return collectSymbols(rootScope)
|
return collectSymbols(rootScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cdlDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
|
||||||
|
const lines = document.getText().split("\n")
|
||||||
|
|
||||||
|
interface Scope {
|
||||||
|
symbol: vscode.DocumentSymbol
|
||||||
|
startLine: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = new vscode.DocumentSymbol(
|
||||||
|
"root",
|
||||||
|
"",
|
||||||
|
vscode.SymbolKind.Namespace,
|
||||||
|
new vscode.Range(0, 0, lines.length, 0),
|
||||||
|
new vscode.Range(0, 0, 0, 0)
|
||||||
|
)
|
||||||
|
root.children = []
|
||||||
|
const stack: Scope[] = [{ symbol: root, startLine: 0 }]
|
||||||
|
|
||||||
|
let pending: { name: string; kind: vscode.SymbolKind; startLine: number } | null = null
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
const trimmed = line.trim()
|
||||||
|
|
||||||
|
const createSymbol = () => {
|
||||||
|
if (!pending) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const startRange = new vscode.Range(
|
||||||
|
pending.startLine,
|
||||||
|
0,
|
||||||
|
pending.startLine,
|
||||||
|
lines[pending.startLine].length
|
||||||
|
)
|
||||||
|
const symbol = new vscode.DocumentSymbol(
|
||||||
|
pending.name,
|
||||||
|
"",
|
||||||
|
pending.kind,
|
||||||
|
startRange,
|
||||||
|
startRange
|
||||||
|
)
|
||||||
|
symbol.children = []
|
||||||
|
stack[stack.length - 1].symbol.children?.push(symbol)
|
||||||
|
stack.push({ symbol, startLine: pending.startLine })
|
||||||
|
pending = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending && trimmed.includes("{")) {
|
||||||
|
createSymbol()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith("}")) {
|
||||||
|
const scope = stack.pop()
|
||||||
|
if (scope) {
|
||||||
|
scope.symbol.range = new vscode.Range(scope.startLine, 0, i, line.length)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let match
|
||||||
|
if ((match = /^\s*EVENT\s+(\w+)/.exec(line))) {
|
||||||
|
pending = {
|
||||||
|
name: match[1],
|
||||||
|
kind: vscode.SymbolKind.Event,
|
||||||
|
startLine: i
|
||||||
|
}
|
||||||
|
if (line.includes("{")) {
|
||||||
|
createSymbol()
|
||||||
|
}
|
||||||
|
} else if ((match = /^\s*PARAM\s+(\w+)/.exec(line))) {
|
||||||
|
pending = {
|
||||||
|
name: match[1],
|
||||||
|
kind: vscode.SymbolKind.Field,
|
||||||
|
startLine: i
|
||||||
|
}
|
||||||
|
if (line.includes("{")) {
|
||||||
|
createSymbol()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return root.children || []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
|
||||||
|
const lines = document.getText().split("\n")
|
||||||
|
|
||||||
|
interface Scope {
|
||||||
|
symbol: vscode.DocumentSymbol
|
||||||
|
startLine: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = new vscode.DocumentSymbol(
|
||||||
|
"root",
|
||||||
|
"",
|
||||||
|
vscode.SymbolKind.Namespace,
|
||||||
|
new vscode.Range(0, 0, lines.length, 0),
|
||||||
|
new vscode.Range(0, 0, 0, 0)
|
||||||
|
)
|
||||||
|
root.children = []
|
||||||
|
const stack: Scope[] = [{ symbol: root, startLine: 0 }]
|
||||||
|
|
||||||
|
let pending: { name: string; kind: vscode.SymbolKind; startLine: number } | null = null
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
const trimmed = line.trim()
|
||||||
|
|
||||||
|
const createSymbol = () => {
|
||||||
|
if (!pending) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const startRange = new vscode.Range(
|
||||||
|
pending.startLine,
|
||||||
|
0,
|
||||||
|
pending.startLine,
|
||||||
|
lines[pending.startLine].length
|
||||||
|
)
|
||||||
|
const symbol = new vscode.DocumentSymbol(
|
||||||
|
pending.name,
|
||||||
|
"",
|
||||||
|
pending.kind,
|
||||||
|
startRange,
|
||||||
|
startRange
|
||||||
|
)
|
||||||
|
symbol.children = []
|
||||||
|
stack[stack.length - 1].symbol.children?.push(symbol)
|
||||||
|
stack.push({ symbol, startLine: pending.startLine })
|
||||||
|
pending = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending && trimmed.includes("{")) {
|
||||||
|
createSymbol()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith("}")) {
|
||||||
|
const scope = stack.pop()
|
||||||
|
if (scope) {
|
||||||
|
scope.symbol.range = new vscode.Range(scope.startLine, 0, i, line.length)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let match
|
||||||
|
if ((match = /^\s*FORMATTING\b/.exec(line))) {
|
||||||
|
pending = {
|
||||||
|
name: "FORMATTING",
|
||||||
|
kind: vscode.SymbolKind.Module,
|
||||||
|
startLine: i
|
||||||
|
}
|
||||||
|
if (line.includes("{")) {
|
||||||
|
createSymbol()
|
||||||
|
}
|
||||||
|
} else if ((match = /^\s*ADDRESS\s+(\w+)/.exec(line))) {
|
||||||
|
pending = {
|
||||||
|
name: match[1],
|
||||||
|
kind: vscode.SymbolKind.Field,
|
||||||
|
startLine: i
|
||||||
|
}
|
||||||
|
if (line.includes("{")) {
|
||||||
|
createSymbol()
|
||||||
|
}
|
||||||
|
} else if ((match = /^\s*BLOCK_TEMPLATE\s+(\w+)/.exec(line))) {
|
||||||
|
pending = {
|
||||||
|
name: match[1],
|
||||||
|
kind: vscode.SymbolKind.Function,
|
||||||
|
startLine: i
|
||||||
|
}
|
||||||
|
if (line.includes("{")) {
|
||||||
|
createSymbol()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return root.children || []
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ export async function getWorkspaceSettings(
|
|||||||
interpreter: resolveVariables(interpreter, workspace),
|
interpreter: resolveVariables(interpreter, workspace),
|
||||||
importStrategy: config.get<string>(`importStrategy`) ?? "useBundled",
|
importStrategy: config.get<string>(`importStrategy`) ?? "useBundled",
|
||||||
showNotifications: config.get<string>(`showNotifications`) ?? "off",
|
showNotifications: config.get<string>(`showNotifications`) ?? "off",
|
||||||
formatter: config.get<boolean>(`formatter`) ?? true
|
formatter: config.get<boolean>(`formatter`) ?? true,
|
||||||
|
inlayHint: config.get<boolean>(`inlayHint`) ?? true
|
||||||
}
|
}
|
||||||
return workspaceSetting
|
return workspaceSetting
|
||||||
}
|
}
|
||||||
@@ -111,7 +112,8 @@ export async function getGlobalSettings(
|
|||||||
interpreter: interpreter,
|
interpreter: interpreter,
|
||||||
importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"),
|
importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"),
|
||||||
showNotifications: getGlobalValue<string>(config, "showNotifications", "off"),
|
showNotifications: getGlobalValue<string>(config, "showNotifications", "off"),
|
||||||
formatter: config.get<boolean>(`formatter`) ?? true
|
formatter: config.get<boolean>(`formatter`) ?? true,
|
||||||
|
inlayHint: config.get<boolean>(`inlayHint`) ?? true
|
||||||
}
|
}
|
||||||
return setting
|
return setting
|
||||||
}
|
}
|
||||||
@@ -126,7 +128,8 @@ export function checkIfConfigurationChanged(
|
|||||||
`${namespace}.interpreter`,
|
`${namespace}.interpreter`,
|
||||||
`${namespace}.importStrategy`,
|
`${namespace}.importStrategy`,
|
||||||
`${namespace}.showNotifications`,
|
`${namespace}.showNotifications`,
|
||||||
`${namespace}.formatter`
|
`${namespace}.formatter`,
|
||||||
|
`${namespace}.inlayHint`
|
||||||
]
|
]
|
||||||
const changed = settings.map((s) => e.affectsConfiguration(s))
|
const changed = settings.map((s) => e.affectsConfiguration(s))
|
||||||
return changed.includes(true)
|
return changed.includes(true)
|
||||||
|
|||||||
+21
-5
@@ -12,7 +12,8 @@ import {
|
|||||||
formatDefFile,
|
formatDefFile,
|
||||||
isFirstLineMachine,
|
isFirstLineMachine,
|
||||||
diagnosticHandler,
|
diagnosticHandler,
|
||||||
tclDocumentSymbolProvider
|
cdlDocumentSymbolProvider,
|
||||||
|
defDocumentSymbolProvider
|
||||||
} from "./common/handlers"
|
} from "./common/handlers"
|
||||||
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
|
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
|
||||||
import {
|
import {
|
||||||
@@ -179,11 +180,26 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
context.subscriptions.push(formatDefProvider)
|
context.subscriptions.push(formatDefProvider)
|
||||||
|
|
||||||
const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider(
|
const cdlSymbolProvider = vscode.languages.registerDocumentSymbolProvider(
|
||||||
{ scheme: "file", language: "tcl" },
|
{ scheme: "file", language: "cdl" },
|
||||||
{ provideDocumentSymbols: tclDocumentSymbolProvider }
|
{
|
||||||
|
provideDocumentSymbols(document: vscode.TextDocument) {
|
||||||
|
return cdlDocumentSymbolProvider(document)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
context.subscriptions.push(tclOutlineProvider)
|
context.subscriptions.push(cdlSymbolProvider)
|
||||||
|
|
||||||
|
// Outline now provided by the language server (Document Symbols). Removing TS provider.
|
||||||
|
const defSymbolProvider = vscode.languages.registerDocumentSymbolProvider(
|
||||||
|
{ scheme: "file", language: "def" },
|
||||||
|
{
|
||||||
|
provideDocumentSymbols(document: vscode.TextDocument) {
|
||||||
|
return defDocumentSymbolProvider(document)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context.subscriptions.push(defSymbolProvider)
|
||||||
|
|
||||||
// Diagnostics collection
|
// Diagnostics collection
|
||||||
const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl")
|
const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl")
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
+11
-2
@@ -2,8 +2,9 @@
|
|||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"displayName": "NX Postprocessor Support",
|
"displayName": "NX Postprocessor Support",
|
||||||
"description": "",
|
"description": "",
|
||||||
"version": "0.4.13",
|
"version": "2025.8.300",
|
||||||
"publisher": "Christoph",
|
"publisher": "Christoph",
|
||||||
|
"icon": "images/nx-1.png",
|
||||||
"serverInfo": {
|
"serverInfo": {
|
||||||
"name": "NX Postprocessor Support",
|
"name": "NX Postprocessor Support",
|
||||||
"module": "nx-post-support"
|
"module": "nx-post-support"
|
||||||
@@ -16,7 +17,10 @@
|
|||||||
"keywords": [
|
"keywords": [
|
||||||
"cdl",
|
"cdl",
|
||||||
"NX CDL",
|
"NX CDL",
|
||||||
"UDE"
|
"UDE",
|
||||||
|
"Postprocessor",
|
||||||
|
"NX CAM",
|
||||||
|
"Siemens NX"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.96.0"
|
"vscode": "^1.96.0"
|
||||||
@@ -84,6 +88,11 @@
|
|||||||
"default": false,
|
"default": false,
|
||||||
"description": "Use the TCL formatter from `NX Postprocessor Support`"
|
"description": "Use the TCL formatter from `NX Postprocessor Support`"
|
||||||
},
|
},
|
||||||
|
"nx-post-support.inlayHint": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Use the Inlay Hints in from `NX Postprocessor Support`"
|
||||||
|
},
|
||||||
"nx-post-support.importStrategy": {
|
"nx-post-support.importStrategy": {
|
||||||
"default": "useBundled",
|
"default": "useBundled",
|
||||||
"description": "Defines where `NX Postprocessor Support` is imported from.",
|
"description": "Defines where `NX Postprocessor Support` is imported from.",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ if debugger_path:
|
|||||||
# 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
|
||||||
# line and set breakpoints as appropriate.
|
# line and set breakpoints as appropriate.
|
||||||
debugpy.breakpoint()
|
# debugpy.breakpoint()
|
||||||
|
|
||||||
SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py")
|
SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py")
|
||||||
# NOTE: Set breakpoint in `lsp_server.py` before continuing.
|
# NOTE: Set breakpoint in `lsp_server.py` before continuing.
|
||||||
|
|||||||
@@ -71,6 +71,10 @@
|
|||||||
{
|
{
|
||||||
"label": "expr",
|
"label": "expr",
|
||||||
"kind": "keyword"
|
"kind": "keyword"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "return",
|
||||||
|
"kind": "keyword"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"MOM_procs": [
|
"MOM_procs": [
|
||||||
@@ -188,7 +192,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "MOM_add_to_block_buffer",
|
"label": "MOM_add_to_line_buffer",
|
||||||
"kind": "function",
|
"kind": "function",
|
||||||
"description": "Depending on the <start|end> attribute specified, this extension will add <value(s)> to either the line's start buffer or the line's end buffer. Each time that this extension is called, it adds to the specified buffer. When the contents of the output buffer are sent to the output file, the contents of the line's start buffer will precede it on the line and the contents of the end buffer will go on the same line after it. The line start buffer, line end buffer, and output buffer are all cleared once they have been written to the output file.",
|
"description": "Depending on the <start|end> attribute specified, this extension will add <value(s)> to either the line's start buffer or the line's end buffer. Each time that this extension is called, it adds to the specified buffer. When the contents of the output buffer are sent to the output file, the contents of the line's start buffer will precede it on the line and the contents of the end buffer will go on the same line after it. The line start buffer, line end buffer, and output buffer are all cleared once they have been written to the output file.",
|
||||||
"format": "MOM_add_to_line_buffer <start|end> <value>+",
|
"format": "MOM_add_to_line_buffer <start|end> <value>+",
|
||||||
|
|||||||
+231
-188
@@ -9,7 +9,9 @@ import os
|
|||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, List, Optional, Tuple
|
import threading
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
import operator
|
import operator
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
@@ -38,142 +40,26 @@ update_sys_path(
|
|||||||
# pylint: disable=wrong-import-position,import-error
|
# pylint: disable=wrong-import-position,import-error
|
||||||
import lsp_jsonrpc as jsonrpc
|
import lsp_jsonrpc as jsonrpc
|
||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
from pygls import server, uris, workspace
|
from pygls import uris, workspace
|
||||||
from pygls.workspace.text_document import TextDocument
|
|
||||||
from common.load_data import standard_items
|
from common.load_data import standard_items
|
||||||
from tclint.lexer import TclSyntaxError
|
|
||||||
from tclint.format import Formatter, FormatterOpts
|
|
||||||
from tclint.violations import Violation
|
|
||||||
from plugins.poco_plugin import commands
|
|
||||||
from tools import checks, parser
|
|
||||||
from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
||||||
from tools.completion_items import completion
|
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
||||||
from tools.inlay_hint import InlayHintGenerator
|
from tools.inlay_hint import InlayHintGenerator
|
||||||
|
from tools.file_sourcing import get_all_psc_files, read_psc_file
|
||||||
DIAGNOSTIC_SOURCE = "nx-post-support"
|
from lsp_tclserver import TclLanguageServer
|
||||||
|
|
||||||
|
|
||||||
class TclLanguageServer(server.LanguageServer):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.parser = parser.CustomParser()
|
|
||||||
for command in commands:
|
|
||||||
self.parser._commands.update(command)
|
|
||||||
self.diagnostics = {}
|
|
||||||
|
|
||||||
def format(
|
|
||||||
self,
|
|
||||||
document: TextDocument,
|
|
||||||
options: lsp.FormattingOptions,
|
|
||||||
range: Optional[Tuple[int, int]] = None,
|
|
||||||
):
|
|
||||||
# parser = Parser(command_plugins=["nx_plugins.poco_plugin.py"])
|
|
||||||
# parser._commands.update(commands)
|
|
||||||
|
|
||||||
indent = "\t" if not options.insert_spaces else " " * options.tab_size
|
|
||||||
formatter = Formatter(
|
|
||||||
FormatterOpts(
|
|
||||||
indent=indent,
|
|
||||||
spaces_in_braces=False,
|
|
||||||
max_blank_lines=500,
|
|
||||||
indent_namespace_eval=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if range is not None:
|
|
||||||
start, end = range
|
|
||||||
return formatter.format_partial(document.source[start:end], self.parser)
|
|
||||||
|
|
||||||
return formatter.format_top(document.source, self.parser)
|
|
||||||
|
|
||||||
def linter(
|
|
||||||
self,
|
|
||||||
document: TextDocument,
|
|
||||||
) -> List[Violation]:
|
|
||||||
violations = []
|
|
||||||
self.parser.violations = []
|
|
||||||
tree = self.parser.parse(document.source)
|
|
||||||
violations += self.parser.violations
|
|
||||||
# log_to_output(tree.pretty(2))
|
|
||||||
for checker in checks.get_checkers():
|
|
||||||
violations += checker.check(document.source, tree)
|
|
||||||
return violations
|
|
||||||
|
|
||||||
def lint(self, document: TextDocument):
|
|
||||||
diagnostics = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
violations = self.linter(document)
|
|
||||||
except TclSyntaxError as e:
|
|
||||||
return [
|
|
||||||
lsp.Diagnostic(
|
|
||||||
message=str(e),
|
|
||||||
severity=lsp.DiagnosticSeverity.Error,
|
|
||||||
range=lsp.Range(
|
|
||||||
start=lsp.Position(e.start[0] - 1, e.start[1] - 1),
|
|
||||||
end=lsp.Position(e.end[0] - 1, e.end[1] - 1),
|
|
||||||
),
|
|
||||||
code="syntax error",
|
|
||||||
source=DIAGNOSTIC_SOURCE,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
for violation in violations:
|
|
||||||
message = violation.message
|
|
||||||
severity = lsp.DiagnosticSeverity.Warning
|
|
||||||
start = lsp.Position(
|
|
||||||
line=violation.start[0] - 1, character=violation.start[1] - 1
|
|
||||||
)
|
|
||||||
end = lsp.Position(
|
|
||||||
line=violation.end[0] - 1, character=violation.end[1] - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
diagnostics.append(
|
|
||||||
lsp.Diagnostic(
|
|
||||||
message=message,
|
|
||||||
severity=severity,
|
|
||||||
range=lsp.Range(
|
|
||||||
start=start,
|
|
||||||
end=end,
|
|
||||||
),
|
|
||||||
code=violation.id,
|
|
||||||
source=DIAGNOSTIC_SOURCE,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
|
|
||||||
def _compute_diagnostics(self, document: TextDocument) -> List[lsp.Diagnostic]:
|
|
||||||
return self.lint(document)
|
|
||||||
|
|
||||||
def compute_diagnostics(self, document: TextDocument):
|
|
||||||
# `None` sentinel ensures that `diagnostics` gets updated if the URI is not
|
|
||||||
# present.
|
|
||||||
_, previous = self.diagnostics.get(document, (0, None))
|
|
||||||
|
|
||||||
diagnostics = self._compute_diagnostics(document)
|
|
||||||
|
|
||||||
# Only update if the list has changed
|
|
||||||
if previous != diagnostics:
|
|
||||||
self.diagnostics[document.uri] = (document.version, diagnostics)
|
|
||||||
|
|
||||||
|
|
||||||
WORKSPACE_SETTINGS = {}
|
WORKSPACE_SETTINGS = {}
|
||||||
GLOBAL_SETTINGS = {}
|
GLOBAL_SETTINGS = {}
|
||||||
RUNNER = pathlib.Path(__file__).parent / "lsp_runner.py"
|
|
||||||
|
|
||||||
|
|
||||||
MAX_WORKERS = 5
|
MAX_WORKERS = 5
|
||||||
LSP_SERVER = TclLanguageServer(
|
LSP_SERVER = TclLanguageServer(name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS)
|
||||||
name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS
|
|
||||||
)
|
|
||||||
|
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
# Tool specific code goes below this.
|
# Tool specific code goes below this.
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
TOOL_MODULE = "nx-post-support"
|
|
||||||
TOOL_DISPLAY = "NX Postprocessor Support"
|
|
||||||
TOOL_ARGS = [] # default arguments always passed to your tool.
|
|
||||||
|
|
||||||
# Delete "Linting features" section if your tool is NOT a linter.
|
# Delete "Linting features" section if your tool is NOT a linter.
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
@@ -189,9 +75,8 @@ def did_open(params: lsp.DidOpenTextDocumentParams) -> None:
|
|||||||
"""LSP handler for textDocument/didOpen request."""
|
"""LSP handler for textDocument/didOpen request."""
|
||||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
LSP_SERVER.compute_diagnostics(document)
|
LSP_SERVER.compute_diagnostics(document)
|
||||||
completion.reset()
|
# Also update custom completion and proc docs for this file
|
||||||
tree = LSP_SERVER.parser.parse(document.source)
|
LSP_SERVER.update_poco_completion_for_file(document)
|
||||||
tree.accept(completion, recurse=True)
|
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
|
||||||
@@ -201,7 +86,7 @@ def did_save(params: lsp.DidSaveTextDocumentParams) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
|
||||||
def did_close(params: lsp.DidCloseTextDocumentParams) -> None:
|
def did_close(_: lsp.DidCloseTextDocumentParams) -> None:
|
||||||
"""LSP handler for textDocument/didClose request."""
|
"""LSP handler for textDocument/didClose request."""
|
||||||
|
|
||||||
|
|
||||||
@@ -210,9 +95,7 @@ def did_change(params: lsp.DidChangeTextDocumentParams) -> None:
|
|||||||
"""LSP handler for textDocument/didChange request"""
|
"""LSP handler for textDocument/didChange request"""
|
||||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
LSP_SERVER.compute_diagnostics(document)
|
LSP_SERVER.compute_diagnostics(document)
|
||||||
completion.reset()
|
LSP_SERVER.update_poco_completion_for_file(document)
|
||||||
tree = LSP_SERVER.parser.parse(document.source)
|
|
||||||
tree.accept(completion, recurse=True)
|
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(
|
@LSP_SERVER.feature(
|
||||||
@@ -241,33 +124,82 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
|||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
|
||||||
def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
|
def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||||
_ = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
from tools.variable_index import build_variable_index
|
||||||
|
from tools.completion_items import BUILTIN_VAR_LABELS
|
||||||
|
|
||||||
items = (
|
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
standard_items.tcl_keyword_list
|
|
||||||
+ standard_items.nx_procs
|
# Base items
|
||||||
+ standard_items.nx_variables
|
poco = [item for items in LSP_SERVER.poco_completion.values() for item in items]
|
||||||
+ completion.custom_functions
|
base_items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco
|
||||||
)
|
|
||||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
# Build variable index from current document
|
||||||
|
globals_set, procs_locals, proc_ranges = build_variable_index(doc.source)
|
||||||
|
|
||||||
|
# Always include globals (excluding built-ins)
|
||||||
|
dynamic_items = []
|
||||||
|
for name in sorted(globals_set):
|
||||||
|
if name not in BUILTIN_VAR_LABELS:
|
||||||
|
dynamic_items.append(lsp.CompletionItem(label=name, kind=lsp.CompletionItemKind.Variable))
|
||||||
|
|
||||||
|
# Include proc-local variables when cursor is inside that proc
|
||||||
|
pos = params.position
|
||||||
|
if pos is not None:
|
||||||
|
for pr in proc_ranges:
|
||||||
|
if pr.start_line <= pos.line <= (pr.end_line or pr.start_line):
|
||||||
|
for name in sorted(procs_locals.get(pr.name, set())):
|
||||||
|
# Exclude built-ins and globals to avoid duplication
|
||||||
|
if name not in BUILTIN_VAR_LABELS and name not in globals_set:
|
||||||
|
dynamic_items.append(lsp.CompletionItem(label=name, kind=lsp.CompletionItemKind.Variable))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Merge with de-duplication for variables only
|
||||||
|
merged: list[lsp.CompletionItem] = []
|
||||||
|
seen_var_labels: set[str] = set()
|
||||||
|
for it in base_items + dynamic_items:
|
||||||
|
if getattr(it, "kind", None) == lsp.CompletionItemKind.Variable:
|
||||||
|
if it.label in seen_var_labels:
|
||||||
|
continue
|
||||||
|
seen_var_labels.add(it.label)
|
||||||
|
merged.append(it)
|
||||||
|
|
||||||
|
return lsp.CompletionList(is_incomplete=False, items=merged)
|
||||||
|
|
||||||
|
|
||||||
# @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
# @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
||||||
# def document_symbols(params: lsp.DocumentSymbolParams):
|
# def document_symbols(params: lsp.DocumentSymbolParams):
|
||||||
# doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
# doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
# return []
|
# ast = LSP_SERVER.parser.parse(doc.source)
|
||||||
|
# symbols = LSP_SERVER.extract_tcl_symbols(ast)
|
||||||
|
|
||||||
|
# return symbols
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
||||||
|
def document_symbols(params: lsp.DocumentSymbolParams):
|
||||||
|
from tools.document_symbols import build_document_symbols
|
||||||
|
|
||||||
|
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
return build_document_symbols(doc.source)
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
|
||||||
def inlay_hints(params: lsp.InlayHintParams):
|
def inlay_hints(params: lsp.InlayHintParams):
|
||||||
|
if not GLOBAL_SETTINGS.get("inlayHint", False):
|
||||||
|
return []
|
||||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
tree = LSP_SERVER.parser.parse(document.source)
|
|
||||||
|
|
||||||
# collect Inlay Hints
|
# Reuse cached AST
|
||||||
generator = InlayHintGenerator(completion.proc_signatures)
|
tree = LSP_SERVER.get_tree(document)
|
||||||
|
|
||||||
|
# Merge proc signatures across files and traverse once
|
||||||
|
merged_signatures = {}
|
||||||
|
for sigs in LSP_SERVER.proc_signatures.values():
|
||||||
|
merged_signatures.update(sigs)
|
||||||
|
|
||||||
|
generator = InlayHintGenerator(merged_signatures)
|
||||||
tree.accept(generator, recurse=True)
|
tree.accept(generator, recurse=True)
|
||||||
|
|
||||||
return generator.hints
|
return generator.hints
|
||||||
|
|
||||||
|
|
||||||
@@ -283,9 +215,10 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
|
|||||||
|
|
||||||
data = []
|
data = []
|
||||||
plugins = []
|
plugins = []
|
||||||
hl = _Highlighter(plugins, log_to_output=log_to_output)
|
hl = _Highlighter(plugins, LSP_SERVER.poco_completion)
|
||||||
|
|
||||||
tree = LSP_SERVER.parser.parse(document.source)
|
# Reuse cached AST
|
||||||
|
tree = LSP_SERVER.get_tree(document)
|
||||||
tree.accept(hl, recurse=True)
|
tree.accept(hl, recurse=True)
|
||||||
|
|
||||||
tokens = hl.tokens()
|
tokens = hl.tokens()
|
||||||
@@ -294,7 +227,7 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
|
|||||||
[
|
[
|
||||||
token.line,
|
token.line,
|
||||||
token.offset,
|
token.offset,
|
||||||
token.lenght,
|
token.length,
|
||||||
TOKEN_TYPES.index(token.tok_type),
|
TOKEN_TYPES.index(token.tok_type),
|
||||||
reduce(operator.or_, token.tok_modifiers, 0),
|
reduce(operator.or_, token.tok_modifiers, 0),
|
||||||
]
|
]
|
||||||
@@ -314,6 +247,13 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Do not show hover for proc name in its declaration
|
||||||
|
from tools.proc_docs import is_proc_declaration_position
|
||||||
|
|
||||||
|
if is_proc_declaration_position(document.source, pos.line, pos.character):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Identify the token under the cursor
|
||||||
for m in re.finditer(r"\b\w+\b", line):
|
for m in re.finditer(r"\b\w+\b", line):
|
||||||
if m.start() <= col <= m.end():
|
if m.start() <= col <= m.end():
|
||||||
token = m.group(0)
|
token = m.group(0)
|
||||||
@@ -321,51 +261,121 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# 1) If token is a known MOM proc/variable, return built-in hover
|
||||||
command = token
|
command = token
|
||||||
data = standard_items.json_data
|
data = standard_items.json_data
|
||||||
|
|
||||||
all_items = data.get("MOM_procs", []) + data.get("mom_variables", [])
|
all_items = data.get("MOM_procs", []) + data.get("mom_variables", [])
|
||||||
|
|
||||||
match = next((item for item in all_items if item["label"] == command), None)
|
match = next((item for item in all_items if item["label"] == command), None)
|
||||||
if not match:
|
if match and match.get("kind") == "function":
|
||||||
return None
|
label = match.get("label", "")
|
||||||
|
parameters = match.get("parameters", [])
|
||||||
if not match.get("kind") == "function":
|
param_lines = "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
||||||
return None
|
example_data = match.get("example", [])
|
||||||
|
example_md = "\n".join(f"{line}" for line in example_data)
|
||||||
label = match.get("label", "")
|
returns_data = match.get("returns", ["None"])
|
||||||
|
returns_md = "\n".join(f"- {line}" for line in returns_data)
|
||||||
parameters = match.get("parameters", [])
|
doc_md = f"""\
|
||||||
param_lines = (
|
|
||||||
"\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
|
||||||
)
|
|
||||||
|
|
||||||
example_data = match.get("example", [])
|
|
||||||
example_md = "\n".join(f"{line}" for line in example_data)
|
|
||||||
|
|
||||||
returns_data = match.get("returns", ["None"])
|
|
||||||
returns_md = "\n".join(f"- {line}" for line in returns_data)
|
|
||||||
|
|
||||||
doc_md = f"""\
|
|
||||||
### 📘 {label}
|
### 📘 {label}
|
||||||
|
|
||||||
**Purpose**
|
**Purpose**
|
||||||
{match.get("description", "No description available.")}
|
{match.get("description", "No description available.")}
|
||||||
|
|
||||||
**Format**
|
**Format**
|
||||||
`{match.get("format", label)}`
|
`{match.get("format", label)}`
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
{param_lines}
|
{param_lines}
|
||||||
|
|
||||||
**Return value**
|
**Return value**
|
||||||
{returns_md}
|
{returns_md}
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
```tcl
|
```tcl
|
||||||
{example_md}"""
|
{example_md}"""
|
||||||
|
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=doc_md))
|
||||||
|
|
||||||
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=doc_md))
|
# 2) Otherwise, check if the token is a custom proc and show its preceding doc block
|
||||||
|
# Build a merged map of proc -> docs gathered during initialization and updates
|
||||||
|
proc_docs: dict[str, str] = {}
|
||||||
|
for file_docs in LSP_SERVER.proc_docs.values():
|
||||||
|
proc_docs.update(file_docs)
|
||||||
|
|
||||||
|
if token in proc_docs:
|
||||||
|
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=proc_docs[token]))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DEFINITION)
|
||||||
|
def goto_definition(params: lsp.DefinitionParams):
|
||||||
|
"""Provide go-to-definition locations for Tcl procs.
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
- Find the token under the cursor.
|
||||||
|
- If it matches a custom proc collected in proc_signatures, locate its declaration
|
||||||
|
by searching the current document first, then other indexed files.
|
||||||
|
- Return a Location pointing to the proc name in its declaration line.
|
||||||
|
"""
|
||||||
|
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
pos = params.position
|
||||||
|
try:
|
||||||
|
line = doc.lines[pos.line]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Identify token under cursor
|
||||||
|
token = None
|
||||||
|
for m in re.finditer(r"\b\w+\b", line):
|
||||||
|
if m.start() <= pos.character <= m.end():
|
||||||
|
token = m.group(0)
|
||||||
|
break
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Helper to search a single source text for a proc declaration
|
||||||
|
def find_decl_in_source(source_text: str, uri: str) -> Optional[lsp.Location]:
|
||||||
|
lines = source_text.split("\n")
|
||||||
|
pattern = re.compile(r"^\s*proc\s+" + re.escape(token) + r"\b")
|
||||||
|
for i, ln in enumerate(lines):
|
||||||
|
m = pattern.match(ln)
|
||||||
|
if m:
|
||||||
|
start_char = ln.find(token)
|
||||||
|
if start_char < 0:
|
||||||
|
start_char = max(m.end() - len(token), 0)
|
||||||
|
start = lsp.Position(i, start_char)
|
||||||
|
end = lsp.Position(i, start_char + len(token))
|
||||||
|
return lsp.Location(uri=uri, range=lsp.Range(start=start, end=end))
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 1) Search in current document
|
||||||
|
loc = find_decl_in_source(doc.source, doc.uri)
|
||||||
|
if loc:
|
||||||
|
return loc
|
||||||
|
|
||||||
|
# 2) Search in indexed files from proc_signatures
|
||||||
|
# Build list of candidate files that declare this token as a proc
|
||||||
|
candidate_files: list[str] = []
|
||||||
|
for file_path, procs in LSP_SERVER.proc_signatures.items():
|
||||||
|
if token in procs:
|
||||||
|
candidate_files.append(file_path)
|
||||||
|
|
||||||
|
for fp in candidate_files:
|
||||||
|
uri = pathlib.Path(fp).as_uri()
|
||||||
|
# Try to get from workspace if available; else read from disk
|
||||||
|
try:
|
||||||
|
other_doc = LSP_SERVER.workspace.get_text_document(uri)
|
||||||
|
source = other_doc.source
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
source = pathlib.Path(fp).read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
loc = find_decl_in_source(source, uri)
|
||||||
|
if loc:
|
||||||
|
return loc
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
@@ -406,7 +416,7 @@ def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | Non
|
|||||||
# Required Language Server Initialization and Exit handlers.
|
# Required Language Server Initialization and Exit handlers.
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
@LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION)
|
@LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION)
|
||||||
def did_change_configuration(params: lsp.DidChangeConfigurationParams):
|
def did_change_configuration(_: lsp.DidChangeConfigurationParams):
|
||||||
"""LSP Handler for Config Changes"""
|
"""LSP Handler for Config Changes"""
|
||||||
|
|
||||||
|
|
||||||
@@ -422,26 +432,60 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
|||||||
|
|
||||||
settings = params.initialization_options["settings"]
|
settings = params.initialization_options["settings"]
|
||||||
_update_workspace_settings(settings)
|
_update_workspace_settings(settings)
|
||||||
log_to_output(
|
log_to_output(f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n")
|
||||||
f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n"
|
log_to_output(f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n")
|
||||||
)
|
|
||||||
log_to_output(
|
|
||||||
f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n"
|
|
||||||
)
|
|
||||||
semantic_tokens_legend = lsp.SemanticTokensLegend(
|
semantic_tokens_legend = lsp.SemanticTokensLegend(
|
||||||
token_types=TOKEN_TYPES,
|
token_types=TOKEN_TYPES,
|
||||||
token_modifiers=TokenModifier,
|
token_modifiers=[m.name for m in TokenModifier],
|
||||||
)
|
)
|
||||||
return lsp.InitializeResult(
|
return lsp.InitializeResult(
|
||||||
capabilities=lsp.ServerCapabilities(
|
capabilities=lsp.ServerCapabilities(
|
||||||
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
|
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
|
||||||
semantic_tokens_provider=lsp.SemanticTokensOptions(
|
semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
|
||||||
legend=semantic_tokens_legend, full=True, range=False
|
definition_provider=True,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(lsp.INITIALIZED)
|
||||||
|
def initialized(_params: lsp.InitializedParams):
|
||||||
|
"""Kick off background indexing to avoid blocking initialization."""
|
||||||
|
|
||||||
|
def index_workspace():
|
||||||
|
try:
|
||||||
|
root = LSP_SERVER.workspace.root_path
|
||||||
|
log_to_output("Background indexing started...")
|
||||||
|
psc_files = get_all_psc_files(pathlib.Path(root))
|
||||||
|
for psc_file in psc_files:
|
||||||
|
poco_files = read_psc_file(psc_file)
|
||||||
|
for sourced_layer in poco_files:
|
||||||
|
completion.reset()
|
||||||
|
try:
|
||||||
|
file_root = pathlib.Path(root).joinpath(sourced_layer.subfolder if sourced_layer.subfolder else "")
|
||||||
|
for tcl_file in sourced_layer.files:
|
||||||
|
filepath = pathlib.Path(file_root).joinpath(f"{tcl_file}.tcl")
|
||||||
|
if not filepath.exists():
|
||||||
|
continue
|
||||||
|
completion.reset()
|
||||||
|
document = LSP_SERVER.workspace.get_text_document(filepath.as_uri())
|
||||||
|
tree = LSP_SERVER.parser.parse(document.source)
|
||||||
|
tree.accept(completion, recurse=True)
|
||||||
|
remove_existing_items(completion.custom_functions, LSP_SERVER.poco_completion)
|
||||||
|
LSP_SERVER.poco_completion[str(filepath)] = completion.custom_functions
|
||||||
|
remove_shared_keys(LSP_SERVER.proc_signatures, completion.proc_signatures)
|
||||||
|
LSP_SERVER.proc_signatures[str(filepath)] = completion.proc_signatures
|
||||||
|
from tools.proc_docs import build_proc_docs
|
||||||
|
|
||||||
|
LSP_SERVER.proc_docs[str(filepath)] = build_proc_docs(tree, document.source)
|
||||||
|
except Exception as e:
|
||||||
|
log_to_output(f"Fehler beim Parsen von {filepath}: {e}")
|
||||||
|
log_to_output("Background indexing completed.")
|
||||||
|
except Exception as e:
|
||||||
|
log_to_output(f"Background indexing failed: {e}")
|
||||||
|
|
||||||
|
threading.Thread(target=index_workspace, name="nxps-indexer", daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.EXIT)
|
@LSP_SERVER.feature(lsp.EXIT)
|
||||||
def on_exit(_params: Optional[Any] = None) -> None:
|
def on_exit(_params: Optional[Any] = None) -> None:
|
||||||
"""Handle clean up on exit."""
|
"""Handle clean up on exit."""
|
||||||
@@ -462,6 +506,7 @@ def _get_global_defaults():
|
|||||||
"importStrategy": GLOBAL_SETTINGS.get("importStrategy", "useBundled"),
|
"importStrategy": GLOBAL_SETTINGS.get("importStrategy", "useBundled"),
|
||||||
"showNotifications": GLOBAL_SETTINGS.get("showNotifications", "off"),
|
"showNotifications": GLOBAL_SETTINGS.get("showNotifications", "off"),
|
||||||
"formatter": GLOBAL_SETTINGS.get("formatter", True),
|
"formatter": GLOBAL_SETTINGS.get("formatter", True),
|
||||||
|
"inlayHint": GLOBAL_SETTINGS.get("inlayHint", True),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -533,9 +578,7 @@ def _get_settings_by_document(document: workspace.Document | None):
|
|||||||
# *****************************************************
|
# *****************************************************
|
||||||
# Logging and notification.
|
# Logging and notification.
|
||||||
# *****************************************************
|
# *****************************************************
|
||||||
def log_to_output(
|
def log_to_output(message: str, msg_type: lsp.MessageType = lsp.MessageType.Log) -> None:
|
||||||
message: str, msg_type: lsp.MessageType = lsp.MessageType.Log
|
|
||||||
) -> None:
|
|
||||||
LSP_SERVER.show_message_log(message, msg_type)
|
LSP_SERVER.show_message_log(message, msg_type)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import logging
|
||||||
|
import pathlib
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
import lsprotocol.types as lsp
|
||||||
|
from pygls.workspace.text_document import TextDocument
|
||||||
|
from tclint.lexer import TclSyntaxError
|
||||||
|
from tclint.format import FormatterOpts
|
||||||
|
from tools.formatter import NxFormatter as Formatter
|
||||||
|
from tclint.violations import Violation
|
||||||
|
from plugins.poco_plugin import commands
|
||||||
|
from tools import checks, parser
|
||||||
|
from pygls import server, uris
|
||||||
|
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
||||||
|
from tools.proc_docs import build_proc_docs
|
||||||
|
|
||||||
|
|
||||||
|
DIAGNOSTIC_SOURCE = "nx-post-support"
|
||||||
|
|
||||||
|
|
||||||
|
class TclLanguageServer(server.LanguageServer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.parser = parser.CustomParser()
|
||||||
|
for command in commands:
|
||||||
|
self.parser._commands.update(command)
|
||||||
|
self.diagnostics = {}
|
||||||
|
self.poco_completion: dict = {}
|
||||||
|
self.proc_signatures: dict = {}
|
||||||
|
self.proc_docs: dict = {}
|
||||||
|
# Cache: (uri, version) -> (tree, violations)
|
||||||
|
self._ast_cache = {}
|
||||||
|
|
||||||
|
def get_tree(self, document: TextDocument):
|
||||||
|
key = (document.uri, document.version)
|
||||||
|
cached = self._ast_cache.get(key)
|
||||||
|
if cached:
|
||||||
|
return cached[0]
|
||||||
|
# Parse and cache
|
||||||
|
self.parser.violations = []
|
||||||
|
tree = self.parser.parse(document.source)
|
||||||
|
violations = list(self.parser.violations)
|
||||||
|
self._ast_cache[key] = (tree, violations)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def get_tree_and_violations(self, document: TextDocument):
|
||||||
|
key = (document.uri, document.version)
|
||||||
|
cached = self._ast_cache.get(key)
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
# Parse and cache
|
||||||
|
self.parser.violations = []
|
||||||
|
tree = self.parser.parse(document.source)
|
||||||
|
violations = list(self.parser.violations)
|
||||||
|
self._ast_cache[key] = (tree, violations)
|
||||||
|
return tree, violations
|
||||||
|
|
||||||
|
def clear_cache_for_uri(self, uri: str):
|
||||||
|
to_delete = [k for k in self._ast_cache.keys() if k[0] == uri]
|
||||||
|
for k in to_delete:
|
||||||
|
del self._ast_cache[k]
|
||||||
|
|
||||||
|
def update_poco_completion_for_file(self, document: TextDocument):
|
||||||
|
"""Update poco_completion for a specific file when it changes"""
|
||||||
|
filepath = str(pathlib.Path(uris.to_fs_path(document.uri)))
|
||||||
|
|
||||||
|
# Remove existing completion items for this file
|
||||||
|
if filepath in self.poco_completion:
|
||||||
|
del self.poco_completion[filepath]
|
||||||
|
if filepath in self.proc_signatures:
|
||||||
|
del self.proc_signatures[filepath]
|
||||||
|
if filepath in self.proc_docs:
|
||||||
|
del self.proc_docs[filepath]
|
||||||
|
|
||||||
|
# Parse and extract new completion items
|
||||||
|
completion.reset()
|
||||||
|
try:
|
||||||
|
tree = self.get_tree(document)
|
||||||
|
tree.accept(completion, recurse=True)
|
||||||
|
remove_existing_items(completion.custom_functions, self.poco_completion)
|
||||||
|
self.poco_completion[filepath] = completion.custom_functions
|
||||||
|
remove_shared_keys(self.proc_signatures, completion.proc_signatures)
|
||||||
|
self.proc_signatures[filepath] = completion.proc_signatures
|
||||||
|
self.proc_docs[filepath] = build_proc_docs(tree, document.source)
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"Error parsing {filepath}: {e}")
|
||||||
|
|
||||||
|
def format(
|
||||||
|
self,
|
||||||
|
document: TextDocument,
|
||||||
|
options: lsp.FormattingOptions,
|
||||||
|
range: Optional[Tuple[int, int]] = None,
|
||||||
|
):
|
||||||
|
# parser = Parser(command_plugins=["nx_plugins.poco_plugin.py"])
|
||||||
|
# parser._commands.update(commands)
|
||||||
|
|
||||||
|
indent = "\t" if not options.insert_spaces else " " * options.tab_size
|
||||||
|
formatter = Formatter(
|
||||||
|
FormatterOpts(
|
||||||
|
indent=indent,
|
||||||
|
spaces_in_braces=False,
|
||||||
|
max_blank_lines=500,
|
||||||
|
indent_namespace_eval=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if range is not None:
|
||||||
|
start, end = range
|
||||||
|
return formatter.format_partial(document.source[start:end], self.parser)
|
||||||
|
|
||||||
|
return formatter.format_top(document.source, self.parser)
|
||||||
|
|
||||||
|
def linter(
|
||||||
|
self,
|
||||||
|
document: TextDocument,
|
||||||
|
) -> List[Violation]:
|
||||||
|
tree, violations = self.get_tree_and_violations(document)
|
||||||
|
for checker in checks.get_checkers():
|
||||||
|
violations += checker.check(document.source, tree)
|
||||||
|
return violations
|
||||||
|
|
||||||
|
def lint(self, document: TextDocument):
|
||||||
|
diagnostics = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
violations = self.linter(document)
|
||||||
|
except TclSyntaxError as e:
|
||||||
|
return [
|
||||||
|
lsp.Diagnostic(
|
||||||
|
message=str(e),
|
||||||
|
severity=lsp.DiagnosticSeverity.Error,
|
||||||
|
range=lsp.Range(
|
||||||
|
start=lsp.Position(e.start[0] - 1, e.start[1] - 1),
|
||||||
|
end=lsp.Position(e.end[0] - 1, e.end[1] - 1),
|
||||||
|
),
|
||||||
|
code="syntax error",
|
||||||
|
source=DIAGNOSTIC_SOURCE,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
for violation in violations:
|
||||||
|
message = violation.message
|
||||||
|
severity = lsp.DiagnosticSeverity.Warning
|
||||||
|
start = lsp.Position(line=violation.start[0] - 1, character=violation.start[1] - 1)
|
||||||
|
end = lsp.Position(line=violation.end[0] - 1, character=violation.end[1] - 1)
|
||||||
|
|
||||||
|
diagnostics.append(
|
||||||
|
lsp.Diagnostic(
|
||||||
|
message=message,
|
||||||
|
severity=severity,
|
||||||
|
range=lsp.Range(
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
),
|
||||||
|
code=violation.id,
|
||||||
|
source=DIAGNOSTIC_SOURCE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
|
|
||||||
|
def _compute_diagnostics(self, document: TextDocument) -> List[lsp.Diagnostic]:
|
||||||
|
return self.lint(document)
|
||||||
|
|
||||||
|
def compute_diagnostics(self, document: TextDocument):
|
||||||
|
# `None` sentinel ensures that `diagnostics` gets updated if the URI is not
|
||||||
|
# present.
|
||||||
|
_, previous = self.diagnostics.get(document, (0, None))
|
||||||
|
|
||||||
|
diagnostics = self._compute_diagnostics(document)
|
||||||
|
|
||||||
|
# Only update if the list has changed
|
||||||
|
if previous != diagnostics:
|
||||||
|
self.diagnostics[document.uri] = (document.version, diagnostics)
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
from tclint.parser import Parser as tcLintParser
|
|
||||||
from tclint.lexer import Lexer as tclingLexer
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = tcLintParser(True)
|
|
||||||
tree = parser.parse("""puts hello
|
|
||||||
proc myProc {arg {arg7 0}} {}
|
|
||||||
set myVar 123
|
|
||||||
puts puts
|
|
||||||
LIB_SPF_prepend MOM_strt Start_Lib {
|
|
||||||
set somthing 1
|
|
||||||
set more 2
|
|
||||||
} myTag
|
|
||||||
|
|
||||||
LIB_SPF_prepend MOM_strt Start_Lib {
|
|
||||||
proc test {} {
|
|
||||||
puts "Hello"
|
|
||||||
}
|
|
||||||
set somthing 1
|
|
||||||
set someting 3
|
|
||||||
} myTag""")
|
|
||||||
|
|
||||||
print(tree.pretty(2))
|
|
||||||
|
|
||||||
|
|
||||||
def lexer_test():
|
|
||||||
lexer = tclingLexer()
|
|
||||||
tree = lexer.input("""
|
|
||||||
if {$oem(custom_clamp_4th) == 1 && $oem(custom_clamp_5th) == 1 \\
|
|
||||||
&& $oem(status_clamp_4th) == "off" && $oem(status_clamp_5th) == "off"}""")
|
|
||||||
|
|
||||||
# print("Lexing input:\n", code)
|
|
||||||
# print("\nTokens:\n" + "-" * 30)
|
|
||||||
|
|
||||||
while lexer.type() is not None:
|
|
||||||
tok_type = lexer.type()
|
|
||||||
tok_value = lexer.value()
|
|
||||||
tok_pos = lexer.pos()
|
|
||||||
print(f"Type: {tok_type:20} | Value: {repr(tok_value):20} | Pos: {tok_pos}")
|
|
||||||
lexer.next()
|
|
||||||
|
|
||||||
|
|
||||||
def test_1():
|
|
||||||
from tclint.lexer import Lexer, TOK_BACKSLASH_NEWLINE
|
|
||||||
|
|
||||||
code = "expr {1 == 2 \\\n&& 3 == 4}"
|
|
||||||
|
|
||||||
lexer = Lexer()
|
|
||||||
lexer.input(code)
|
|
||||||
|
|
||||||
while lexer.type() is not None:
|
|
||||||
print(
|
|
||||||
f"Type: {lexer.type():<20} | Value: {lexer.value()!r} | Pos: {lexer.pos()}"
|
|
||||||
)
|
|
||||||
lexer.next()
|
|
||||||
|
|
||||||
|
|
||||||
class NodeVisitor:
|
|
||||||
def visit_script(self, node):
|
|
||||||
for stmt in node.statements:
|
|
||||||
stmt.accept(self)
|
|
||||||
|
|
||||||
def visit_proc(self, node):
|
|
||||||
print(f"Proc: {node.name}, args={node.args}")
|
|
||||||
|
|
||||||
def visit_set(self, node):
|
|
||||||
print(f"Set: {node.varname} = {node.value}")
|
|
||||||
|
|
||||||
def visit_namespace(self, node):
|
|
||||||
print(f"Namespace: {node.name}")
|
|
||||||
|
|
||||||
def visit_command(self, node):
|
|
||||||
print(f"Command: {node.name} {node.args}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_1()
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
from tclint.syntax_tree import Visitor, Command, BareWord, List
|
from tclint.syntax_tree import Visitor, Command, BareWord, List
|
||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
|
from common.load_data import standard_items
|
||||||
|
|
||||||
|
BUILTIN_VAR_LABELS = {ci.label for ci in standard_items.nx_variables}
|
||||||
|
|
||||||
|
|
||||||
class CompletionItems:
|
class CompletionItems:
|
||||||
@@ -33,19 +36,25 @@ class _Completion(Visitor):
|
|||||||
self._custom_functions = []
|
self._custom_functions = []
|
||||||
self._proc_signatures = {}
|
self._proc_signatures = {}
|
||||||
|
|
||||||
|
def _append_unique(self, item: lsp.CompletionItem):
|
||||||
|
# Avoid duplicate labels within the same file scan
|
||||||
|
if not any(ci.label == item.label for ci in self._custom_functions):
|
||||||
|
self._custom_functions.append(item)
|
||||||
|
|
||||||
def visit_command(self, command: Command):
|
def visit_command(self, command: Command):
|
||||||
routine = command.routine
|
routine = command.routine
|
||||||
|
|
||||||
|
# Collect custom proc names and their signatures
|
||||||
if routine.contents == "proc" and command.args:
|
if routine.contents == "proc" and command.args:
|
||||||
first_arg = command.args[0]
|
first_arg = command.args[0]
|
||||||
if hasattr(first_arg, "value") and not any(
|
if not getattr(first_arg, "value", None):
|
||||||
item.label == first_arg.value for item in self._custom_functions
|
return
|
||||||
):
|
|
||||||
self._custom_functions.append(
|
if any(item.label == first_arg.value for item in standard_items.nx_procs):
|
||||||
lsp.CompletionItem(
|
return
|
||||||
label=first_arg.value, kind=lsp.CompletionItemKind.Function
|
|
||||||
)
|
# Record proc name as a completion item
|
||||||
)
|
self._append_unique(lsp.CompletionItem(label=first_arg.value, kind=lsp.CompletionItemKind.Function))
|
||||||
if len(command.args) < 2:
|
if len(command.args) < 2:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -64,5 +73,52 @@ class _Completion(Visitor):
|
|||||||
|
|
||||||
self._proc_signatures[first_arg.value] = param_names
|
self._proc_signatures[first_arg.value] = param_names
|
||||||
|
|
||||||
|
# Collect global variables declared with: global var1 var2 ...
|
||||||
|
elif routine.contents == "global" and command.args:
|
||||||
|
for arg in command.args:
|
||||||
|
if isinstance(arg, BareWord) and getattr(arg, "value", None):
|
||||||
|
if arg.value not in BUILTIN_VAR_LABELS:
|
||||||
|
self._append_unique(lsp.CompletionItem(label=arg.value, kind=lsp.CompletionItemKind.Variable))
|
||||||
|
|
||||||
|
# Collect variables set with explicit global namespace: set ::var_name ...
|
||||||
|
elif routine.contents == "set" and command.args:
|
||||||
|
first = command.args[0]
|
||||||
|
if isinstance(first, BareWord) and getattr(first, "value", None):
|
||||||
|
var_name = first.value
|
||||||
|
if var_name.startswith("::"):
|
||||||
|
base_name = var_name.split("(", 1)[0]
|
||||||
|
clean_name = base_name[2:] # remove leading '::' for completion display
|
||||||
|
if clean_name not in BUILTIN_VAR_LABELS:
|
||||||
|
self._append_unique(lsp.CompletionItem(label=clean_name, kind=lsp.CompletionItemKind.Variable))
|
||||||
|
|
||||||
|
|
||||||
|
def remove_existing_items(items: list[lsp.CompletionItem], store: dict) -> None:
|
||||||
|
"""
|
||||||
|
Entfernt alle CompletionItems aus dem store, deren label in der items-Liste vorkommt.
|
||||||
|
Änderungen erfolgen in-place.
|
||||||
|
"""
|
||||||
|
labels_to_remove = {item.label for item in items}
|
||||||
|
|
||||||
|
for key in list(store.keys()):
|
||||||
|
filtered = [ci for ci in store[key] if ci.label not in labels_to_remove]
|
||||||
|
if filtered:
|
||||||
|
store[key] = filtered
|
||||||
|
else:
|
||||||
|
del store[key]
|
||||||
|
|
||||||
|
|
||||||
|
def remove_shared_keys(nested_dict: dict[str, dict[str, list]], flat_dict: dict[str, list]) -> None:
|
||||||
|
"""
|
||||||
|
Entfernt alle Keys aus nested_dict[file][func], wenn func auch in flat_dict vorhanden ist.
|
||||||
|
Änderungen erfolgen in-place.
|
||||||
|
"""
|
||||||
|
for file_path, func_dict in list(nested_dict.items()):
|
||||||
|
for func_name in list(func_dict.keys()):
|
||||||
|
if func_name in flat_dict:
|
||||||
|
del nested_dict[file_path][func_name]
|
||||||
|
|
||||||
|
if not nested_dict[file_path]:
|
||||||
|
del nested_dict[file_path]
|
||||||
|
|
||||||
|
|
||||||
completion = _Completion()
|
completion = _Completion()
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import lsprotocol.types as lsp
|
||||||
|
|
||||||
|
# Precompiled regex patterns for performance
|
||||||
|
NS_RE = re.compile(r"^\s*namespace\s+eval\s+([^\s\{]+)")
|
||||||
|
PROC_RE = re.compile(r"^\s*proc\s+([^\s\{]+)\s+\{.*\}\s+\{")
|
||||||
|
SET_RE = re.compile(r"^\s*set\s+([^\s\}]+)")
|
||||||
|
EVENT_START_RE = re.compile(r"^\s*LIB_GE_command_buffer_edit_(prepend|append|insert|replace)\b")
|
||||||
|
EVENT_CLOSE_INLINE_RE = re.compile(r"^\s*\}\s*(\S+)\s*.*$")
|
||||||
|
EVENT_NAME_LINE_RE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\b")
|
||||||
|
|
||||||
|
|
||||||
|
def build_document_symbols(source: str) -> list[lsp.DocumentSymbol]:
|
||||||
|
"""Parse a Tcl document source and return DocumentSymbols.
|
||||||
|
|
||||||
|
This mirrors the previous inline implementation in lsp_server, but is extracted
|
||||||
|
for readability and reuse.
|
||||||
|
"""
|
||||||
|
lines = source.split("\n")
|
||||||
|
|
||||||
|
class Scope:
|
||||||
|
def __init__(self, name: str, symbol: lsp.DocumentSymbol, start_line: int):
|
||||||
|
self.name = name
|
||||||
|
self.symbol = symbol
|
||||||
|
self.start_line = start_line
|
||||||
|
self.brace_count = 0
|
||||||
|
|
||||||
|
root_symbol = lsp.DocumentSymbol(
|
||||||
|
name="root",
|
||||||
|
detail="",
|
||||||
|
kind=lsp.SymbolKind.Namespace,
|
||||||
|
range=lsp.Range(start=lsp.Position(0, 0), end=lsp.Position(len(lines), 0)),
|
||||||
|
selection_range=lsp.Range(start=lsp.Position(0, 0), end=lsp.Position(0, 0)),
|
||||||
|
children=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_stack: list[Scope] = [Scope("", root_symbol, 0)]
|
||||||
|
pending_event: dict | None = None
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
# If inside an event, update its brace count for this line
|
||||||
|
if pending_event is not None:
|
||||||
|
pe_open = line.count("{")
|
||||||
|
pe_close = line.count("}")
|
||||||
|
pending_event["brace_count"] = pending_event.get("brace_count", 1) + pe_open - pe_close
|
||||||
|
# If this line closed the outer event block, finalize the event
|
||||||
|
if pending_event["brace_count"] <= 0:
|
||||||
|
# Try inline name on the same line
|
||||||
|
close_inline = EVENT_CLOSE_INLINE_RE.match(line)
|
||||||
|
if close_inline:
|
||||||
|
event_name = close_inline.group(1)
|
||||||
|
name_line_index = i
|
||||||
|
else:
|
||||||
|
# Look ahead to next non-empty line for the name
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines) and lines[j].strip() == "":
|
||||||
|
j += 1
|
||||||
|
event_name = None
|
||||||
|
name_line_index = i
|
||||||
|
if j < len(lines):
|
||||||
|
name_line = lines[j]
|
||||||
|
name_match = EVENT_NAME_LINE_RE.match(name_line)
|
||||||
|
if name_match:
|
||||||
|
event_name = name_match.group(1)
|
||||||
|
name_line_index = j
|
||||||
|
if event_name:
|
||||||
|
start_line = pending_event["start"]
|
||||||
|
edit_type = pending_event["edit_type"]
|
||||||
|
children = pending_event.get("children", [])
|
||||||
|
detail = f"Event ({edit_type})"
|
||||||
|
event_symbol = lsp.DocumentSymbol(
|
||||||
|
name=event_name,
|
||||||
|
detail=detail,
|
||||||
|
kind=lsp.SymbolKind.Event,
|
||||||
|
range=lsp.Range(start=lsp.Position(start_line, 0), end=lsp.Position(name_line_index, len(lines[name_line_index]))),
|
||||||
|
selection_range=lsp.Range(start=lsp.Position(name_line_index, 0), end=lsp.Position(name_line_index, len(lines[name_line_index]))),
|
||||||
|
children=children or [],
|
||||||
|
)
|
||||||
|
if scope_stack[-1].symbol.children is None:
|
||||||
|
scope_stack[-1].symbol.children = []
|
||||||
|
scope_stack[-1].symbol.children.append(event_symbol)
|
||||||
|
# Clear event tracking and continue
|
||||||
|
pending_event = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
ns_match = NS_RE.match(line)
|
||||||
|
proc_match = PROC_RE.match(line)
|
||||||
|
set_match = SET_RE.match(line)
|
||||||
|
|
||||||
|
# Namespace
|
||||||
|
if ns_match:
|
||||||
|
ns_name = ns_match.group(1)
|
||||||
|
start = lsp.Position(i, 0)
|
||||||
|
end = lsp.Position(i, len(line))
|
||||||
|
sel_start_char = line.find(ns_name)
|
||||||
|
sel_end_char = sel_start_char + len(ns_name) if sel_start_char >= 0 else len(line)
|
||||||
|
ns_symbol = lsp.DocumentSymbol(
|
||||||
|
name=ns_name,
|
||||||
|
detail="Namespace",
|
||||||
|
kind=lsp.SymbolKind.Namespace,
|
||||||
|
range=lsp.Range(start=start, end=end),
|
||||||
|
selection_range=lsp.Range(
|
||||||
|
start=lsp.Position(i, max(sel_start_char, 0)),
|
||||||
|
end=lsp.Position(i, max(sel_end_char, 0)),
|
||||||
|
),
|
||||||
|
children=[],
|
||||||
|
)
|
||||||
|
scope = Scope(ns_name, ns_symbol, i)
|
||||||
|
if scope_stack[-1].symbol.children is None:
|
||||||
|
scope_stack[-1].symbol.children = []
|
||||||
|
scope_stack[-1].symbol.children.append(ns_symbol)
|
||||||
|
scope_stack.append(scope)
|
||||||
|
|
||||||
|
# Proc
|
||||||
|
elif proc_match:
|
||||||
|
proc_name = proc_match.group(1)
|
||||||
|
start = lsp.Position(i, 0)
|
||||||
|
end = lsp.Position(i, len(line))
|
||||||
|
sel_start_char = line.find(proc_name)
|
||||||
|
sel_end_char = sel_start_char + len(proc_name) if sel_start_char >= 0 else len(line)
|
||||||
|
proc_symbol = lsp.DocumentSymbol(
|
||||||
|
name=proc_name,
|
||||||
|
detail="Procedure",
|
||||||
|
kind=lsp.SymbolKind.Function,
|
||||||
|
range=lsp.Range(start=start, end=end),
|
||||||
|
selection_range=lsp.Range(
|
||||||
|
start=lsp.Position(i, max(sel_start_char, 0)),
|
||||||
|
end=lsp.Position(i, max(sel_end_char, 0)),
|
||||||
|
),
|
||||||
|
children=[],
|
||||||
|
)
|
||||||
|
scope = Scope(proc_name, proc_symbol, i)
|
||||||
|
if scope_stack[-1].symbol.children is None:
|
||||||
|
scope_stack[-1].symbol.children = []
|
||||||
|
scope_stack[-1].symbol.children.append(proc_symbol)
|
||||||
|
scope_stack.append(scope)
|
||||||
|
|
||||||
|
# set variable
|
||||||
|
elif set_match:
|
||||||
|
var_name = set_match.group(1)
|
||||||
|
start = lsp.Position(i, 0)
|
||||||
|
end = lsp.Position(i, len(line))
|
||||||
|
sel_start_char = line.find(var_name)
|
||||||
|
sel_end_char = sel_start_char + len(var_name) if sel_start_char >= 0 else len(line)
|
||||||
|
var_symbol = lsp.DocumentSymbol(
|
||||||
|
name=var_name,
|
||||||
|
detail="Variable",
|
||||||
|
kind=lsp.SymbolKind.Variable,
|
||||||
|
range=lsp.Range(start=start, end=end),
|
||||||
|
selection_range=lsp.Range(
|
||||||
|
start=lsp.Position(i, max(sel_start_char, 0)),
|
||||||
|
end=lsp.Position(i, max(sel_end_char, 0)),
|
||||||
|
),
|
||||||
|
children=None,
|
||||||
|
)
|
||||||
|
# Attach to current scope or pending event as child
|
||||||
|
if pending_event is not None:
|
||||||
|
pending_event["children"].append(var_symbol)
|
||||||
|
else:
|
||||||
|
if scope_stack[-1].symbol.children is None:
|
||||||
|
scope_stack[-1].symbol.children = []
|
||||||
|
scope_stack[-1].symbol.children.append(var_symbol)
|
||||||
|
|
||||||
|
# Event start (buffer edit)
|
||||||
|
m = EVENT_START_RE.match(line)
|
||||||
|
if m:
|
||||||
|
edit_type = m.group(1)
|
||||||
|
pending_event = {"start": i, "edit_type": edit_type, "children": []}
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Brace balancing for scopes (namespace/proc)
|
||||||
|
open_count = line.count("{")
|
||||||
|
close_count = line.count("}")
|
||||||
|
scope_stack[-1].brace_count += open_count - close_count
|
||||||
|
|
||||||
|
# Close finished scopes
|
||||||
|
while len(scope_stack) > 1 and scope_stack[-1].brace_count <= 0:
|
||||||
|
finished = scope_stack.pop()
|
||||||
|
# Set the full range from startLine to current line
|
||||||
|
finished.symbol.range = lsp.Range(
|
||||||
|
start=lsp.Position(finished.start_line, 0),
|
||||||
|
end=lsp.Position(i, len(line)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return top-level children
|
||||||
|
return root_symbol.children
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SourcedFile:
|
||||||
|
layer_name: str
|
||||||
|
subfolder: Optional[str]
|
||||||
|
files: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
def read_psc_file(psc_file: Path) -> List[SourcedFile]:
|
||||||
|
tree = ET.parse(psc_file)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
layers = root.findall(".//Layer")
|
||||||
|
|
||||||
|
layer_info_list: List[SourcedFile] = []
|
||||||
|
for layer in layers:
|
||||||
|
layer_name = layer.attrib.get("Name")
|
||||||
|
subfolder = layer.attrib.get("SubFolder")
|
||||||
|
# Scripts-Filenames
|
||||||
|
scripts = layer.find("Scripts")
|
||||||
|
script_names = []
|
||||||
|
if scripts is not None:
|
||||||
|
for filename in scripts.findall("Filename"):
|
||||||
|
name = filename.attrib.get("Name")
|
||||||
|
if name:
|
||||||
|
script_names.append(name)
|
||||||
|
|
||||||
|
layer_info_list.append(
|
||||||
|
SourcedFile(layer_name=layer_name, subfolder=subfolder, files=script_names)
|
||||||
|
)
|
||||||
|
return layer_info_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_psc_files(root_path: Path) -> list[Path]:
|
||||||
|
return [path for path in root_path.rglob("*.psc")]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test = get_all_psc_files(
|
||||||
|
Path(
|
||||||
|
r"H:\janus-engineering-customers\KSB_Frankenthal\custom\library\machine\installed_machines\ksb_pe_grob_g550_sone\postprocessor"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(test)
|
||||||
|
for pp in test:
|
||||||
|
read_psc_file(pp)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from tclint.format import Formatter as BaseFormatter
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class NxFormatter(BaseFormatter):
|
||||||
|
"""
|
||||||
|
Custom formatter that inherits from tclint's Formatter but preserves explicit
|
||||||
|
line continuations (\\) inside braced expressions when they span multiple lines.
|
||||||
|
|
||||||
|
This avoids generating syntax errors in environments that require a trailing
|
||||||
|
backslash for multi-line expressions (e.g., certain NX Post interpreters),
|
||||||
|
while leaving all other formatting behavior unchanged.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def format_braced_expression(self, expr) -> List[str]: # type: ignore[override]
|
||||||
|
# This method mirrors BaseFormatter.format_braced_expression but inserts
|
||||||
|
# a line continuation (" \") between continuation lines similar to
|
||||||
|
# BaseFormatter.format_expression.
|
||||||
|
formatted = [""]
|
||||||
|
for child in expr.children:
|
||||||
|
lines = self.format(child)
|
||||||
|
formatted[-1] += lines[0]
|
||||||
|
for line in lines[1:]:
|
||||||
|
# add continuation on the previous line; keep next line at the same level
|
||||||
|
formatted[-1] += " \\" # keep explicit continuation
|
||||||
|
formatted += [line]
|
||||||
|
|
||||||
|
if expr.pos[0] == expr.end_pos[0]:
|
||||||
|
return self._brace(formatted)
|
||||||
|
|
||||||
|
return ["{"] + self._indent(formatted, self.opts.indent) + ["}"]
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import re
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from tclint.syntax_tree import Visitor, Command
|
||||||
|
from tools.parser import CustomParser
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_comment_prefix(line: str) -> str:
|
||||||
|
"""Strip leading '# ' or '#' from a line."""
|
||||||
|
if line.lstrip().startswith("#"):
|
||||||
|
# remove up to one leading '#' and one optional following space
|
||||||
|
return re.sub(r"^\s*#\s?", "", line)
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
def extract_doc_block_above(lines: List[str], start_line_index: int) -> str | None:
|
||||||
|
"""
|
||||||
|
Extract a contiguous block of line comments immediately above the given line index.
|
||||||
|
|
||||||
|
- lines: document split into lines
|
||||||
|
- start_line_index: 0-based index of the line where the proc command starts
|
||||||
|
|
||||||
|
Returns the cleaned documentation text or None if no comment block found.
|
||||||
|
"""
|
||||||
|
i = start_line_index - 1
|
||||||
|
if i < 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
doc_lines: List[str] = []
|
||||||
|
|
||||||
|
# Skip trailing empty lines directly above
|
||||||
|
while i >= 0 and lines[i].strip() == "":
|
||||||
|
i -= 1
|
||||||
|
|
||||||
|
# Collect contiguous comment lines going upwards
|
||||||
|
while i >= 0 and lines[i].lstrip().startswith("#"):
|
||||||
|
doc_lines.append(lines[i])
|
||||||
|
i -= 1
|
||||||
|
|
||||||
|
if not doc_lines:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Reverse to original order and strip comment prefixes
|
||||||
|
doc_lines.reverse()
|
||||||
|
cleaned = [_strip_comment_prefix(line_text) for line_text in doc_lines]
|
||||||
|
|
||||||
|
# Simple tag -> markdown conversions for nicer rendering
|
||||||
|
md_lines: List[str] = []
|
||||||
|
tag_map = {
|
||||||
|
"<Documentation>": "### Documentation",
|
||||||
|
"<Arguments>": "### Arguments",
|
||||||
|
"<Returnvalue>": "### Return value",
|
||||||
|
"<Example>": "### Example",
|
||||||
|
"<Internal Documentation>": "### Internal Documentation",
|
||||||
|
"<Internal Example>": "### Internal Example",
|
||||||
|
}
|
||||||
|
|
||||||
|
in_example = False
|
||||||
|
code_block_open = False
|
||||||
|
|
||||||
|
def close_code_block_if_open():
|
||||||
|
nonlocal code_block_open
|
||||||
|
if code_block_open:
|
||||||
|
md_lines.append("```")
|
||||||
|
code_block_open = False
|
||||||
|
|
||||||
|
for line in cleaned:
|
||||||
|
stripped = line.strip()
|
||||||
|
# Convert tags to headings and manage example sections
|
||||||
|
if stripped in tag_map:
|
||||||
|
# If we hit any new tag, close a pending code block
|
||||||
|
close_code_block_if_open()
|
||||||
|
heading = tag_map[stripped]
|
||||||
|
md_lines.append(heading)
|
||||||
|
in_example = heading in ("### Example", "### Internal Example")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_example:
|
||||||
|
low = stripped.lower()
|
||||||
|
if low.startswith("code:"):
|
||||||
|
# Open code block if needed and append the code content
|
||||||
|
code_text = line.split(":", 1)[1].strip()
|
||||||
|
if not code_block_open:
|
||||||
|
md_lines.append("```tcl")
|
||||||
|
code_block_open = True
|
||||||
|
md_lines.append(code_text)
|
||||||
|
continue
|
||||||
|
# Keep name/desc lines as regular text outside code
|
||||||
|
if low.startswith("name:") or low.startswith("desc:"):
|
||||||
|
md_lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
md_lines.append(line)
|
||||||
|
|
||||||
|
# Close any dangling code fence at the end of the block
|
||||||
|
close_code_block_if_open()
|
||||||
|
|
||||||
|
return "\n".join(md_lines).rstrip()
|
||||||
|
|
||||||
|
|
||||||
|
class ProcDocExtractor(Visitor):
|
||||||
|
"""Visitor that collects documentation blocks above proc declarations."""
|
||||||
|
|
||||||
|
def __init__(self, source_text: str):
|
||||||
|
super().__init__()
|
||||||
|
self._lines = source_text.split("\n")
|
||||||
|
self.docs: Dict[str, str] = {}
|
||||||
|
|
||||||
|
def visit_command(self, command: Command):
|
||||||
|
routine = getattr(command.routine, "contents", None)
|
||||||
|
if routine != "proc":
|
||||||
|
return
|
||||||
|
|
||||||
|
if not command.args:
|
||||||
|
return
|
||||||
|
name_node = command.args[0]
|
||||||
|
proc_name = getattr(name_node, "contents", None)
|
||||||
|
if not proc_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prefer line of the 'proc' keyword; fallback to the name node
|
||||||
|
pos = getattr(command.routine, "pos", None) or getattr(name_node, "pos", None)
|
||||||
|
if not pos:
|
||||||
|
return
|
||||||
|
line_idx = pos[0] - 1 # 0-based
|
||||||
|
block = extract_doc_block_above(self._lines, line_idx)
|
||||||
|
if block:
|
||||||
|
self.docs[proc_name] = block
|
||||||
|
|
||||||
|
|
||||||
|
def build_proc_docs(tree, source_text: str) -> Dict[str, str]:
|
||||||
|
"""Build a mapping of proc name -> markdown doc from a parsed tree and source text."""
|
||||||
|
extractor = ProcDocExtractor(source_text)
|
||||||
|
tree.accept(extractor, recurse=True)
|
||||||
|
return extractor.docs
|
||||||
|
|
||||||
|
|
||||||
|
def is_proc_declaration_position(source_text: str, line_zero_based: int, char_zero_based: int) -> bool:
|
||||||
|
"""Return True if the position is on a proc name within its declaration."""
|
||||||
|
parser = CustomParser()
|
||||||
|
tree = parser.parse(source_text)
|
||||||
|
|
||||||
|
# Walk commands to find 'proc' declarations and check if position intersects the name arg
|
||||||
|
class _DeclFinder(Visitor):
|
||||||
|
def __init__(self):
|
||||||
|
self.is_decl = False
|
||||||
|
|
||||||
|
def visit_command(self, command: Command):
|
||||||
|
if self.is_decl:
|
||||||
|
return
|
||||||
|
routine = getattr(command.routine, "contents", None)
|
||||||
|
if routine != "proc" or not command.args:
|
||||||
|
return
|
||||||
|
name_node = command.args[0]
|
||||||
|
if not hasattr(name_node, "pos"):
|
||||||
|
return
|
||||||
|
# Calculate range for the name token
|
||||||
|
try:
|
||||||
|
start_line, start_col = name_node.pos
|
||||||
|
end_line, end_col = getattr(name_node, "end_pos", name_node.pos)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
if start_line - 1 == line_zero_based:
|
||||||
|
length = 0
|
||||||
|
if hasattr(name_node, "value") and name_node.value is not None:
|
||||||
|
length = len(name_node.value)
|
||||||
|
elif hasattr(name_node, "contents") and name_node.contents is not None:
|
||||||
|
length = len(name_node.contents)
|
||||||
|
if length:
|
||||||
|
start_c = start_col - 1
|
||||||
|
end_c = start_c + length
|
||||||
|
if start_c <= char_zero_based <= end_c:
|
||||||
|
self.is_decl = True
|
||||||
|
|
||||||
|
finder = _DeclFinder()
|
||||||
|
tree.accept(finder, recurse=True)
|
||||||
|
return finder.is_decl
|
||||||
@@ -4,7 +4,7 @@ from tclint.syntax_tree import Visitor, QuotedWord, Command, BareWord
|
|||||||
from tclint.commands import get_commands
|
from tclint.commands import get_commands
|
||||||
import attrs
|
import attrs
|
||||||
from common.load_data import standard_items
|
from common.load_data import standard_items
|
||||||
from tools.completion_items import completion
|
import lsprotocol.types as lsp
|
||||||
|
|
||||||
|
|
||||||
class TokenModifier(enum.IntFlag):
|
class TokenModifier(enum.IntFlag):
|
||||||
@@ -25,6 +25,11 @@ class Token:
|
|||||||
tok_type: str = ""
|
tok_type: str = ""
|
||||||
tok_modifiers: List[TokenModifier] = attrs.field(factory=list)
|
tok_modifiers: List[TokenModifier] = attrs.field(factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self) -> int:
|
||||||
|
"""Compatibility alias for misspelled 'lenght' field."""
|
||||||
|
return self.lenght
|
||||||
|
|
||||||
|
|
||||||
TOKEN_TYPES = [
|
TOKEN_TYPES = [
|
||||||
"keyword",
|
"keyword",
|
||||||
@@ -40,10 +45,10 @@ TOKEN_TYPES = [
|
|||||||
|
|
||||||
|
|
||||||
class _Highlighter(Visitor):
|
class _Highlighter(Visitor):
|
||||||
def __init__(self, plugins, log_to_output):
|
def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]):
|
||||||
self._commands = get_commands(plugins)
|
self._commands = get_commands(plugins)
|
||||||
self._tokens = []
|
self._tokens = []
|
||||||
self.log_to_output = log_to_output
|
self.custom_functions = custom_functions
|
||||||
|
|
||||||
def _get_token_info(self, node):
|
def _get_token_info(self, node):
|
||||||
"""Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren."""
|
"""Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren."""
|
||||||
@@ -58,11 +63,7 @@ class _Highlighter(Visitor):
|
|||||||
# CompoundBareWord: versuche erstes Segment
|
# CompoundBareWord: versuche erstes Segment
|
||||||
if hasattr(node, "children") and node.children:
|
if hasattr(node, "children") and node.children:
|
||||||
first_segment = node.children[0]
|
first_segment = node.children[0]
|
||||||
if (
|
if hasattr(first_segment, "value") and first_segment.value is not None and hasattr(first_segment, "pos"):
|
||||||
hasattr(first_segment, "value")
|
|
||||||
and first_segment.value is not None
|
|
||||||
and hasattr(first_segment, "pos")
|
|
||||||
):
|
|
||||||
line, col = first_segment.pos
|
line, col = first_segment.pos
|
||||||
return (line - 1, col - 1), len(first_segment.value)
|
return (line - 1, col - 1), len(first_segment.value)
|
||||||
|
|
||||||
@@ -84,17 +85,23 @@ class _Highlighter(Visitor):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def visit_bare_word(self, word: BareWord):
|
def visit_bare_word(self, word: BareWord):
|
||||||
if any(item.label == word.value for item in standard_items.nx_procs) or any(
|
# Intentionally do not classify bare words as functions here.
|
||||||
item.label == word.value for item in completion.custom_functions
|
# Function highlighting is handled in visit_command for the routine only,
|
||||||
):
|
# using completion items (custom functions) as the source of truth.
|
||||||
line, col = word.pos
|
return
|
||||||
self._tokens.append(
|
|
||||||
(((line - 1, col - 1), len(word.value), "function", []))
|
|
||||||
)
|
|
||||||
|
|
||||||
def visit_command(self, command: Command):
|
def visit_command(self, command: Command):
|
||||||
routine = command.routine
|
routine = command.routine
|
||||||
|
|
||||||
|
# Highlight functions (custom or standard) when used as the routine
|
||||||
|
name = getattr(routine, "contents", None)
|
||||||
|
if name:
|
||||||
|
in_custom = any(item.label == name for items in self.custom_functions.values() for item in items)
|
||||||
|
in_standard = any(item.label == name for item in standard_items.nx_procs)
|
||||||
|
if in_custom or in_standard:
|
||||||
|
line, col = routine.contents_pos
|
||||||
|
self._tokens.append((((line - 1, col - 1), len(name), "function", [])))
|
||||||
|
|
||||||
if routine.contents == "puts":
|
if routine.contents == "puts":
|
||||||
line, col = routine.contents_pos
|
line, col = routine.contents_pos
|
||||||
self._tokens.append(
|
self._tokens.append(
|
||||||
@@ -158,9 +165,7 @@ class _Highlighter(Visitor):
|
|||||||
# Parameter mit Default-Wert ist meist eine List (z. B. {arg default})
|
# Parameter mit Default-Wert ist meist eine List (z. B. {arg default})
|
||||||
elif hasattr(child, "children") and len(child.children) >= 1:
|
elif hasattr(child, "children") and len(child.children) >= 1:
|
||||||
name_node = child.children[0]
|
name_node = child.children[0]
|
||||||
if hasattr(name_node, "value") and hasattr(
|
if hasattr(name_node, "value") and hasattr(name_node, "pos"):
|
||||||
name_node, "pos"
|
|
||||||
):
|
|
||||||
line, col = name_node.pos
|
line, col = name_node.pos
|
||||||
self._tokens.append(
|
self._tokens.append(
|
||||||
(
|
(
|
||||||
@@ -174,9 +179,7 @@ class _Highlighter(Visitor):
|
|||||||
first_arg = command.args[1]
|
first_arg = command.args[1]
|
||||||
if hasattr(first_arg, "pos") and first_arg.value is not None:
|
if hasattr(first_arg, "pos") and first_arg.value is not None:
|
||||||
line, col = first_arg.pos
|
line, col = first_arg.pos
|
||||||
self._tokens.append(
|
self._tokens.append((((line - 1, col - 1), len(first_arg.value), "class", [])))
|
||||||
(((line - 1, col - 1), len(first_arg.value), "class", []))
|
|
||||||
)
|
|
||||||
|
|
||||||
def tokens(self) -> list[Token]:
|
def tokens(self) -> list[Token]:
|
||||||
"""Encode tokens as described in
|
"""Encode tokens as described in
|
||||||
@@ -185,9 +188,7 @@ class _Highlighter(Visitor):
|
|||||||
tokens = []
|
tokens = []
|
||||||
last_line = 0
|
last_line = 0
|
||||||
last_col = 0
|
last_col = 0
|
||||||
for (line, col), length, tok_type, tok_modifier in sorted(
|
for (line, col), length, tok_type, tok_modifier in sorted(self._tokens, key=lambda x: x[0]):
|
||||||
self._tokens, key=lambda x: x[0]
|
|
||||||
):
|
|
||||||
line_delta = line - last_line
|
line_delta = line - last_line
|
||||||
col_delta = col
|
col_delta = col
|
||||||
if line == last_line:
|
if line == last_line:
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import List, DefaultDict, Union
|
||||||
|
|
||||||
|
from tclint.syntax_tree import Visitor, Command, CommandSub, Node, Script
|
||||||
|
|
||||||
|
|
||||||
|
class SymbolTable:
|
||||||
|
"""Holds a symbol table (links symbols to nodes)."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.proc_def: DefaultDict[str, list[Node]] = defaultdict(list)
|
||||||
|
|
||||||
|
def add_proc_definition(self, command: Command) -> None:
|
||||||
|
"""Add definition of procedure"""
|
||||||
|
# command holds the "proc" keyword, so the proc name is 1st argument
|
||||||
|
proc_name_node = command.args[0]
|
||||||
|
proc_name = proc_name_node.contents
|
||||||
|
if not proc_name:
|
||||||
|
return
|
||||||
|
logging.debug(
|
||||||
|
f"Definition of proc '{proc_name}' at {proc_name_node._pos_str()}"
|
||||||
|
)
|
||||||
|
self.proc_def[proc_name].append(proc_name_node)
|
||||||
|
|
||||||
|
def lookup_proc_definitions(self, symbol_text: str) -> List[Node]:
|
||||||
|
"""Lookup definitions of the procedure pointed at by node"""
|
||||||
|
if symbol_text is None or symbol_text not in self.proc_def:
|
||||||
|
return []
|
||||||
|
return self.proc_def[symbol_text]
|
||||||
|
|
||||||
|
|
||||||
|
class SymbolTableBuilder(Visitor):
|
||||||
|
"""Builds a symbol table."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.table = SymbolTable()
|
||||||
|
|
||||||
|
def build(self, tree: Union[CommandSub, Script]) -> SymbolTable:
|
||||||
|
"""Run the builder visitor through the syntax tree, building a table."""
|
||||||
|
tree.accept(self, recurse=True)
|
||||||
|
return self.table
|
||||||
|
|
||||||
|
def visit_command(self, command: Command) -> None:
|
||||||
|
if command.routine.contents == "proc":
|
||||||
|
self.table.add_proc_definition(command)
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, Set, List, Tuple
|
||||||
|
|
||||||
|
# Reuse patterns similar to document_symbols
|
||||||
|
NS_RE = re.compile(r"^\s*namespace\s+eval\s+([^\s\{]+)")
|
||||||
|
PROC_RE = re.compile(r"^\s*proc\s+([^\s\{]+)\s+\{.*\}\s+\{")
|
||||||
|
SET_RE = re.compile(r"^\s*set\s+([^\s\}]+)")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProcRange:
|
||||||
|
name: str
|
||||||
|
start_line: int
|
||||||
|
end_line: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def build_variable_index(source: str) -> tuple[Set[str], Dict[str, Set[str]], List[ProcRange]]:
|
||||||
|
"""
|
||||||
|
Parse Tcl source text and build:
|
||||||
|
- globals: set of variable names considered global suggestions
|
||||||
|
- procs: mapping proc_name -> set of local variable names (set without :: inside that proc)
|
||||||
|
- proc_ranges: list of ProcRange (name, start_line, end_line)
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- set ::var -> global var suggestion (strip leading :: and any array index "(")
|
||||||
|
- set var without :: at top level (not in namespace/proc) -> global suggestion
|
||||||
|
- set var without :: inside proc -> local to that proc
|
||||||
|
- set var inside namespace (no ::) is ignored for global suggestions
|
||||||
|
"""
|
||||||
|
lines = source.split("\n")
|
||||||
|
|
||||||
|
class Scope:
|
||||||
|
def __init__(self, name: str, kind: str, start_line: int):
|
||||||
|
self.name = name
|
||||||
|
self.kind = kind # "namespace" or "proc" or "root"
|
||||||
|
self.start_line = start_line
|
||||||
|
self.brace_count = 0
|
||||||
|
|
||||||
|
globals_set: Set[str] = set()
|
||||||
|
procs: Dict[str, Set[str]] = {}
|
||||||
|
proc_ranges: List[ProcRange] = []
|
||||||
|
|
||||||
|
scope_stack: List[Scope] = [Scope("", "root", 0)]
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
ns_match = NS_RE.match(line)
|
||||||
|
proc_match = PROC_RE.match(line)
|
||||||
|
set_match = SET_RE.match(line)
|
||||||
|
|
||||||
|
# Namespace scope
|
||||||
|
if ns_match:
|
||||||
|
scope_stack.append(Scope(ns_match.group(1), "namespace", i))
|
||||||
|
|
||||||
|
# Proc scope
|
||||||
|
elif proc_match:
|
||||||
|
pname = proc_match.group(1)
|
||||||
|
scope_stack.append(Scope(pname, "proc", i))
|
||||||
|
proc_ranges.append(ProcRange(name=pname, start_line=i, end_line=None))
|
||||||
|
|
||||||
|
# Track set statements
|
||||||
|
if set_match:
|
||||||
|
raw_name = set_match.group(1)
|
||||||
|
# Normalize array names and leading ::
|
||||||
|
base = raw_name.split("(", 1)[0]
|
||||||
|
if base.startswith("::"):
|
||||||
|
clean = base[2:]
|
||||||
|
globals_set.add(clean)
|
||||||
|
else:
|
||||||
|
top = scope_stack[-1]
|
||||||
|
if top.kind == "root":
|
||||||
|
globals_set.add(base)
|
||||||
|
elif top.kind == "proc":
|
||||||
|
procs.setdefault(top.name, set()).add(base)
|
||||||
|
else:
|
||||||
|
# inside namespace without :: -> ignore for globals
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Brace balancing for current top scope
|
||||||
|
open_count = line.count("{")
|
||||||
|
close_count = line.count("}")
|
||||||
|
scope_stack[-1].brace_count += open_count - close_count
|
||||||
|
|
||||||
|
# Close finished scopes
|
||||||
|
while len(scope_stack) > 1 and scope_stack[-1].brace_count <= 0:
|
||||||
|
finished = scope_stack.pop()
|
||||||
|
if finished.kind == "proc":
|
||||||
|
# Update the last matching proc range end_line
|
||||||
|
for pr in reversed(proc_ranges):
|
||||||
|
if pr.name == finished.name and pr.end_line is None:
|
||||||
|
pr.end_line = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Finalize any unterminated proc ranges
|
||||||
|
for pr in proc_ranges:
|
||||||
|
if pr.end_line is None:
|
||||||
|
pr.end_line = len(lines) - 1
|
||||||
|
|
||||||
|
return globals_set, procs, proc_ranges
|
||||||
|
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
THIS_DIR = Path(__file__).parent
|
||||||
|
SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||||
|
if str(SRC_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC_DIR))
|
||||||
|
|
||||||
|
import lsprotocol.types as lsp # type: ignore
|
||||||
|
from lsp_server import LSP_SERVER, document_symbols # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_document_symbols_namespace_proc_set_hierarchy(tmp_path: Path):
|
||||||
|
source_lines = [
|
||||||
|
"set top_var 1",
|
||||||
|
"namespace eval myns {",
|
||||||
|
" set ns_var 2",
|
||||||
|
" proc add {a b} {",
|
||||||
|
" set sum [expr {$a + $b}]",
|
||||||
|
" return $sum",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
"proc top_proc {} {",
|
||||||
|
" set x 3",
|
||||||
|
"}",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
uri = Path(tmp_path / "sym.tcl").as_uri()
|
||||||
|
# Put a text document into the workspace
|
||||||
|
LSP_SERVER.workspace.put_text_document(lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source))
|
||||||
|
|
||||||
|
# Request document symbols
|
||||||
|
params = lsp.DocumentSymbolParams(text_document=lsp.TextDocumentIdentifier(uri=uri))
|
||||||
|
symbols = document_symbols(params)
|
||||||
|
|
||||||
|
# Expect at least 2 top-level children: root contains 'set top_var' (variable) and 'namespace myns' and 'proc top_proc'
|
||||||
|
names_kinds = {(s.name, s.kind) for s in symbols}
|
||||||
|
assert ("root", lsp.SymbolKind.Namespace) not in names_kinds # root should not be included itself
|
||||||
|
|
||||||
|
# Find namespace symbol
|
||||||
|
ns = next(s for s in symbols if s.name == "myns")
|
||||||
|
assert ns.kind == lsp.SymbolKind.Namespace
|
||||||
|
assert ns.children is not None
|
||||||
|
|
||||||
|
# Inside namespace: has variable and proc
|
||||||
|
child_names = {c.name for c in ns.children}
|
||||||
|
assert "ns_var" in child_names
|
||||||
|
assert "add" in child_names
|
||||||
|
|
||||||
|
# top-level variable and proc also present
|
||||||
|
top_names = {s.name for s in symbols}
|
||||||
|
assert "top_var" in top_names
|
||||||
|
assert "top_proc" in top_names
|
||||||
|
|
||||||
|
# Check that proc add has no children (we're not extracting params as children here)
|
||||||
|
add = next(c for c in ns.children if c.name == "add")
|
||||||
|
assert add.kind == lsp.SymbolKind.Function
|
||||||
|
assert add.children == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_buffer_edit_events_are_symbols_with_type_and_name(tmp_path: Path):
|
||||||
|
source_lines = [
|
||||||
|
"LIB_GE_command_buffer_edit_insert LIB_ROTARY_positioning_first_move_pos ROTARY_POSITIONING_FIRST_MOVE_POS {",
|
||||||
|
" MOM_enable_address Z M_coolant_off D M_coolant_1 M_coolant_2 H_pressure",
|
||||||
|
"}",
|
||||||
|
" Coolant after @DECOMPOSEZUL",
|
||||||
|
"",
|
||||||
|
"LIB_GE_command_buffer_edit_append MOM_start_of_path_LIB MOM_start_of_path_LIB_ENTRY_end {",
|
||||||
|
" MOM_force once M_coolant_1 M_coolant_2 H_pressure",
|
||||||
|
"}",
|
||||||
|
" force_coolant",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
uri = Path(tmp_path / "events.tcl").as_uri()
|
||||||
|
LSP_SERVER.workspace.put_text_document(lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source))
|
||||||
|
params = lsp.DocumentSymbolParams(text_document=lsp.TextDocumentIdentifier(uri=uri))
|
||||||
|
symbols = document_symbols(params)
|
||||||
|
|
||||||
|
# Find event symbols
|
||||||
|
events = [s for s in symbols if s.kind == lsp.SymbolKind.Event]
|
||||||
|
assert events, "Expected at least one event symbol"
|
||||||
|
|
||||||
|
# Verify names and details
|
||||||
|
names = [e.name for e in events]
|
||||||
|
assert "Coolant" in names or "force_coolant" in names
|
||||||
|
for e in events:
|
||||||
|
assert e.detail.startswith("Event (")
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_children_include_set_variable(tmp_path: Path):
|
||||||
|
source_lines = [
|
||||||
|
"LIB_GE_command_buffer_edit_append MOM_rapid_move_LIB MOM_rapid_move_LIB_ENTRY_start {",
|
||||||
|
" if {[info exists ::mom_lift_off_output] && $::kapp_vars(retract_start) == 0} {",
|
||||||
|
" kapp_retract_subpgm",
|
||||||
|
" }",
|
||||||
|
" set ::kapp_vars(retract_start) 0",
|
||||||
|
"}",
|
||||||
|
" KappRetractSubPgm",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
uri = Path(tmp_path / "event_children.tcl").as_uri()
|
||||||
|
LSP_SERVER.workspace.put_text_document(lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source))
|
||||||
|
params = lsp.DocumentSymbolParams(text_document=lsp.TextDocumentIdentifier(uri=uri))
|
||||||
|
symbols = document_symbols(params)
|
||||||
|
|
||||||
|
events = [s for s in symbols if s.kind == lsp.SymbolKind.Event and s.name == "KappRetractSubPgm"]
|
||||||
|
assert events, "Expected event symbol for KappRetractSubPgm"
|
||||||
|
ev = events[0]
|
||||||
|
assert ev.children is not None
|
||||||
|
# Ensure the set variable is a child of the event
|
||||||
|
child_names = {c.name for c in ev.children}
|
||||||
|
assert "::kapp_vars(retract_start)" in child_names
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Ensure server/src is on the path for imports
|
||||||
|
THIS_DIR = Path(__file__).parent
|
||||||
|
SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||||
|
if str(SRC_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC_DIR))
|
||||||
|
|
||||||
|
import lsprotocol.types as lsp # type: ignore
|
||||||
|
from lsp_server import LSP_SERVER, goto_definition # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def _loc_to_tuple(loc: lsp.Location) -> tuple[str, int, int, int, int]:
|
||||||
|
"""Helper to normalize Location into a tuple for easy asserts."""
|
||||||
|
return (
|
||||||
|
loc.uri,
|
||||||
|
loc.range.start.line,
|
||||||
|
loc.range.start.character,
|
||||||
|
loc.range.end.line,
|
||||||
|
loc.range.end.character,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_first_location(result) -> lsp.Location | None:
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
if isinstance(result, list):
|
||||||
|
return result[0] if result else None
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def test_goto_definition_same_file(tmp_path: Path):
|
||||||
|
source_lines = [
|
||||||
|
"proc add {a b} {",
|
||||||
|
" return [expr {$a + $b}]",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
"set x [add 1 2]",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
uri = Path(tmp_path / "same.tcl").as_uri()
|
||||||
|
# Put a text document into the workspace
|
||||||
|
LSP_SERVER.workspace.put_text_document(
|
||||||
|
lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Position on the word 'add' in the last line
|
||||||
|
line_idx = 4
|
||||||
|
char_idx = source_lines[line_idx].index("add") + 1 # somewhere inside token
|
||||||
|
|
||||||
|
params = lsp.DefinitionParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=uri),
|
||||||
|
position=lsp.Position(line=line_idx, character=char_idx),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = goto_definition(params)
|
||||||
|
loc = _extract_first_location(result)
|
||||||
|
|
||||||
|
assert loc is not None
|
||||||
|
assert loc.uri == uri
|
||||||
|
# Definition should be on line 0 at the token 'add'
|
||||||
|
start = loc.range.start
|
||||||
|
end = loc.range.end
|
||||||
|
assert start.line == 0
|
||||||
|
assert end.line == 0
|
||||||
|
assert source_lines[0][start.character : end.character] == "add"
|
||||||
|
|
||||||
|
|
||||||
|
def test_goto_definition_cross_file(tmp_path: Path):
|
||||||
|
# File A declares the proc
|
||||||
|
a_lines = [
|
||||||
|
"proc myproc {arg} {",
|
||||||
|
" return $arg",
|
||||||
|
"}",
|
||||||
|
]
|
||||||
|
a_src = "\n".join(a_lines)
|
||||||
|
a_path = tmp_path / "a.tcl"
|
||||||
|
a_uri = a_path.as_uri()
|
||||||
|
LSP_SERVER.workspace.put_text_document(
|
||||||
|
lsp.TextDocumentItem(uri=a_uri, language_id="tcl", version=1, text=a_src)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update indices for file A so proc_signatures gets populated
|
||||||
|
doc_a = LSP_SERVER.workspace.get_text_document(a_uri)
|
||||||
|
LSP_SERVER.update_poco_completion_for_file(doc_a)
|
||||||
|
|
||||||
|
# File B calls the proc
|
||||||
|
b_lines = [
|
||||||
|
"set y [myproc 42]",
|
||||||
|
]
|
||||||
|
b_src = "\n".join(b_lines)
|
||||||
|
b_uri = (tmp_path / "b.tcl").as_uri()
|
||||||
|
LSP_SERVER.workspace.put_text_document(
|
||||||
|
lsp.TextDocumentItem(uri=b_uri, language_id="tcl", version=1, text=b_src)
|
||||||
|
)
|
||||||
|
|
||||||
|
call_line = 0
|
||||||
|
call_char = b_lines[0].index("myproc") + 2
|
||||||
|
|
||||||
|
params = lsp.DefinitionParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=b_uri),
|
||||||
|
position=lsp.Position(line=call_line, character=call_char),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = goto_definition(params)
|
||||||
|
loc = _extract_first_location(result)
|
||||||
|
|
||||||
|
assert loc is not None
|
||||||
|
assert loc.uri == a_uri
|
||||||
|
start = loc.range.start
|
||||||
|
end = loc.range.end
|
||||||
|
assert start.line == 0
|
||||||
|
assert a_lines[start.line][start.character : end.character] == "myproc"
|
||||||
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Ensure server/src is on the path for imports
|
||||||
|
THIS_DIR = Path(__file__).parent
|
||||||
|
SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||||
|
if str(SRC_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC_DIR))
|
||||||
|
|
||||||
|
import lsprotocol.types as lsp # type: ignore
|
||||||
|
from tools.proc_docs import build_proc_docs, is_proc_declaration_position
|
||||||
|
from lsp_server import LSP_SERVER, hover # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_proc_docs_extracts_block_and_tags():
|
||||||
|
source = (
|
||||||
|
"#____________________________________________________________________________________________\n"
|
||||||
|
"# <Documentation>\n"
|
||||||
|
"# This procedure creates a new directory if it does not exist.\n"
|
||||||
|
"# <Arguments>\n"
|
||||||
|
"# directory\n"
|
||||||
|
"#\tThe full pathname of the directory to be created.\n"
|
||||||
|
"# <Returnvalue>\n"
|
||||||
|
"# 0 - directory created or already exists\n"
|
||||||
|
"# 1 - error\n"
|
||||||
|
"#______________________________________________________________________________________________\n"
|
||||||
|
"proc LIB_FH_create_directory {directory} {\n"
|
||||||
|
" return 0\n"
|
||||||
|
"}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
from tools.parser import CustomParser
|
||||||
|
|
||||||
|
tree = CustomParser().parse(source)
|
||||||
|
docs = build_proc_docs(tree, source)
|
||||||
|
|
||||||
|
assert "LIB_FH_create_directory" in docs
|
||||||
|
md = docs["LIB_FH_create_directory"]
|
||||||
|
# Tags become markdown headings
|
||||||
|
assert "### Documentation" in md
|
||||||
|
assert "### Arguments" in md
|
||||||
|
assert "### Return value" in md
|
||||||
|
# Content preserved
|
||||||
|
assert "creates a new directory" in md
|
||||||
|
|
||||||
|
|
||||||
|
def test_hover_shows_doc_on_usage_but_not_on_declaration(tmp_path: Path):
|
||||||
|
# Build a TCL file with a documented proc and a usage
|
||||||
|
source_lines = [
|
||||||
|
"#_________________________________________________________________________________________________",
|
||||||
|
"# <Documentation>",
|
||||||
|
"# Function to delete the file",
|
||||||
|
"#_________________________________________________________________________________________________",
|
||||||
|
"proc SERVICE_remove_file {file} {",
|
||||||
|
" if {![SERVICE_check_file_exists $file]} {return}",
|
||||||
|
" MOM_remove_file $file",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
"proc SERVICE_check_file_exists {file} {",
|
||||||
|
" if {[file exists $file]} {return 1}",
|
||||||
|
" return 0",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
"# usage below",
|
||||||
|
"SERVICE_remove_file \"C:/tmp/x\"",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
# Register document with server
|
||||||
|
uri = Path(tmp_path / "test.tcl").as_uri()
|
||||||
|
LSP_SERVER.workspace.put_text_document(
|
||||||
|
lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Force server to parse and build proc docs
|
||||||
|
doc = LSP_SERVER.workspace.get_text_document(uri)
|
||||||
|
LSP_SERVER.update_poco_completion_for_file(doc)
|
||||||
|
|
||||||
|
# 1) Hover on usage -> should return docs
|
||||||
|
usage_line = source_lines.index("SERVICE_remove_file \"C:/tmp/x\"")
|
||||||
|
char_index = source_lines[usage_line].find("SERVICE_remove_file") + 5 # inside the token
|
||||||
|
|
||||||
|
params = lsp.HoverParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=uri),
|
||||||
|
position=lsp.Position(line=usage_line, character=char_index),
|
||||||
|
)
|
||||||
|
result = hover(params)
|
||||||
|
assert result is not None
|
||||||
|
assert "Function to delete the file" in result.contents.value # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
# 2) Hover on declaration name -> should be None
|
||||||
|
decl_line = source_lines.index("proc SERVICE_remove_file {file} {")
|
||||||
|
decl_char = source_lines[decl_line].find("SERVICE_remove_file") + 2
|
||||||
|
assert is_proc_declaration_position(source, decl_line, decl_char)
|
||||||
|
|
||||||
|
params_decl = lsp.HoverParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=uri),
|
||||||
|
position=lsp.Position(line=decl_line, character=decl_char),
|
||||||
|
)
|
||||||
|
none_result = hover(params_decl)
|
||||||
|
assert none_result is None
|
||||||
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Ensure server/src is on sys.path for imports
|
||||||
|
THIS_DIR = Path(__file__).parent
|
||||||
|
SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||||
|
if str(SRC_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC_DIR))
|
||||||
|
|
||||||
|
import lsprotocol.types as lsp # type: ignore
|
||||||
|
from tools.parser import CustomParser
|
||||||
|
from tools.proc_docs import build_proc_docs
|
||||||
|
|
||||||
|
|
||||||
|
def test_example_is_marked_as_tcl_code_block():
|
||||||
|
source_lines = [
|
||||||
|
"#____________________________________________________________________________________________",
|
||||||
|
"# <Documentation>",
|
||||||
|
"# This procedure creates a new directory if it does not exist.",
|
||||||
|
"# <Arguments>",
|
||||||
|
"# directory",
|
||||||
|
"#\tThe full pathname of the directory to be created.",
|
||||||
|
"# <Returnvalue>",
|
||||||
|
"# 0 - directory created or already exists",
|
||||||
|
"# 1 - error",
|
||||||
|
"# <Example>",
|
||||||
|
"# name: Example 1",
|
||||||
|
"# code: LIB_FH_create_directory \"C:/Temp/Test\"",
|
||||||
|
"# desc: If error = 0, the directory is created.",
|
||||||
|
"proc LIB_FH_create_directory {directory} {",
|
||||||
|
" return 0",
|
||||||
|
"}",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
tree = CustomParser().parse(source)
|
||||||
|
docs = build_proc_docs(tree, source)
|
||||||
|
|
||||||
|
assert "LIB_FH_create_directory" in docs
|
||||||
|
md = docs["LIB_FH_create_directory"]
|
||||||
|
|
||||||
|
# Headings preserved
|
||||||
|
assert "### Documentation" in md
|
||||||
|
assert "### Arguments" in md
|
||||||
|
assert "### Return value" in md
|
||||||
|
assert "### Example" in md
|
||||||
|
|
||||||
|
# Code fence with tcl language hint and the code line present
|
||||||
|
assert "```tcl" in md
|
||||||
|
assert "LIB_FH_create_directory \"C:/Temp/Test\"" in md
|
||||||
|
assert md.strip().endswith("```")
|
||||||
|
|
||||||
|
|
||||||
|
def test_internal_example_is_marked_as_tcl_code_block():
|
||||||
|
source_lines = [
|
||||||
|
"# <Internal Documentation>",
|
||||||
|
"# Helper utility",
|
||||||
|
"# <Internal Example>",
|
||||||
|
"# code: puts \"hello\"",
|
||||||
|
"proc helper {} {",
|
||||||
|
" return",
|
||||||
|
"}",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
tree = CustomParser().parse(source)
|
||||||
|
docs = build_proc_docs(tree, source)
|
||||||
|
|
||||||
|
assert "helper" in docs
|
||||||
|
md = docs["helper"]
|
||||||
|
|
||||||
|
assert "### Internal Documentation" in md
|
||||||
|
assert "### Internal Example" in md
|
||||||
|
assert "```tcl" in md and "puts \"hello\"" in md and md.strip().endswith("```")
|
||||||
|
|
||||||
+59
-214
@@ -1,30 +1,19 @@
|
|||||||
{
|
{
|
||||||
"fileTypes": [
|
"fileTypes": [ "tcl" ],
|
||||||
"tcl"
|
|
||||||
],
|
|
||||||
"name": "tcl",
|
"name": "tcl",
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{ "include": "#main" }
|
||||||
"include": "#main"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"scopeName": "source.tcl",
|
"scopeName": "source.tcl",
|
||||||
|
"uuid": "c7017136-2ff2-48e9-bdb0-570cf238b4a2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"main": {
|
"main": {
|
||||||
"patterns": [
|
"patterns": [ { "include": "#command" } ]
|
||||||
{
|
|
||||||
"include": "#command"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"args": {
|
"args": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{ "include": "#quoted_args" },
|
||||||
"include": "#quoted_args"
|
{ "include": "#numeric" },
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#numeric"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"match": "((?<=;)\\s*#.*$)",
|
"match": "((?<=;)\\s*#.*$)",
|
||||||
"name": "comment.tcl",
|
"name": "comment.tcl",
|
||||||
@@ -36,22 +25,10 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"begin": "((?<!\\\\)\\{)",
|
"begin": "((?<!\\\\)\\{)",
|
||||||
"beginCaptures": {
|
"beginCaptures": { "1": { "name": "punctuation.tcl" } },
|
||||||
"1": {
|
"patterns": [ { "include": "#braced__1" } ],
|
||||||
"name": "punctuation.tcl"
|
"end": "(\\}([^\\s\\]\\x{0022}]*))",
|
||||||
}
|
"endCaptures": { "1": { "name": "punctuation.tcl" } }
|
||||||
},
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"include": "#braced__1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": "(\\}([^\\s\\]]*))",
|
|
||||||
"endCaptures": {
|
|
||||||
"1": {
|
|
||||||
"name": "punctuation.tcl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -61,42 +38,24 @@
|
|||||||
"match": "(\\\\(?:\\d{1,3}|x[a-fA-F0-9]{1,2}|u[a-fA-F0-9]{1,4}|U[a-fA-F0-9]{1,8}|.))",
|
"match": "(\\\\(?:\\d{1,3}|x[a-fA-F0-9]{1,2}|u[a-fA-F0-9]{1,4}|U[a-fA-F0-9]{1,8}|.))",
|
||||||
"name": "constant.character.escape.tcl"
|
"name": "constant.character.escape.tcl"
|
||||||
},
|
},
|
||||||
{
|
{ "include": "#keywords" },
|
||||||
"include": "#keywords"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"match": "((?<={)\\s*(?:after|append|apply|array|auto_execok|auto_import|auto_load|auto_mkindex|auto_qualify|auto_reset|bgerror|binary|break|catch|cd|chan|clock|close|concat|continue|coroutine|dde|dict|encoding|eof|error|eval|exec|exit|expr|fblocked|fconfigure|fcopy|fileevent|file|flush|foreach|for|format|gets|global|glob|history|http|if|incr|info|interp|join|lappend|lassign|lindex|linsert|list|llength|lmap|load|lrange|lrepeat|lreplace|lreverse|lsearch|lset|lsort|memory|my|namespace|nextto|next|oo::class|oo::copy|oo::define|oo::objdefine|oo::object|open|package|parray|pid|pkg::create|pkg_mkIndex|platform::shell|proc|puts|pwd|read|regexp|registry|regsub|rename|return|scan|seek|self|set|socket|source|split|string|subst|switch|tailcall|tcl::prefix|tcl_endOfWord|tcl_findLibrary|tcl_startOfNextWord|tcl_startOfPreviousWord|tcl_wordBreakAfter|tcl_wordBreakBefore|tell|throw|time|trace|try|unknown|unload|unset|update|uplevel|upvar|variable|vwait|while|yieldto|yield)\\s+)",
|
"match": "((?<={)\\s*(?:after|append|apply|array|auto_execok|auto_import|auto_load|auto_mkindex|auto_qualify|auto_reset|bgerror|binary|break|catch|cd|chan|clock|close|concat|continue|coroutine|dde|dict|encoding|eof|error|eval|exec|exit|expr|fblocked|fconfigure|fcopy|fileevent|file|flush|foreach|for|format|gets|global|glob|history|http|if|incr|info|interp|join|lappend|lassign|lindex|linsert|list|llength|lmap|load|lrange|lrepeat|lreplace|lreverse|lsearch|lset|lsort|memory|my|namespace|nextto|next|oo::class|oo::copy|oo::define|oo::objdefine|oo::object|open|package|parray|pid|pkg::create|pkg_mkIndex|platform::shell|proc|puts|pwd|read|regexp|registry|regsub|rename|return|scan|seek|self|set|socket|source|split|string|subst|switch|tailcall|tcl::prefix|tcl_endOfWord|tcl_findLibrary|tcl_startOfNextWord|tcl_startOfPreviousWord|tcl_wordBreakAfter|tcl_wordBreakBefore|tell|throw|time|trace|try|unknown|unload|unset|update|uplevel|upvar|variable|vwait|while|yieldto|yield)\\s+)",
|
||||||
"name": "keyword.tcl",
|
"name": "keyword.tcl",
|
||||||
"comment": "Special handling for known commands."
|
"comment": "Special handling for known commands."
|
||||||
},
|
},
|
||||||
{
|
{ "include": "#braced_inner" },
|
||||||
"include": "#braced_inner"
|
{ "include": "#args" }
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#args"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"braced_inner": {
|
"braced_inner": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"begin": "(\\{)",
|
"begin": "(\\{)",
|
||||||
"beginCaptures": {
|
"beginCaptures": { "1": { "name": "punctuation.tcl" } },
|
||||||
"1": {
|
"patterns": [ { "include": "#braced_inner__1" } ],
|
||||||
"name": "punctuation.tcl"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"include": "#braced_inner__1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": "(\\})",
|
"end": "(\\})",
|
||||||
"endCaptures": {
|
"endCaptures": { "1": { "name": "punctuation.tcl" } }
|
||||||
"1": {
|
|
||||||
"name": "punctuation.tcl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -106,15 +65,9 @@
|
|||||||
"match": "(\\\\[\\x{007b}\\x{007d}])",
|
"match": "(\\\\[\\x{007b}\\x{007d}])",
|
||||||
"name": "constant.character.escape.tcl"
|
"name": "constant.character.escape.tcl"
|
||||||
},
|
},
|
||||||
{
|
{ "include": "#numeric" },
|
||||||
"include": "#numeric"
|
{ "include": "#braced_inner" },
|
||||||
},
|
{ "include": "#args" }
|
||||||
{
|
|
||||||
"include": "#braced_inner"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#args"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"braced_lit": {
|
"braced_lit": {
|
||||||
@@ -139,22 +92,10 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"begin": "(\\\\\\s*$)",
|
"begin": "(\\\\\\s*$)",
|
||||||
"beginCaptures": {
|
"beginCaptures": { "1": { "name": "constant.character.escape.tcl" } },
|
||||||
"1": {
|
"patterns": [ { "include": "#command__1" } ],
|
||||||
"name": "constant.character.escape.tcl"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"include": "#command__1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": "((?<!\\\\\\s)$)",
|
"end": "((?<!\\\\\\s)$)",
|
||||||
"endCaptures": {
|
"endCaptures": { "1": { "name": "none.tcl" } }
|
||||||
"1": {
|
|
||||||
"name": "none.tcl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"match": "(^\\s*#.*)",
|
"match": "(^\\s*#.*)",
|
||||||
@@ -169,73 +110,41 @@
|
|||||||
{
|
{
|
||||||
"match": "(?<=^|{)(\\s*proc\\s+)(\\S+\\s+)((?:(?<!\\\\){)(?:[^\\x{007b}\\x{007d}\\n]*(?:\\g<1>(?:\\\\{|\\\\}|[^\\x{007b}\\x{007d}])*)*)})",
|
"match": "(?<=^|{)(\\s*proc\\s+)(\\S+\\s+)((?:(?<!\\\\){)(?:[^\\x{007b}\\x{007d}\\n]*(?:\\g<1>(?:\\\\{|\\\\}|[^\\x{007b}\\x{007d}])*)*)})",
|
||||||
"captures": {
|
"captures": {
|
||||||
"1": {
|
"1": { "name": "keyword.tcl" },
|
||||||
"name": "keyword.tcl"
|
"2": { "name": "entity.name.function.tcl" },
|
||||||
},
|
"3": { "name": "none.tcl" }
|
||||||
"2": {
|
|
||||||
"name": "entity.name.function.tcl"
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"name": "none.tcl"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"comment": "Proc command."
|
"comment": "Proc command."
|
||||||
},
|
},
|
||||||
|
{ "include": "#regexp" },
|
||||||
|
{ "include": "#keywords" },
|
||||||
{
|
{
|
||||||
"include": "#regexp"
|
"match": "(^\\s*[^\\s\\x{0022}]+|(?:(?<=[^\\x{005c}]\\[)[^\\s\\]]+))",
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#keywords"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": "(^\\s*\\S+|(?:(?<=[^\\x{005c}]\\[)[^\\s\\]]++(?!\\])))",
|
|
||||||
"name": "keyword.tcl",
|
"name": "keyword.tcl",
|
||||||
"comment": "All other commands. (NOTE: Iro doesn't support possessive quantifiers, but the grammar does; be sure to replace the second + with ++ after regenerating!)"
|
"comment": "All other commands. (NOTE: Iro doesn't support possessive quantifiers, but the grammar does; be sure to replace the second + with ++ after regenerating!)"
|
||||||
},
|
},
|
||||||
{
|
{ "include": "#args" }
|
||||||
"include": "#args"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"command__1": {
|
"command__1": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{ "include": "#keywords" },
|
||||||
"include": "#keywords"
|
{ "include": "#args" }
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#args"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"embedded": {
|
"embedded": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"begin": "((?<!\\\\)\\[)",
|
"begin": "((?<!\\\\)\\[)",
|
||||||
"beginCaptures": {
|
"beginCaptures": { "1": { "name": "punctuation.tcl" } },
|
||||||
"1": {
|
"patterns": [ { "include": "#embedded__1" } ],
|
||||||
"name": "punctuation.tcl"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"include": "#embedded__1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": "((?<!\\\\)\\])",
|
"end": "((?<!\\\\)\\])",
|
||||||
"endCaptures": {
|
"endCaptures": { "1": { "name": "punctuation.tcl" } }
|
||||||
"1": {
|
|
||||||
"name": "punctuation.tcl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"embedded__1": {
|
"embedded__1": {
|
||||||
"patterns": [
|
"patterns": [ { "include": "#command" } ]
|
||||||
{
|
|
||||||
"include": "#command"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"keywords": {
|
"keywords": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
@@ -263,30 +172,16 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"begin": "((?<!\\\\)\")",
|
"begin": "((?<!\\\\)\")",
|
||||||
"beginCaptures": {
|
"beginCaptures": { "1": { "name": "string.tcl" } },
|
||||||
"1": {
|
"patterns": [ { "include": "#quoted__1" } ],
|
||||||
"name": "string.tcl"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"include": "#quoted__1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": "(\")",
|
"end": "(\")",
|
||||||
"endCaptures": {
|
"endCaptures": { "1": { "name": "string.tcl" } }
|
||||||
"1": {
|
|
||||||
"name": "string.tcl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"quoted__1": {
|
"quoted__1": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{ "include": "#quoted_args" },
|
||||||
"include": "#quoted_args"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"match": "([^\\x{0024}\\x{005b}\\x{0022}\\x{005c}]+)",
|
"match": "([^\\x{0024}\\x{005b}\\x{0022}\\x{005c}]+)",
|
||||||
"name": "string.tcl"
|
"name": "string.tcl"
|
||||||
@@ -295,22 +190,14 @@
|
|||||||
},
|
},
|
||||||
"quoted_args": {
|
"quoted_args": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{ "include": "#braced" },
|
||||||
"include": "#braced"
|
{ "include": "#quoted" },
|
||||||
},
|
{ "include": "#embedded" },
|
||||||
{
|
|
||||||
"include": "#quoted"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#embedded"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"match": "(\\\\(?:\\d{1,3}|x[a-fA-F0-9]{1,2}|u[a-fA-F0-9]{1,4}|U[a-fA-F0-9]{1,8}|.))",
|
"match": "(\\\\(?:\\d{1,3}|x[a-fA-F0-9]{1,2}|u[a-fA-F0-9]{1,4}|U[a-fA-F0-9]{1,8}|.))",
|
||||||
"name": "constant.character.escape.tcl"
|
"name": "constant.character.escape.tcl"
|
||||||
},
|
},
|
||||||
{
|
{ "include": "#variable" }
|
||||||
"include": "#variable"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"regexp": {
|
"regexp": {
|
||||||
@@ -318,57 +205,27 @@
|
|||||||
{
|
{
|
||||||
"begin": "((?<=^|[\\[\\x{007b}\\x{003b}])\\s*(?:regexp|regsub)\\s+)((?:(?:(?:-about|-all|-expanded|-indices|-inline|-lineanchor|-linestop|-line|-nocase|(?:-index\\s+(?:end|\\d+)(?:[\\+\\-]-?\\d+)?))\\s+)*(?:--\\s+)?)?)",
|
"begin": "((?<=^|[\\[\\x{007b}\\x{003b}])\\s*(?:regexp|regsub)\\s+)((?:(?:(?:-about|-all|-expanded|-indices|-inline|-lineanchor|-linestop|-line|-nocase|(?:-index\\s+(?:end|\\d+)(?:[\\+\\-]-?\\d+)?))\\s+)*(?:--\\s+)?)?)",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"1": {
|
"1": { "name": "keyword.tcl" },
|
||||||
"name": "keyword.tcl"
|
"2": { "name": "none.tcl" }
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"name": "none.tcl"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"patterns": [
|
"patterns": [ { "include": "#regexp__1" } ],
|
||||||
{
|
|
||||||
"include": "#regexp__1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": "(\\s+)",
|
"end": "(\\s+)",
|
||||||
"endCaptures": {
|
"endCaptures": { "1": { "name": "none.tcl" } }
|
||||||
"1": {
|
|
||||||
"name": "none.tcl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{ "include": "#args" }
|
||||||
"include": "#args"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"regexp__1": {
|
"regexp__1": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{ "include": "#braced_lit_re" },
|
||||||
"include": "#braced_lit_re"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"begin": "((?:(?<!\\\\)\"))",
|
"begin": "((?:(?<!\\\\)\"))",
|
||||||
"beginCaptures": {
|
"beginCaptures": { "1": { "name": "string.regexp.tcl" } },
|
||||||
"1": {
|
"patterns": [ { "include": "#regexp__2" } ],
|
||||||
"name": "string.regexp.tcl"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"include": "#regexp__2"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": "((?:(?<!\\\\)\"))",
|
"end": "((?:(?<!\\\\)\"))",
|
||||||
"endCaptures": {
|
"endCaptures": { "1": { "name": "string.regexp.tcl" } }
|
||||||
"1": {
|
|
||||||
"name": "string.regexp.tcl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#variable"
|
|
||||||
},
|
},
|
||||||
|
{ "include": "#variable" },
|
||||||
{
|
{
|
||||||
"match": "(\\S+)",
|
"match": "(\\S+)",
|
||||||
"name": "string.regexp.tcl",
|
"name": "string.regexp.tcl",
|
||||||
@@ -378,15 +235,9 @@
|
|||||||
},
|
},
|
||||||
"regexp__2": {
|
"regexp__2": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{ "include": "#braced_lit_re" },
|
||||||
"include": "#braced_lit_re"
|
{ "include": "#embedded" },
|
||||||
},
|
{ "include": "#variable" },
|
||||||
{
|
|
||||||
"include": "#embedded"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#variable"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"match": "(\\\\\"|[^\\x{0022}])",
|
"match": "(\\\\\"|[^\\x{0022}])",
|
||||||
"name": "string.regexp.tcl",
|
"name": "string.regexp.tcl",
|
||||||
@@ -404,15 +255,9 @@
|
|||||||
},
|
},
|
||||||
"word_lit__1": {
|
"word_lit__1": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{ "include": "#braced_lit" },
|
||||||
"include": "#braced_lit"
|
{ "include": "#embedded" },
|
||||||
},
|
{ "include": "#variable" },
|
||||||
{
|
|
||||||
"include": "#embedded"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "#variable"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"match": "(\\\\\"|[^\\x{0022}])",
|
"match": "(\\\\\"|[^\\x{0022}])",
|
||||||
"name": "string.tcl",
|
"name": "string.tcl",
|
||||||
|
|||||||
Reference in New Issue
Block a user