Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9d12555b5 | ||
|
|
fe8d60cf64 | ||
|
|
3679a808c5 | ||
|
|
91fb97a7c0 | ||
|
|
727ea1fa0d | ||
|
|
4a94f1aea8 | ||
|
|
781b240128 | ||
|
|
1a542cc9e9 | ||
|
|
5b2bf39cba | ||
|
|
db8f849a06 | ||
|
|
2ce48ad558 |
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: Build Package Files
|
- name: Build Package Files
|
||||||
run: npm run package
|
run: npm run package
|
||||||
- name: Update package.json Version
|
- name: Update package.json Version
|
||||||
run: npm version ${{ github.ref_name }} --no-git-tag-version
|
run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version
|
||||||
- name: Commit and Push Changes
|
- name: Commit and Push Changes
|
||||||
run: |
|
run: |
|
||||||
git config user.name "${{ vars.USERNAME_GIT }}"
|
git config user.name "${{ vars.USERNAME_GIT }}"
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
# Get current branch; if detached, resolve to a remote branch that contains this commit
|
# Get current branch; if detached, resolve to a remote branch that contains this commit
|
||||||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
if [[ "$BRANCH" == "HEAD" || -z "$BRANCH" ]]; then
|
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
|
# 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 }}'
|
CANDIDATE='${{ github.event.release.target_commitish }}'
|
||||||
if [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]]; then
|
if [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]]; then
|
||||||
BRANCH="$(git branch -r --contains "$GITHUB_SHA" | sed -n 's|.*origin/||p' | head -n1)"
|
BRANCH="$(git branch -r --contains "$GITHUB_SHA" | sed -n 's|.*origin/||p' | head -n1)"
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Install from the VS Code Marketplace
|
1. Install from the VS Code Marketplace
|
||||||
2. Install Python 3.8 or higher
|
2. Open any `.cdl`, `.tcl`, or `.def` file
|
||||||
3. Open any `.cdl`, `.tcl`, or `.def` file
|
3. The extension will automatically activate and provide language support
|
||||||
4. The extension will automatically activate and provide language support
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"version": "0.3.0",
|
"version": "2025.9.100",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"version": "0.3.0",
|
"version": "2025.9.100",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/vscode": "^1.96.0",
|
"@types/vscode": "^1.96.0",
|
||||||
"@vscode/vsce": "^3.2.1",
|
"@vscode/vsce": "^3.2.1",
|
||||||
|
|||||||
+1
-2
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"displayName": "NX Postprocessor Support",
|
"displayName": "NX Postprocessor Support",
|
||||||
"description": "VS Code extension for NX CAM postprocessor development with syntax highlighting, formatting, linting, and auto-completion for CDL, TCL, and DEF files",
|
"description": "",
|
||||||
"version": "2025.9.100",
|
"version": "2025.9.100",
|
||||||
"publisher": "Christoph",
|
"publisher": "Christoph",
|
||||||
"icon": "images/nx-1.png",
|
"icon": "images/nx-1.png",
|
||||||
"extensionDependencies": ["ms-python.python"],
|
|
||||||
"serverInfo": {
|
"serverInfo": {
|
||||||
"name": "NX Postprocessor Support",
|
"name": "NX Postprocessor Support",
|
||||||
"module": "nx-post-support"
|
"module": "nx-post-support"
|
||||||
|
|||||||
+62
-47
@@ -5,33 +5,23 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
|
||||||
import tomllib
|
|
||||||
import urllib.request as url_lib
|
import urllib.request as url_lib
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import nox # pylint: disable=import-error
|
import nox # pylint: disable=import-error
|
||||||
|
|
||||||
|
|
||||||
def _read_dependencies() -> List[str]:
|
|
||||||
"""Read project dependencies from pyproject.toml."""
|
|
||||||
pyproject = tomllib.loads(pathlib.Path("pyproject.toml").read_text())
|
|
||||||
return list(pyproject.get("project", {}).get("dependencies", []))
|
|
||||||
|
|
||||||
|
|
||||||
def _install_bundle(session: nox.Session) -> None:
|
def _install_bundle(session: nox.Session) -> None:
|
||||||
deps = _read_dependencies()
|
session.install(
|
||||||
session.run(
|
"-t",
|
||||||
"uv",
|
|
||||||
"pip",
|
|
||||||
"install",
|
|
||||||
"--target",
|
|
||||||
"./libs",
|
"./libs",
|
||||||
"--no-cache-dir",
|
"--no-cache-dir",
|
||||||
|
"--implementation",
|
||||||
"py",
|
"py",
|
||||||
|
"--no-deps",
|
||||||
"--upgrade",
|
"--upgrade",
|
||||||
*deps,
|
"-r",
|
||||||
external=True,
|
"./requirements.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +34,24 @@ def _check_files(names: List[str]) -> None:
|
|||||||
raise Exception(f"Please update {os.fspath(file_path)}.")
|
raise Exception(f"Please update {os.fspath(file_path)}.")
|
||||||
|
|
||||||
|
|
||||||
|
def _update_pip_packages(session: nox.Session) -> None:
|
||||||
|
session.install("wheel", "pip-tools==7.3.0")
|
||||||
|
session.run(
|
||||||
|
"pip-compile",
|
||||||
|
"--generate-hashes",
|
||||||
|
"--resolver=backtracking",
|
||||||
|
"--upgrade",
|
||||||
|
"./requirements.in",
|
||||||
|
)
|
||||||
|
session.run(
|
||||||
|
"pip-compile",
|
||||||
|
"--generate-hashes",
|
||||||
|
"--resolver=backtracking",
|
||||||
|
"--upgrade",
|
||||||
|
"./src/test/python_tests/requirements.in",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_package_data(package):
|
def _get_package_data(package):
|
||||||
json_uri = f"https://registry.npmjs.org/{package}"
|
json_uri = f"https://registry.npmjs.org/{package}"
|
||||||
with url_lib.urlopen(json_uri) as response:
|
with url_lib.urlopen(json_uri) as response:
|
||||||
@@ -72,8 +80,13 @@ def _update_npm_packages(session: nox.Session) -> None:
|
|||||||
package_json["devDependencies"][package] = latest
|
package_json["devDependencies"][package] = latest
|
||||||
|
|
||||||
# Ensure engine matches the package
|
# Ensure engine matches the package
|
||||||
if package_json["engines"]["vscode"] != package_json["devDependencies"]["@types/vscode"]:
|
if (
|
||||||
print("Please check VS Code engine version and @types/vscode version in package.json.")
|
package_json["engines"]["vscode"]
|
||||||
|
!= package_json["devDependencies"]["@types/vscode"]
|
||||||
|
):
|
||||||
|
print(
|
||||||
|
"Please check VS Code engine version and @types/vscode version in package.json."
|
||||||
|
)
|
||||||
|
|
||||||
new_package_json = json.dumps(package_json, indent=4)
|
new_package_json = json.dumps(package_json, indent=4)
|
||||||
# JSON dumps uses \n for line ending on all platforms by default
|
# JSON dumps uses \n for line ending on all platforms by default
|
||||||
@@ -84,60 +97,65 @@ def _update_npm_packages(session: nox.Session) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _setup_template_environment(session: nox.Session) -> None:
|
def _setup_template_environment(session: nox.Session) -> None:
|
||||||
"""Install project dependencies into the bundled libs directory."""
|
session.install("wheel", "pip-tools")
|
||||||
|
session.run(
|
||||||
|
"pip-compile",
|
||||||
|
"--generate-hashes",
|
||||||
|
"--resolver=backtracking",
|
||||||
|
"--upgrade",
|
||||||
|
"./requirements.in",
|
||||||
|
)
|
||||||
|
# session.run(
|
||||||
|
# "pip-compile",
|
||||||
|
# "--generate-hashes",
|
||||||
|
# "--resolver=backtracking",
|
||||||
|
# "--upgrade",
|
||||||
|
# "./src/test/python_tests/requirements.in",
|
||||||
|
# )
|
||||||
_install_bundle(session)
|
_install_bundle(session)
|
||||||
|
|
||||||
|
|
||||||
@nox.session()
|
@nox.session()
|
||||||
def setup(session: nox.Session) -> None:
|
def setup(session: nox.Session) -> None:
|
||||||
"""Sets up the template for development."""
|
"""Sets up the template for development."""
|
||||||
|
session.install("pip<24")
|
||||||
_setup_template_environment(session)
|
_setup_template_environment(session)
|
||||||
|
|
||||||
|
|
||||||
@nox.session()
|
@nox.session()
|
||||||
def tests(session: nox.Session) -> None:
|
def tests(session: nox.Session) -> None:
|
||||||
"""Runs all the tests for the extension."""
|
"""Runs all the tests for the extension."""
|
||||||
deps = _read_dependencies()
|
session.install("-r", "src/test/python_tests/requirements.txt")
|
||||||
session.run("uv", "pip", "install", "--python", sys.executable, *deps, external=True)
|
session.run("pytest", "src/test/python_tests")
|
||||||
session.run("uv", "pip", "install", "--python", sys.executable, "pytest", external=True)
|
|
||||||
session.run("pytest", "tests/python_tests")
|
|
||||||
|
|
||||||
|
|
||||||
@nox.session()
|
@nox.session()
|
||||||
def lint(session: nox.Session) -> None:
|
def lint(session: nox.Session) -> None:
|
||||||
"""Runs linter and formatter checks on python files."""
|
"""Runs linter and formatter checks on python files."""
|
||||||
deps = _read_dependencies()
|
session.install("-r", "./requirements.txt")
|
||||||
session.run("uv", "pip", "install", "--python", sys.executable, *deps, external=True)
|
session.install("-r", "src/test/python_tests/requirements.txt")
|
||||||
session.run(
|
|
||||||
"uv",
|
session.install("pylint")
|
||||||
"pip",
|
|
||||||
"install",
|
|
||||||
"--python",
|
|
||||||
sys.executable,
|
|
||||||
"pytest",
|
|
||||||
"pylint",
|
|
||||||
"black",
|
|
||||||
"isort",
|
|
||||||
external=True,
|
|
||||||
)
|
|
||||||
session.run("pylint", "-d", "W0511", "./bundled/tool")
|
session.run("pylint", "-d", "W0511", "./bundled/tool")
|
||||||
session.run(
|
session.run(
|
||||||
"pylint",
|
"pylint",
|
||||||
"-d",
|
"-d",
|
||||||
"W0511",
|
"W0511",
|
||||||
"--ignore=./tests/python_tests/test_data",
|
"--ignore=./src/test/python_tests/test_data",
|
||||||
"./tests/python_tests",
|
"./src/test/python_tests",
|
||||||
)
|
)
|
||||||
session.run("pylint", "-d", "W0511", "noxfile.py")
|
session.run("pylint", "-d", "W0511", "noxfile.py")
|
||||||
|
|
||||||
# check formatting using black
|
# check formatting using black
|
||||||
|
session.install("black")
|
||||||
session.run("black", "--check", "./bundled/tool")
|
session.run("black", "--check", "./bundled/tool")
|
||||||
session.run("black", "--check", "./tests/python_tests")
|
session.run("black", "--check", "./src/test/python_tests")
|
||||||
session.run("black", "--check", "noxfile.py")
|
session.run("black", "--check", "noxfile.py")
|
||||||
|
|
||||||
# check import sorting using isort
|
# check import sorting using isort
|
||||||
|
session.install("isort")
|
||||||
session.run("isort", "--check", "./bundled/tool")
|
session.run("isort", "--check", "./bundled/tool")
|
||||||
session.run("isort", "--check", "./tests/python_tests")
|
session.run("isort", "--check", "./src/test/python_tests")
|
||||||
session.run("isort", "--check", "noxfile.py")
|
session.run("isort", "--check", "noxfile.py")
|
||||||
|
|
||||||
# check typescript code
|
# check typescript code
|
||||||
@@ -153,12 +171,9 @@ def build_package(session: nox.Session) -> None:
|
|||||||
session.run("npm", "run", "vsce-package", external=True)
|
session.run("npm", "run", "vsce-package", external=True)
|
||||||
|
|
||||||
|
|
||||||
def _update_uv_lock(session: nox.Session) -> None:
|
|
||||||
session.run("uv", "lock", "--upgrade", external=True)
|
|
||||||
|
|
||||||
|
|
||||||
@nox.session()
|
@nox.session()
|
||||||
def update_packages(session: nox.Session) -> None:
|
def update_packages(session: nox.Session) -> None:
|
||||||
"""Update Python and npm packages."""
|
"""Update pip and npm packages."""
|
||||||
_update_uv_lock(session)
|
session.install("wheel", "pip-tools")
|
||||||
|
_update_pip_packages(session)
|
||||||
_update_npm_packages(session)
|
_update_npm_packages(session)
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "nx-post-support-server"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Python language server for NX Postprocessor Support"
|
|
||||||
requires-python = ">=3.8"
|
|
||||||
dependencies = [
|
|
||||||
"pygls",
|
|
||||||
"packaging",
|
|
||||||
"tclint",
|
|
||||||
]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["setuptools>=61.0"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# This file is used to generate requirements.txt.
|
||||||
|
# NOTE:
|
||||||
|
# Use Python 3.8 or greater which ever is the minimum version of the python
|
||||||
|
# you plan on supporting when creating the environment or using pip-tools.
|
||||||
|
# Only run the commands below to manully upgrade packages in requirements.txt:
|
||||||
|
# 1) python -m pip install pip-tools
|
||||||
|
# 2) pip-compile --generate-hashes --resolver=backtracking --upgrade ./requirements.in
|
||||||
|
# If you are using nox commands to setup or build package you don't need to
|
||||||
|
# run the above commands manually.
|
||||||
|
|
||||||
|
# Required packages
|
||||||
|
pygls
|
||||||
|
packaging
|
||||||
|
|
||||||
|
# TODO: Add your tool here
|
||||||
|
tclint
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.11
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --generate-hashes ./requirements.in
|
||||||
|
#
|
||||||
|
attrs==25.3.0 \
|
||||||
|
--hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \
|
||||||
|
--hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b
|
||||||
|
# via
|
||||||
|
# cattrs
|
||||||
|
# lsprotocol
|
||||||
|
cattrs==25.1.1 \
|
||||||
|
--hash=sha256:1b40b2d3402af7be79a7e7e097a9b4cd16d4c06e6d526644b0b26a063a1cc064 \
|
||||||
|
--hash=sha256:c914b734e0f2d59e5b720d145ee010f1fd9a13ee93900922a2f3f9d593b8382c
|
||||||
|
# via
|
||||||
|
# lsprotocol
|
||||||
|
# pygls
|
||||||
|
importlib-metadata==6.8.0 \
|
||||||
|
--hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
|
||||||
|
--hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
|
||||||
|
# via tclint
|
||||||
|
lsprotocol==2023.0.1 \
|
||||||
|
--hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \
|
||||||
|
--hash=sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d
|
||||||
|
# via pygls
|
||||||
|
packaging==25.0 \
|
||||||
|
--hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
|
||||||
|
--hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
|
||||||
|
# via -r ./requirements.in
|
||||||
|
pathspec==0.11.2 \
|
||||||
|
--hash=sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20 \
|
||||||
|
--hash=sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3
|
||||||
|
# via tclint
|
||||||
|
ply==3.11 \
|
||||||
|
--hash=sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3 \
|
||||||
|
--hash=sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce
|
||||||
|
# via tclint
|
||||||
|
pygls==1.3.1 \
|
||||||
|
--hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \
|
||||||
|
--hash=sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e
|
||||||
|
# via
|
||||||
|
# -r ./requirements.in
|
||||||
|
# tclint
|
||||||
|
tclint==0.6.0 \
|
||||||
|
--hash=sha256:8dd4d7b519e040c164615df8072cc4c28def4bfdc9d2a8672a280b0984b45fc3 \
|
||||||
|
--hash=sha256:f60d2378dd203c0ee1268e9f9138f17cb6106b8824b727e584f054c5932a87db
|
||||||
|
# via -r ./requirements.in
|
||||||
|
typing-extensions==4.14.1 \
|
||||||
|
--hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \
|
||||||
|
--hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76
|
||||||
|
# via cattrs
|
||||||
|
voluptuous==0.15.2 \
|
||||||
|
--hash=sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566 \
|
||||||
|
--hash=sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa
|
||||||
|
# via tclint
|
||||||
|
zipp==3.23.0 \
|
||||||
|
--hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \
|
||||||
|
--hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166
|
||||||
|
# via importlib-metadata
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "MOM_add_to_line_buffer",
|
"label": "MOM_add_to_block_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>+",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
|||||||
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
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
|
from tools.file_sourcing import get_all_psc_files, read_psc_file
|
||||||
|
from tools.signature_help import get_signature_help
|
||||||
from lsp_tclserver import TclLanguageServer
|
from lsp_tclserver import TclLanguageServer
|
||||||
|
|
||||||
|
|
||||||
@@ -235,6 +236,22 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
|
|||||||
return lsp.SemanticTokens(data=data)
|
return lsp.SemanticTokens(data=data)
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(
|
||||||
|
lsp.TEXT_DOCUMENT_SIGNATURE_HELP,
|
||||||
|
lsp.SignatureHelpOptions(trigger_characters=[" ", "\t", "[", ",", "(", "{", '"', "'"], retrigger_characters=list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_:$\"'{}[]() ,\t")),
|
||||||
|
)
|
||||||
|
def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None:
|
||||||
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
tree = LSP_SERVER.get_tree(document)
|
||||||
|
|
||||||
|
# Merge proc signatures across files
|
||||||
|
merged_signatures = {}
|
||||||
|
for sigs in LSP_SERVER.proc_signatures.values():
|
||||||
|
merged_signatures.update(sigs)
|
||||||
|
|
||||||
|
return get_signature_help(document.source, tree, merged_signatures, params.position)
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_HOVER)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_HOVER)
|
||||||
def hover(params: lsp.HoverParams) -> lsp.Hover:
|
def hover(params: lsp.HoverParams) -> lsp.Hover:
|
||||||
pos = params.position
|
pos = params.position
|
||||||
|
|||||||
+127
-6
@@ -1,11 +1,12 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from tclint.syntax_tree import Visitor, BareWord, Command
|
from tclint.syntax_tree import Visitor, BareWord, Command, VarSub, QuotedWord, BracedWord
|
||||||
from tclint.violations import Violation, Rule
|
from tclint.violations import Violation, Rule
|
||||||
|
|
||||||
|
|
||||||
class Rules(Enum):
|
class Rules(Enum):
|
||||||
VALIDATION = "validation"
|
VALIDATION = "validation"
|
||||||
OPTIONAL_ARG_POSITION = "optinal_args"
|
OPTIONAL_ARG_POSITION = "optinal_args"
|
||||||
|
UNDECLARED_VARIABLE = "undeclared-variable"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.value
|
return self.value
|
||||||
@@ -21,10 +22,7 @@ class CommandArgsCheck(Visitor):
|
|||||||
return self._violations
|
return self._violations
|
||||||
|
|
||||||
def visit_command(self, command: Command):
|
def visit_command(self, command: Command):
|
||||||
if (
|
if not hasattr(command.routine, "contents") or command.routine.contents != "proc":
|
||||||
not hasattr(command.routine, "contents")
|
|
||||||
or command.routine.contents != "proc"
|
|
||||||
):
|
|
||||||
return
|
return
|
||||||
if len(command.args) < 2:
|
if len(command.args) < 2:
|
||||||
return
|
return
|
||||||
@@ -51,7 +49,130 @@ class CommandArgsCheck(Visitor):
|
|||||||
found_optional = True
|
found_optional = True
|
||||||
|
|
||||||
|
|
||||||
|
class UndeclaredVariableCheck(Visitor):
|
||||||
|
"""
|
||||||
|
Warn when inside a proc a variable is used ($var) that is neither:
|
||||||
|
- declared global in the proc via `global var`, nor
|
||||||
|
- referenced as namespaced global (e.g. $::var), nor
|
||||||
|
- assigned locally in the proc via `set var ...`, nor
|
||||||
|
- a proc argument.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._violations = []
|
||||||
|
|
||||||
|
def check(self, source, tree):
|
||||||
|
self._violations.clear()
|
||||||
|
# Traverse to find procs; inner scanning is handled per-proc
|
||||||
|
tree.accept(self, recurse=True)
|
||||||
|
return self._violations
|
||||||
|
|
||||||
|
def visit_command(self, command: Command):
|
||||||
|
# Only interested in procs
|
||||||
|
if not hasattr(command.routine, "contents"):
|
||||||
|
return
|
||||||
|
if command.routine.contents != "proc":
|
||||||
|
return
|
||||||
|
# Expect: proc <name> <args> <body>
|
||||||
|
if len(command.args) < 3:
|
||||||
|
return
|
||||||
|
|
||||||
|
proc_name_node = command.args[0]
|
||||||
|
args_node = command.args[1]
|
||||||
|
body_node = command.args[2]
|
||||||
|
|
||||||
|
# Collect proc arg names
|
||||||
|
arg_names = set()
|
||||||
|
if hasattr(args_node, "children"):
|
||||||
|
for child in args_node.children:
|
||||||
|
if isinstance(child, BareWord):
|
||||||
|
if child.contents:
|
||||||
|
arg_names.add(child.contents)
|
||||||
|
elif hasattr(child, "children") and child.children:
|
||||||
|
# Defaulted arg: first element should be the arg name
|
||||||
|
first = child.children[0]
|
||||||
|
if hasattr(first, "contents") and first.contents:
|
||||||
|
arg_names.add(first.contents)
|
||||||
|
|
||||||
|
# Two-pass scan over body: first collect locals (set) and declared globals
|
||||||
|
locals_set = set(arg_names)
|
||||||
|
declared_globals = set()
|
||||||
|
|
||||||
|
def _word_text(node) -> str | None:
|
||||||
|
if isinstance(node, BareWord) or isinstance(node, QuotedWord) or isinstance(node, BracedWord):
|
||||||
|
return node.contents
|
||||||
|
return getattr(node, "contents", None)
|
||||||
|
|
||||||
|
def _collect(node):
|
||||||
|
# Depth-first walk to collect set/global declarations
|
||||||
|
if isinstance(node, Command) and hasattr(node.routine, "contents"):
|
||||||
|
name = node.routine.contents
|
||||||
|
if name == "global":
|
||||||
|
for an in node.args:
|
||||||
|
text = _word_text(an)
|
||||||
|
if text:
|
||||||
|
declared_globals.add(text)
|
||||||
|
elif name == "set" and node.args:
|
||||||
|
# set var [value] - first arg is the variable name
|
||||||
|
first_arg = node.args[0]
|
||||||
|
text = _word_text(first_arg)
|
||||||
|
if text:
|
||||||
|
base = text.split("(", 1)[0] # Handle array syntax
|
||||||
|
if not base.startswith("::"):
|
||||||
|
locals_set.add(base)
|
||||||
|
elif name == "foreach" and len(node.args) >= 3:
|
||||||
|
# foreach varlist1 list1 ?varlist2 list2 ...? body
|
||||||
|
# Treat loop variables as locals within the proc
|
||||||
|
# Iterate pairs (varlist, list) over all but the last arg (body)
|
||||||
|
pair_args = node.args[:-1]
|
||||||
|
i = 0
|
||||||
|
while i + 1 < len(pair_args):
|
||||||
|
varnode = pair_args[i]
|
||||||
|
text = _word_text(varnode) or ""
|
||||||
|
# Split var list by whitespace if braced list, else single name
|
||||||
|
names = text.split()
|
||||||
|
for nm in names:
|
||||||
|
base = nm.split("(", 1)[0]
|
||||||
|
if base and not base.startswith("::"):
|
||||||
|
locals_set.add(base)
|
||||||
|
i += 2
|
||||||
|
# Recurse
|
||||||
|
for ch in getattr(node, "children", []):
|
||||||
|
_collect(ch)
|
||||||
|
|
||||||
|
_collect(body_node)
|
||||||
|
|
||||||
|
# Second pass: flag VarSub usages that are not accounted for
|
||||||
|
def _scan(node):
|
||||||
|
# 1) Variable substitutions like $var
|
||||||
|
if isinstance(node, VarSub):
|
||||||
|
var_name = node.value or ""
|
||||||
|
if var_name.startswith("::"):
|
||||||
|
return
|
||||||
|
base = var_name
|
||||||
|
if base not in locals_set and base not in declared_globals:
|
||||||
|
msg = f"Variable '{base}' used in proc is not set locally and not declared global; use 'global {base}' or '$::{base}'"
|
||||||
|
self._violations.append(Violation(Rules.UNDECLARED_VARIABLE, msg, node.pos, node.end_pos))
|
||||||
|
# 2) Commands that take a variable name as first argument, e.g., 'incr var [amount]'
|
||||||
|
elif isinstance(node, Command) and hasattr(node.routine, "contents"):
|
||||||
|
if node.routine.contents == "incr" and node.args:
|
||||||
|
first_arg = node.args[0]
|
||||||
|
text = _word_text(first_arg) or ""
|
||||||
|
base = text.split("(", 1)[0]
|
||||||
|
if base and not base.startswith("::"):
|
||||||
|
if base not in locals_set and base not in declared_globals:
|
||||||
|
msg = f"Variable '{base}' used in proc is not set locally and not declared global; use 'global {base}'"
|
||||||
|
self._violations.append(Violation(Rules.UNDECLARED_VARIABLE, msg, first_arg.pos, first_arg.end_pos))
|
||||||
|
for ch in getattr(node, "children", []):
|
||||||
|
_scan(ch)
|
||||||
|
|
||||||
|
_scan(body_node)
|
||||||
|
|
||||||
|
|
||||||
def get_checkers():
|
def get_checkers():
|
||||||
checkers = (CommandArgsCheck(),)
|
checkers = (
|
||||||
|
CommandArgsCheck(),
|
||||||
|
UndeclaredVariableCheck(),
|
||||||
|
)
|
||||||
|
|
||||||
return checkers
|
return checkers
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ class _Completion(Visitor):
|
|||||||
def _append_unique(self, item: lsp.CompletionItem):
|
def _append_unique(self, item: lsp.CompletionItem):
|
||||||
# Avoid duplicate labels within the same file scan
|
# Avoid duplicate labels within the same file scan
|
||||||
if not any(ci.label == item.label for ci in self._custom_functions):
|
if not any(ci.label == item.label for ci in self._custom_functions):
|
||||||
|
# For functions, attach a command to trigger signature help after completion
|
||||||
|
if item.kind == lsp.CompletionItemKind.Function and item.command is None:
|
||||||
|
item.command = lsp.Command(title="Trigger Signature Help", command="editor.action.triggerParameterHints")
|
||||||
self._custom_functions.append(item)
|
self._custom_functions.append(item)
|
||||||
|
|
||||||
def visit_command(self, command: Command):
|
def visit_command(self, command: Command):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import enum
|
import enum
|
||||||
from typing import List
|
from typing import List
|
||||||
from tclint.syntax_tree import Visitor, QuotedWord, Command, BareWord
|
from tclint.syntax_tree import Visitor, QuotedWord, Command, BareWord, VarSub
|
||||||
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
|
||||||
@@ -14,6 +14,9 @@ class TokenModifier(enum.IntFlag):
|
|||||||
definition = enum.auto()
|
definition = enum.auto()
|
||||||
declaration = enum.auto()
|
declaration = enum.auto()
|
||||||
builtin = enum.auto()
|
builtin = enum.auto()
|
||||||
|
globalvar = enum.auto()
|
||||||
|
reference = enum.auto()
|
||||||
|
write = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
@attrs.define
|
@attrs.define
|
||||||
@@ -101,6 +104,10 @@ class _Highlighter(Visitor):
|
|||||||
if in_custom or in_standard:
|
if in_custom or in_standard:
|
||||||
line, col = routine.contents_pos
|
line, col = routine.contents_pos
|
||||||
self._tokens.append((((line - 1, col - 1), len(name), "function", [])))
|
self._tokens.append((((line - 1, col - 1), len(name), "function", [])))
|
||||||
|
# Highlight 'global' itself as a keyword
|
||||||
|
if name == "global":
|
||||||
|
line, col = routine.contents_pos
|
||||||
|
self._tokens.append((((line - 1, col - 1), len(name), "keyword", [])))
|
||||||
|
|
||||||
if routine.contents == "puts":
|
if routine.contents == "puts":
|
||||||
line, col = routine.contents_pos
|
line, col = routine.contents_pos
|
||||||
@@ -114,21 +121,34 @@ class _Highlighter(Visitor):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# Variable declaration vs reference for 'set'
|
||||||
if routine.contents == "set" and command.args:
|
if routine.contents == "set" and command.args:
|
||||||
first_arg = command.args[0]
|
first_arg = command.args[0]
|
||||||
token_info = self._get_token_info(first_arg)
|
token_info = self._get_token_info(first_arg)
|
||||||
if token_info:
|
if token_info:
|
||||||
(line, col), length = token_info
|
(line, col), length = token_info
|
||||||
self._tokens.append(
|
mods = [TokenModifier.declaration, TokenModifier.write]
|
||||||
(
|
self._tokens.append(((line, col), length, "variable", mods))
|
||||||
(
|
# If there is exactly one argument, 'set var' is a read/reference
|
||||||
(line, col),
|
if len(command.args) == 1 and token_info:
|
||||||
length,
|
(line, col), length = token_info
|
||||||
"variable",
|
mods = [TokenModifier.reference]
|
||||||
[TokenModifier.declaration],
|
self._tokens.append(((line, col), length, "variable", mods))
|
||||||
)
|
# Variable modification via 'incr var [amount]'
|
||||||
)
|
if routine.contents == "incr" and command.args:
|
||||||
)
|
first_arg = command.args[0]
|
||||||
|
token_info = self._get_token_info(first_arg)
|
||||||
|
if token_info:
|
||||||
|
(line, col), length = token_info
|
||||||
|
mods = [TokenModifier.write]
|
||||||
|
# Add global flag if explicitly namespaced
|
||||||
|
try:
|
||||||
|
name_text = getattr(first_arg, "value", None) or getattr(first_arg, "contents", None) or ""
|
||||||
|
if isinstance(name_text, str) and name_text.startswith("::"):
|
||||||
|
mods.append(TokenModifier.globalvar)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._tokens.append(((line, col), length, "variable", mods))
|
||||||
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, "pos") and hasattr(first_arg, "value"):
|
if hasattr(first_arg, "pos") and hasattr(first_arg, "value"):
|
||||||
@@ -153,11 +173,12 @@ class _Highlighter(Visitor):
|
|||||||
# Parameter kann einfaches Wort sein
|
# Parameter kann einfaches Wort sein
|
||||||
if hasattr(child, "value") and child.value is not None:
|
if hasattr(child, "value") and child.value is not None:
|
||||||
line, col = child.pos
|
line, col = child.pos
|
||||||
|
# Parameters are variables (locals): mark as variable with declaration
|
||||||
self._tokens.append(
|
self._tokens.append(
|
||||||
(
|
(
|
||||||
(line - 1, col - 1),
|
(line - 1, col - 1),
|
||||||
len(child.value),
|
len(child.value),
|
||||||
"parameter",
|
"variable",
|
||||||
[TokenModifier.declaration],
|
[TokenModifier.declaration],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -171,7 +192,7 @@ class _Highlighter(Visitor):
|
|||||||
(
|
(
|
||||||
(line - 1, col - 1),
|
(line - 1, col - 1),
|
||||||
len(name_node.value),
|
len(name_node.value),
|
||||||
"parameter",
|
"variable",
|
||||||
[TokenModifier.declaration],
|
[TokenModifier.declaration],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -181,6 +202,17 @@ class _Highlighter(Visitor):
|
|||||||
line, col = first_arg.pos
|
line, col = first_arg.pos
|
||||||
self._tokens.append((((line - 1, col - 1), len(first_arg.value), "class", [])))
|
self._tokens.append((((line - 1, col - 1), len(first_arg.value), "class", [])))
|
||||||
|
|
||||||
|
# Highlight variables used via $var and $::var as references (read)
|
||||||
|
for arg in getattr(command, "args", []):
|
||||||
|
for child in getattr(arg, "children", []):
|
||||||
|
if isinstance(child, VarSub) and hasattr(child, "pos") and child.value:
|
||||||
|
line, col = child.pos
|
||||||
|
# Determine if this looks like a global reference ($::var)
|
||||||
|
mods = [TokenModifier.reference]
|
||||||
|
if child.value.startswith("::"):
|
||||||
|
mods.append(TokenModifier.globalvar)
|
||||||
|
self._tokens.append((((line - 1, col - 1), len(child.value), "variable", mods)))
|
||||||
|
|
||||||
def tokens(self) -> list[Token]:
|
def tokens(self) -> list[Token]:
|
||||||
"""Encode tokens as described in
|
"""Encode tokens as described in
|
||||||
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens.
|
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens.
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
import lsprotocol.types as lsp
|
||||||
|
from tclint.syntax_tree import Visitor, Command, Node
|
||||||
|
|
||||||
|
|
||||||
|
class _CommandAtPositionFinder(Visitor):
|
||||||
|
def __init__(self, line1: int, col1: int, lines: list[str]):
|
||||||
|
# Cursor position in 1-based line/col to match AST
|
||||||
|
self.line1 = line1
|
||||||
|
self.col1 = col1
|
||||||
|
self.lines = lines
|
||||||
|
self.match: Optional[Command] = None
|
||||||
|
|
||||||
|
def _pos_le(self, a: Tuple[int, int] | None, b: Tuple[int, int] | None) -> bool:
|
||||||
|
if a is None or b is None:
|
||||||
|
return False
|
||||||
|
return a[0] < b[0] or (a[0] == b[0] and a[1] <= b[1])
|
||||||
|
|
||||||
|
def _pos_ge(self, a: Tuple[int, int] | None, b: Tuple[int, int] | None) -> bool:
|
||||||
|
if a is None or b is None:
|
||||||
|
return False
|
||||||
|
return a[0] > b[0] or (a[0] == b[0] and a[1] >= b[1])
|
||||||
|
|
||||||
|
def _contains_or_trailing(self, node: Node) -> bool:
|
||||||
|
# Standard containment
|
||||||
|
if self._pos_le(node.pos, (self.line1, self.col1)) and self._pos_ge(node.end_pos, (self.line1, self.col1)):
|
||||||
|
return True
|
||||||
|
# Extend containment to trailing whitespace on same line as the command end
|
||||||
|
if getattr(node, "end_pos", None) and node.end_pos and node.end_pos[0] == self.line1 and self.col1 >= node.end_pos[1]:
|
||||||
|
try:
|
||||||
|
line = self.lines[self.line1 - 1]
|
||||||
|
except Exception:
|
||||||
|
line = ""
|
||||||
|
start = max(0, node.end_pos[1] - 1)
|
||||||
|
end = max(0, min(len(line), self.col1 - 1))
|
||||||
|
segment = line[start:end]
|
||||||
|
# If there is a command terminator (';' or ']') between end_pos and cursor, do not extend
|
||||||
|
if ";" not in segment and "]" not in segment:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def visit_command(self, command: Command):
|
||||||
|
if hasattr(command, "pos") and hasattr(command, "end_pos") and self._contains_or_trailing(command):
|
||||||
|
# Prefer the deepest command: overwrite and continue
|
||||||
|
self.match = command
|
||||||
|
# Continue traversal
|
||||||
|
for ch in getattr(command, "children", []):
|
||||||
|
ch.accept(self, recurse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_active_parameter(cmd: Command, line1: int, col1: int) -> int:
|
||||||
|
# Count which arg contains the cursor; else number of args before cursor
|
||||||
|
for idx, arg in enumerate(getattr(cmd, "args", []) or []):
|
||||||
|
if getattr(arg, "pos", None) and getattr(arg, "end_pos", None):
|
||||||
|
if (arg.pos[0] < line1 or (arg.pos[0] == line1 and arg.pos[1] <= col1)) and (arg.end_pos[0] > line1 or (arg.end_pos[0] == line1 and arg.end_pos[1] >= col1)):
|
||||||
|
return idx
|
||||||
|
# Not inside any arg; compute based on separator position
|
||||||
|
count = 0
|
||||||
|
for arg in getattr(cmd, "args", []) or []:
|
||||||
|
if getattr(arg, "end_pos", None):
|
||||||
|
if arg.end_pos[0] < line1 or (arg.end_pos[0] == line1 and arg.end_pos[1] <= col1):
|
||||||
|
count += 1
|
||||||
|
return min(count, max(0, len(getattr(cmd, "args", [])) - 1))
|
||||||
|
|
||||||
|
|
||||||
|
def get_signature_help(
|
||||||
|
document_text: str,
|
||||||
|
tree,
|
||||||
|
merged_signatures: Dict[str, List[str]],
|
||||||
|
position: lsp.Position,
|
||||||
|
) -> Optional[lsp.SignatureHelp]:
|
||||||
|
# Convert to 1-based coordinates expected by AST
|
||||||
|
line1 = position.line + 1
|
||||||
|
col1 = position.character + 1
|
||||||
|
|
||||||
|
# Find the innermost command that contains the position
|
||||||
|
# Pass lines for trailing-space containment handling
|
||||||
|
lines = document_text.splitlines()
|
||||||
|
finder = _CommandAtPositionFinder(line1, col1, lines)
|
||||||
|
tree.accept(finder, recurse=True)
|
||||||
|
cmd = finder.match
|
||||||
|
if not cmd or not hasattr(cmd, "routine") or not hasattr(cmd.routine, "contents"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
routine_name = cmd.routine.contents
|
||||||
|
if not routine_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
params = merged_signatures.get(routine_name)
|
||||||
|
if not params:
|
||||||
|
# No known signature for this routine
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Build a signature info
|
||||||
|
label = f"{routine_name}({', '.join(params)})"
|
||||||
|
parameters = [lsp.ParameterInformation(label=p) for p in params]
|
||||||
|
|
||||||
|
active_param = _compute_active_parameter(cmd, line1, col1)
|
||||||
|
active_param = max(0, min(active_param, len(params) - 1))
|
||||||
|
|
||||||
|
sig = lsp.SignatureInformation(label=label, parameters=parameters)
|
||||||
|
return lsp.SignatureHelp(signatures=[sig], active_signature=0, active_parameter=active_param)
|
||||||
Reference in New Issue
Block a user