Compare commits
110
Commits
0.4.10
...
rust_parser
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a61161992 | ||
|
|
a8bf55882e | ||
|
|
377bddf579 | ||
|
|
cdc1cb4805 | ||
|
|
34430097d8 | ||
|
|
dcf447ed4f | ||
|
|
783a9aa7d7 | ||
|
|
63638d6b3b | ||
|
|
af8cd58fda | ||
|
|
df0f3d7a61 | ||
|
|
72906a1930 | ||
|
|
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 | ||
|
|
7c834176a3 | ||
|
|
72be83c282 | ||
|
|
fa5ea00384 | ||
|
|
a8b4fd9d1e | ||
|
|
35d0f20fba | ||
|
|
440ebc4df5 | ||
|
|
1b48cc1983 | ||
|
|
6cde05b786 | ||
|
|
cde58db3d7 | ||
|
|
3613391c23 | ||
|
|
894a5b8b52 | ||
|
|
c644ac9bd3 | ||
|
|
fd5b1f2e71 | ||
|
|
57c65221d3 | ||
|
|
528c7a6b9e | ||
|
|
2d034838b4 | ||
|
|
f0ed8ac708 | ||
|
|
6a30c1ab53 | ||
|
|
6d404f26f2 | ||
|
|
afc675f47f | ||
|
|
d2936ec5f6 | ||
|
|
8b44b41d68 | ||
|
|
5124c12bc6 | ||
|
|
32061809a7 | ||
|
|
f762c42e00 | ||
|
|
4ccdf9e9f3 | ||
|
|
b86d4f7b58 | ||
|
|
0318532308 | ||
|
|
5e2e85830b | ||
|
|
98e2d104a8 | ||
|
|
44fd772151 | ||
|
|
d90f9b5425 | ||
|
|
fed8b84b46 | ||
|
|
ed045aa459 | ||
|
|
8da61abfce | ||
|
|
0ea2cbec87 | ||
|
|
d99d7c6bf6 |
@@ -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,41 @@ 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 }}
|
||||||
|
- name: Pack Extension for Release
|
||||||
|
run: vsce pack
|
||||||
|
- name: Upload VSIX to Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: "nx-post-support-*.vsix"
|
||||||
|
# generate_release_notes: true # Automatische Release Notes
|
||||||
|
# draft: false # N
|
||||||
|
# prerelease: false
|
||||||
|
|||||||
Vendored
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
"ruff.configuration": {
|
"ruff.configuration": {
|
||||||
"lint": {
|
"lint": {
|
||||||
"extend-ignore": ["F821", "E402"]
|
"extend-ignore": ["F821", "E402"]
|
||||||
}
|
},
|
||||||
|
"line-length": 200
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -11,7 +11,11 @@ esbuild.js
|
|||||||
.gitea
|
.gitea
|
||||||
.venv/**
|
.venv/**
|
||||||
.nox/**
|
.nox/**
|
||||||
|
server/.venv/**
|
||||||
|
server/.nox/**
|
||||||
**/__pycache__/**
|
**/__pycache__/**
|
||||||
**/requirements.txt
|
**/requirements.txt
|
||||||
**/requirements.in
|
**/requirements.in
|
||||||
**/server/src/_debug_server.py
|
**/server/src/_debug_server.py
|
||||||
|
server/noxfile.py
|
||||||
|
client/**
|
||||||
@@ -1,21 +1,661 @@
|
|||||||
MIT License
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (c) 2025 Christoph
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Preamble
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
copies or substantial portions of the Software.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
The licenses for most software and other practical works are designed
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
share and change all versions of a program--to make sure it remains free
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
software for all its users.
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
nx-post-support
|
||||||
|
Copyright (C) 2025 Christoph
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
AGPL-3.0 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 |
+12
-3
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"displayName": "NX Postprocessor Support",
|
"displayName": "NX Postprocessor Support",
|
||||||
"description": "",
|
"description": "VS Code extension for NX CAM postprocessor development with syntax highlighting, formatting, linting, and auto-completion for CDL, TCL, and DEF files",
|
||||||
"version": "0.4.9",
|
"version": "2025.9.100",
|
||||||
"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.",
|
||||||
|
|||||||
+47
-62
@@ -5,23 +5,33 @@
|
|||||||
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:
|
||||||
session.install(
|
deps = _read_dependencies()
|
||||||
"-t",
|
session.run(
|
||||||
|
"uv",
|
||||||
|
"pip",
|
||||||
|
"install",
|
||||||
|
"--target",
|
||||||
"./libs",
|
"./libs",
|
||||||
"--no-cache-dir",
|
"--no-cache-dir",
|
||||||
"--implementation",
|
|
||||||
"py",
|
"py",
|
||||||
"--no-deps",
|
|
||||||
"--upgrade",
|
"--upgrade",
|
||||||
"-r",
|
*deps,
|
||||||
"./requirements.txt",
|
external=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -34,24 +44,6 @@ 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:
|
||||||
@@ -80,13 +72,8 @@ 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 (
|
if package_json["engines"]["vscode"] != package_json["devDependencies"]["@types/vscode"]:
|
||||||
package_json["engines"]["vscode"]
|
print("Please check VS Code engine version and @types/vscode version in package.json.")
|
||||||
!= 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
|
||||||
@@ -97,65 +84,60 @@ def _update_npm_packages(session: nox.Session) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _setup_template_environment(session: nox.Session) -> None:
|
def _setup_template_environment(session: nox.Session) -> None:
|
||||||
session.install("wheel", "pip-tools")
|
"""Install project dependencies into the bundled libs directory."""
|
||||||
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."""
|
||||||
session.install("-r", "src/test/python_tests/requirements.txt")
|
deps = _read_dependencies()
|
||||||
session.run("pytest", "src/test/python_tests")
|
session.run("uv", "pip", "install", "--python", sys.executable, *deps, external=True)
|
||||||
|
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."""
|
||||||
session.install("-r", "./requirements.txt")
|
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(
|
||||||
session.install("pylint")
|
"uv",
|
||||||
|
"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=./src/test/python_tests/test_data",
|
"--ignore=./tests/python_tests/test_data",
|
||||||
"./src/test/python_tests",
|
"./tests/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", "./src/test/python_tests")
|
session.run("black", "--check", "./tests/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", "./src/test/python_tests")
|
session.run("isort", "--check", "./tests/python_tests")
|
||||||
session.run("isort", "--check", "noxfile.py")
|
session.run("isort", "--check", "noxfile.py")
|
||||||
|
|
||||||
# check typescript code
|
# check typescript code
|
||||||
@@ -171,9 +153,12 @@ 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 pip and npm packages."""
|
"""Update Python and npm packages."""
|
||||||
session.install("wheel", "pip-tools")
|
_update_uv_lock(session)
|
||||||
_update_pip_packages(session)
|
|
||||||
_update_npm_packages(session)
|
_update_npm_packages(session)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[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"
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
#
|
|
||||||
# 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
|
|
||||||
@@ -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>+",
|
||||||
@@ -514,6 +518,31 @@
|
|||||||
"set syslog [MOM_ask_syslog_name]"
|
"set syslog [MOM_ask_syslog_name]"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "MOM_ask_ude_info",
|
||||||
|
"kind": "function",
|
||||||
|
"description": "This command is used to retrieve the information about a user-defined event (UDE) of a specified object.",
|
||||||
|
"format": "MOM_ask_ude_info object_name object_type <Start/End/\"\">",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Object_name",
|
||||||
|
"desc": "The name of the object to which the UDE is attached. It can be a group, an operation, a tool, a geometry, or a method."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "object_type",
|
||||||
|
"desc": "The type of the object to which the UDE is attached. The following types are available.(group || operation/oper || tool || geometry/geom || method/meth)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Start/End/\"\"",
|
||||||
|
"desc": "This parameter is optional. Indicates whether you want to retrieve the UDE attached to the start event, end event, or both. If it is empty, returns the UDEs attached to the specified operation."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": [
|
||||||
|
"0 - The UDE information retrieval failed.",
|
||||||
|
"1 - The UDE information successfully retrieved."
|
||||||
|
],
|
||||||
|
"example": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "MOM_cancel_suppress_force_once_per_event",
|
"label": "MOM_cancel_suppress_force_once_per_event",
|
||||||
"kind": "function",
|
"kind": "function",
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def format_tcl(src: str, indent_str=" ") -> str:
|
|
||||||
"""
|
|
||||||
Simple Tcl formatter with special handling for:
|
|
||||||
1) Single-line 'if {cond} {action}' blocks remain on one line.
|
|
||||||
2) Combined closing-and-opening lines like '} else {' dedent then re-indent.
|
|
||||||
3) Lines like '} Tag' stay on the same line: '} Tag'.
|
|
||||||
4) Standard multi-line blocks for 'if', 'elseif', 'else', '{', '}'.
|
|
||||||
"""
|
|
||||||
level = 0
|
|
||||||
out_lines = []
|
|
||||||
|
|
||||||
for raw_line in src.splitlines():
|
|
||||||
stripped = raw_line.strip()
|
|
||||||
|
|
||||||
# Comments are ignored
|
|
||||||
if stripped.startswith("#"):
|
|
||||||
out_lines.append(indent_str * level + stripped)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Single-line 'if {cond} {action}' → no indent change
|
|
||||||
if re.match(r"^(if|elseif)\s*\{[^}]+\}\s*\{[^}]+\}$", stripped):
|
|
||||||
out_lines.append(indent_str * level + stripped)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Combined '} else {' → dedent, print, then indent
|
|
||||||
if re.match(r"^\}\s*(elseif|else)\b.*\{$", stripped):
|
|
||||||
level = max(level - 1, 0)
|
|
||||||
out_lines.append(indent_str * level + stripped)
|
|
||||||
level += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# SPECIAL: closing brace plus tag on same line: '} Tag'
|
|
||||||
m = re.match(r"^\}\s+(.+)", stripped)
|
|
||||||
if m and not stripped.startswith("#"):
|
|
||||||
# close one block
|
|
||||||
level = max(level - 1, 0)
|
|
||||||
# stay on one line: "} Tag"
|
|
||||||
out_lines.append(f"{indent_str * level}}} {m.group(1)}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Pure '}' → dedent then print
|
|
||||||
if stripped == "}":
|
|
||||||
level = max(level - 1, 0)
|
|
||||||
out_lines.append(f"{indent_str * level}{stripped}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 'elseif' or 'else' alone → align with matching 'if'
|
|
||||||
if re.match(r"^(elseif|else)\b(?!.*\{)", stripped):
|
|
||||||
level = max(level - 1, 0)
|
|
||||||
out_lines.append(f"{indent_str * level}{stripped}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Default: print at current indent
|
|
||||||
out_lines.append(f"{indent_str * level}{stripped}")
|
|
||||||
|
|
||||||
# Open a new block on lines ending with '{'
|
|
||||||
if re.match(r"^(if|elseif)\b.*\{$", stripped) or stripped.endswith("{"):
|
|
||||||
level += 1
|
|
||||||
|
|
||||||
return "\n".join(out_lines)
|
|
||||||
@@ -21,6 +21,7 @@ class StandardCompletionItems:
|
|||||||
self.__tcl_keyword_list = self.__load_tcl_keyword()
|
self.__tcl_keyword_list = self.__load_tcl_keyword()
|
||||||
self.__nx_procs = self.__load_nx_procs()
|
self.__nx_procs = self.__load_nx_procs()
|
||||||
self.__nx_variables = self.__load_nx_variables()
|
self.__nx_variables = self.__load_nx_variables()
|
||||||
|
self.__custom_functions = list[lsp.CompletionItem]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json_data(self):
|
def json_data(self):
|
||||||
@@ -38,6 +39,14 @@ class StandardCompletionItems:
|
|||||||
def nx_variables(self):
|
def nx_variables(self):
|
||||||
return self.__nx_variables
|
return self.__nx_variables
|
||||||
|
|
||||||
|
@property
|
||||||
|
def custom_functions(self) -> list[lsp.CompletionItem]:
|
||||||
|
return self.__custom_functions
|
||||||
|
|
||||||
|
@custom_functions.setter
|
||||||
|
def custom_functions(self, value: lsp.CompletionItem):
|
||||||
|
self.__custom_functions.append(value)
|
||||||
|
|
||||||
def __load_json(self) -> dict:
|
def __load_json(self) -> dict:
|
||||||
with open(
|
with open(
|
||||||
pathlib.Path(__file__).parent.joinpath("completion_list.json"), "r"
|
pathlib.Path(__file__).parent.joinpath("completion_list.json"), "r"
|
||||||
|
|||||||
+276
-204
@@ -4,16 +4,16 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import threading
|
||||||
import traceback
|
|
||||||
from typing import Any, List, Optional, Sequence, Tuple
|
from typing import Any, Optional
|
||||||
import re
|
import operator
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
@@ -39,159 +39,27 @@ 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 lsp_utils as utils
|
|
||||||
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 common.formatter import format_tcl
|
from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
||||||
from tclint.parser import Parser
|
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
||||||
from tclint.lexer import TclSyntaxError
|
from tools.inlay_hint import InlayHintGenerator
|
||||||
from tclint.format import Formatter, FormatterOpts
|
from tools.file_sourcing import get_all_psc_files, read_psc_file
|
||||||
from tclint.violations import Violation
|
from lsp_tclserver import TclLanguageServer
|
||||||
from tools.semantic_tokens import (
|
|
||||||
SemanticTokenCollector,
|
|
||||||
collect_semantic_tokens,
|
|
||||||
encode_tokens,
|
|
||||||
)
|
|
||||||
from plugins.poco_plugin import commands
|
|
||||||
from tools import checks
|
|
||||||
|
|
||||||
DIAGNOSTIC_SOURCE = "nx-post-support"
|
|
||||||
|
|
||||||
|
|
||||||
class TclLanguageServer(server.LanguageServer):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.parser = Parser()
|
|
||||||
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
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
TOKEN_TYPES = [
|
|
||||||
"command",
|
|
||||||
"variable",
|
|
||||||
"function",
|
|
||||||
"string",
|
|
||||||
"number",
|
|
||||||
"keyword",
|
|
||||||
"comment",
|
|
||||||
]
|
|
||||||
TOKEN_MODIFIERS = []
|
|
||||||
|
|
||||||
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.
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
@@ -207,16 +75,18 @@ 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)
|
||||||
|
# Also update custom completion and proc docs for this file
|
||||||
|
LSP_SERVER.update_poco_completion_for_file(document)
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
|
||||||
def did_save(params: lsp.DidSaveTextDocumentParams) -> None:
|
def did_save(params: lsp.DidSaveTextDocumentParams) -> None:
|
||||||
"""LSP handler for textDocument/didSave request."""
|
"""LSP handler for textDocument/didSave request."""
|
||||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
_ = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
|
||||||
|
|
||||||
@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."""
|
||||||
|
|
||||||
|
|
||||||
@@ -225,6 +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)
|
||||||
|
LSP_SERVER.update_poco_completion_for_file(document)
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(
|
@LSP_SERVER.feature(
|
||||||
@@ -253,23 +124,114 @@ 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:
|
||||||
items = (
|
from tools.variable_index import build_variable_index
|
||||||
standard_items.tcl_keyword_list
|
from tools.completion_items import BUILTIN_VAR_LABELS
|
||||||
+ standard_items.nx_procs
|
|
||||||
+ standard_items.nx_variables
|
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
)
|
|
||||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
# Base items
|
||||||
|
poco = [item for items in LSP_SERVER.poco_completion.values() for item in items]
|
||||||
|
base_items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco
|
||||||
|
|
||||||
|
# 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_SEMANTIC_TOKENS_FULL)
|
# @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
||||||
def on_semantic_tokens(params: lsp.SemanticTokensParams):
|
# def document_symbols(params: lsp.DocumentSymbolParams):
|
||||||
doc = LSP_SERVER.workspace.get_document(params.text_document.uri)
|
# doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
code = doc.source
|
# ast = LSP_SERVER.parser.parse(doc.source)
|
||||||
|
# symbols = LSP_SERVER.extract_tcl_symbols(ast)
|
||||||
|
|
||||||
tokens = collect_semantic_tokens(code)
|
# return symbols
|
||||||
data = encode_tokens(tokens)
|
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Reuse cached AST
|
||||||
|
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)
|
||||||
|
return generator.hints
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(
|
||||||
|
lsp.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
|
||||||
|
lsp.SemanticTokensLegend(
|
||||||
|
token_types=TOKEN_TYPES,
|
||||||
|
token_modifiers=[m.name for m in TokenModifier],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def semantic_tokens(params: lsp.SemanticTokensParams):
|
||||||
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
plugins = []
|
||||||
|
hl = _Highlighter(plugins, LSP_SERVER.poco_completion)
|
||||||
|
|
||||||
|
# Reuse cached AST
|
||||||
|
tree = LSP_SERVER.get_tree(document)
|
||||||
|
tree.accept(hl, recurse=True)
|
||||||
|
|
||||||
|
tokens = hl.tokens()
|
||||||
|
for token in tokens:
|
||||||
|
data.extend(
|
||||||
|
[
|
||||||
|
token.line,
|
||||||
|
token.offset,
|
||||||
|
token.length,
|
||||||
|
TOKEN_TYPES.index(token.tok_type),
|
||||||
|
reduce(operator.or_, token.tok_modifiers, 0),
|
||||||
|
]
|
||||||
|
)
|
||||||
return lsp.SemanticTokens(data=data)
|
return lsp.SemanticTokens(data=data)
|
||||||
|
|
||||||
|
|
||||||
@@ -285,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)
|
||||||
@@ -292,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
|
||||||
|
|
||||||
|
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
@@ -363,12 +402,12 @@ def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | Non
|
|||||||
start = lsp.Position(line=0, character=0)
|
start = lsp.Position(line=0, character=0)
|
||||||
last_line = source.rsplit("\n", 1)[-1]
|
last_line = source.rsplit("\n", 1)[-1]
|
||||||
end = lsp.Position(line=source.count("\n"), character=len(last_line))
|
end = lsp.Position(line=source.count("\n"), character=len(last_line))
|
||||||
|
if GLOBAL_SETTINGS.get("formatter", True):
|
||||||
formatted = LSP_SERVER.format(doc, params.options)
|
source = LSP_SERVER.format(doc, params.options)
|
||||||
return [
|
return [
|
||||||
lsp.TextEdit(
|
lsp.TextEdit(
|
||||||
range=lsp.Range(start=start, end=end),
|
range=lsp.Range(start=start, end=end),
|
||||||
new_text=formatted,
|
new_text=source,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -377,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"""
|
||||||
|
|
||||||
|
|
||||||
@@ -393,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=TOKEN_MODIFIERS,
|
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."""
|
||||||
@@ -433,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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -504,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)
|
||||||
@@ -36,6 +36,12 @@ def lib_ge_command_buffer_edit_prepend(args, parser):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def lib_ge_command_buffer_edit_replace(args, parser):
|
||||||
|
_lib_ge_command_buffer_edit(
|
||||||
|
args, parser, "LIB_GE_command_buffer_edit_replace", 3, 5
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def lib_ge_command_buffer_edit_insert(args, parser):
|
def lib_ge_command_buffer_edit_insert(args, parser):
|
||||||
_lib_ge_command_buffer_edit(args, parser, "LIB_GE_command_buffer_edit_insert", 2, 6)
|
_lib_ge_command_buffer_edit(args, parser, "LIB_GE_command_buffer_edit_insert", 2, 6)
|
||||||
|
|
||||||
@@ -44,5 +50,6 @@ commands = [
|
|||||||
{"LIB_GE_command_buffer_edit_append": lib_ge_command_buffer_edit_append},
|
{"LIB_GE_command_buffer_edit_append": lib_ge_command_buffer_edit_append},
|
||||||
{"LIB_GE_command_buffer_edit_prepend": lib_ge_command_buffer_edit_prepend},
|
{"LIB_GE_command_buffer_edit_prepend": lib_ge_command_buffer_edit_prepend},
|
||||||
{"LIB_GE_command_buffer_edit_insert": lib_ge_command_buffer_edit_insert},
|
{"LIB_GE_command_buffer_edit_insert": lib_ge_command_buffer_edit_insert},
|
||||||
|
{"LIB_GE_command_buffer_edit_replace": lib_ge_command_buffer_edit_replace},
|
||||||
{"LIB_GE_command_buffer": _lib_ge_command_buffer},
|
{"LIB_GE_command_buffer": _lib_ge_command_buffer},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
from tclint.parser import Parser as tcLintParser
|
|
||||||
from tclint.lexer import Lexer as tclingLexer
|
|
||||||
from parser.lexer import Lexer
|
|
||||||
from parser.parser import Parser
|
|
||||||
|
|
||||||
|
|
||||||
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("""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("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():
|
|
||||||
code = """
|
|
||||||
proc myProc {arg } {
|
|
||||||
set myVar 1
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace eval myNS {
|
|
||||||
proc innerProc {} {
|
|
||||||
MOM_abort_program "Test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set result [myNS::innerProc]
|
|
||||||
"""
|
|
||||||
|
|
||||||
lexer = Lexer()
|
|
||||||
lexer.input(code)
|
|
||||||
parser = Parser(lexer)
|
|
||||||
ast = parser.parse()
|
|
||||||
|
|
||||||
visitor = NodeVisitor()
|
|
||||||
ast.accept(visitor)
|
|
||||||
|
|
||||||
|
|
||||||
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__":
|
|
||||||
main()
|
|
||||||
@@ -1,4 +1,57 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from tclint.syntax_tree import Visitor, BareWord, Command
|
||||||
|
from tclint.violations import Violation, Rule
|
||||||
|
|
||||||
|
|
||||||
|
class Rules(Enum):
|
||||||
|
VALIDATION = "validation"
|
||||||
|
OPTIONAL_ARG_POSITION = "optinal_args"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class CommandArgsCheck(Visitor):
|
||||||
|
def __init__(self):
|
||||||
|
self._violations = []
|
||||||
|
|
||||||
|
def check(self, _, tree):
|
||||||
|
self._violations.clear()
|
||||||
|
tree.accept(self, recurse=True)
|
||||||
|
return self._violations
|
||||||
|
|
||||||
|
def visit_command(self, command: Command):
|
||||||
|
if (
|
||||||
|
not hasattr(command.routine, "contents")
|
||||||
|
or command.routine.contents != "proc"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
if len(command.args) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
args_node = command.args[1]
|
||||||
|
if not hasattr(args_node, "children"):
|
||||||
|
return
|
||||||
|
|
||||||
|
found_optional = False
|
||||||
|
for arg in args_node.children:
|
||||||
|
# Required argument
|
||||||
|
if isinstance(arg, BareWord):
|
||||||
|
if found_optional:
|
||||||
|
self._violations.append(
|
||||||
|
Violation(
|
||||||
|
Rules.OPTIONAL_ARG_POSITION,
|
||||||
|
"Required argument follows optional one",
|
||||||
|
arg.pos,
|
||||||
|
arg.end_pos,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Optional argument
|
||||||
|
elif hasattr(arg, "children") and len(arg.children) >= 2:
|
||||||
|
found_optional = True
|
||||||
|
|
||||||
|
|
||||||
def get_checkers():
|
def get_checkers():
|
||||||
checkers = ()
|
checkers = (CommandArgsCheck(),)
|
||||||
|
|
||||||
return checkers
|
return checkers
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
from tclint.syntax_tree import Visitor, Command, BareWord, List
|
||||||
|
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:
|
||||||
|
def __init__(self):
|
||||||
|
self._custom_functions: list[lsp.CompletionItem] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def custom_functions(self) -> list[lsp.CompletionItem]:
|
||||||
|
return self._custom_functions
|
||||||
|
|
||||||
|
@custom_functions.setter
|
||||||
|
def custom_functions(self, value: lsp.CompletionItem):
|
||||||
|
self._custom_functions.append(value)
|
||||||
|
|
||||||
|
|
||||||
|
class _Completion(Visitor):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._custom_functions: list[lsp.CompletionItem] = []
|
||||||
|
self._proc_signatures = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def custom_functions(self) -> list[lsp.CompletionItem]:
|
||||||
|
return self._custom_functions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proc_signatures(self):
|
||||||
|
return self._proc_signatures
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._custom_functions = []
|
||||||
|
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):
|
||||||
|
routine = command.routine
|
||||||
|
|
||||||
|
# Collect custom proc names and their signatures
|
||||||
|
if routine.contents == "proc" and command.args:
|
||||||
|
first_arg = command.args[0]
|
||||||
|
if not getattr(first_arg, "value", None):
|
||||||
|
return
|
||||||
|
|
||||||
|
if any(item.label == first_arg.value for item in standard_items.nx_procs):
|
||||||
|
return
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
return
|
||||||
|
|
||||||
|
param_list_node = command.args[1]
|
||||||
|
if not hasattr(param_list_node, "children"):
|
||||||
|
return
|
||||||
|
|
||||||
|
param_names = []
|
||||||
|
for arg in param_list_node.children:
|
||||||
|
if isinstance(arg, BareWord):
|
||||||
|
param_names.append(arg.value)
|
||||||
|
elif isinstance(arg, List) and len(arg.children) >= 1:
|
||||||
|
first = arg.children[0]
|
||||||
|
if isinstance(first, BareWord):
|
||||||
|
param_names.append(first.value)
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -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,29 @@
|
|||||||
|
import lsprotocol.types as lsp
|
||||||
|
from tclint.syntax_tree import Visitor, Command
|
||||||
|
|
||||||
|
|
||||||
|
class InlayHintGenerator(Visitor):
|
||||||
|
def __init__(self, proc_signatures):
|
||||||
|
self.proc_signatures = proc_signatures
|
||||||
|
self.hints = []
|
||||||
|
|
||||||
|
def visit_command(self, command: Command):
|
||||||
|
name = getattr(command.routine, "contents", None)
|
||||||
|
if name not in self.proc_signatures:
|
||||||
|
return
|
||||||
|
|
||||||
|
param_names = self.proc_signatures[name]
|
||||||
|
for idx, arg in enumerate(command.args):
|
||||||
|
if idx >= len(param_names):
|
||||||
|
break
|
||||||
|
param_name = param_names[idx]
|
||||||
|
|
||||||
|
if arg.pos:
|
||||||
|
line, col = arg.pos
|
||||||
|
self.hints.append(
|
||||||
|
lsp.InlayHint(
|
||||||
|
position=lsp.Position(line=line - 1, character=col - 1),
|
||||||
|
label=f"{param_name}:",
|
||||||
|
kind=lsp.InlayHintKind.Parameter,
|
||||||
|
)
|
||||||
|
)
|
||||||
+64
-40
@@ -1,49 +1,73 @@
|
|||||||
import textwrap
|
import io
|
||||||
|
from typing import Optional, Tuple
|
||||||
from tclint.parser import Parser
|
from tclint.parser import Parser
|
||||||
from tclint.lexer import (
|
from tclint.commands import CommandArgError
|
||||||
STATE_BRACEDWORD,
|
from tclint.syntax_tree import (
|
||||||
TOK_LBRACE,
|
BracedWord,
|
||||||
TOK_EOF,
|
BareWord,
|
||||||
TOK_RBRACE,
|
BracedExpression,
|
||||||
TclSyntaxError,
|
List,
|
||||||
|
QuotedWord,
|
||||||
|
Expression,
|
||||||
)
|
)
|
||||||
from tclint.syntax_tree import BracedWord
|
from tclint.lexer import TclSyntaxError, Lexer, TOK_EOF
|
||||||
|
|
||||||
|
|
||||||
class CustomParser(Parser):
|
class CustomParser(Parser):
|
||||||
def parse_braced_word(self, ts):
|
def __init__(self, debug=False, command_plugins=None):
|
||||||
"""
|
super().__init__(debug, command_plugins)
|
||||||
Ersetzt BracedWord durch echtes Script, wenn mehrzeilig.
|
# Used to normalize newlines consistently with open()'s universal newlines mode.
|
||||||
"""
|
self._decoder = io.IncrementalNewlineDecoder(None, True)
|
||||||
|
|
||||||
|
def parse(self, script: str, pos: Optional[Tuple[int, int]] = None):
|
||||||
|
script = self._decoder.decode(script, True)
|
||||||
|
lexer = Lexer(pos=pos)
|
||||||
|
lexer.input(script)
|
||||||
|
tree = self._parse_script(lexer, in_command_sub=False)
|
||||||
|
assert lexer.type() == TOK_EOF, (
|
||||||
|
"Didn't reach EOF parsing script, please file a bug report."
|
||||||
|
)
|
||||||
|
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def _parse_operator(self, ts):
|
||||||
pos = ts.pos()
|
pos = ts.pos()
|
||||||
ts.lexer.push_state(STATE_BRACEDWORD)
|
|
||||||
ts.assert_(TOK_LBRACE)
|
|
||||||
|
|
||||||
content = ""
|
# hacky logic to handle parsing legal operators
|
||||||
expected = [pos]
|
|
||||||
while True:
|
if ts.value() in {"*", "&", "|"}:
|
||||||
t = ts.type()
|
# one or two of these characters are legal operators
|
||||||
if t == TOK_EOF:
|
operator = ts.value()
|
||||||
raise TclSyntaxError(
|
|
||||||
"reached EOF without finding match for brace",
|
|
||||||
expected[-1],
|
|
||||||
ts.pos(),
|
|
||||||
)
|
|
||||||
if t == TOK_LBRACE:
|
|
||||||
expected.append(ts.pos())
|
|
||||||
elif t == TOK_RBRACE:
|
|
||||||
expected.pop()
|
|
||||||
if not expected:
|
|
||||||
ts.lexer.pop_state()
|
|
||||||
ts.next()
|
|
||||||
break
|
|
||||||
content += ts.value()
|
|
||||||
ts.next()
|
ts.next()
|
||||||
|
if ts.value() == operator:
|
||||||
|
operator += ts.value()
|
||||||
|
ts.next()
|
||||||
|
elif ts.value() in {"<", ">"}:
|
||||||
|
operator = ts.value()
|
||||||
|
ts.next()
|
||||||
|
if ts.value() in {operator, "="}:
|
||||||
|
operator += ts.value()
|
||||||
|
ts.next()
|
||||||
|
elif ts.value() in {"=", "!"}:
|
||||||
|
operator = ts.value()
|
||||||
|
ts.next()
|
||||||
|
if ts.value() != "=":
|
||||||
|
raise TclSyntaxError(
|
||||||
|
f"invalid operator in expression: {operator}", pos, ts.pos()
|
||||||
|
)
|
||||||
|
operator += ts.value()
|
||||||
|
ts.next()
|
||||||
|
elif ts.value() in {"*", "/", "%", "+", "-", "^", "eq", "ne", "in", "ni"}:
|
||||||
|
operator = ts.value()
|
||||||
|
ts.next()
|
||||||
|
else:
|
||||||
|
message = "invalid operator in expression: "
|
||||||
|
if ts.value() == "\\ ":
|
||||||
|
message += (
|
||||||
|
"\\ (check for trailing whitespace if it's the end of the line)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message += ts.value()
|
||||||
|
raise TclSyntaxError(message, pos, ts.pos())
|
||||||
|
|
||||||
end_pos = ts.pos()
|
return BareWord(operator, pos=pos, end_pos=ts.pos())
|
||||||
# Mehrzeilig? Dann als Script parsen:
|
|
||||||
if "\n" in content.strip():
|
|
||||||
self.parse_script(content)
|
|
||||||
|
|
||||||
# Einzeilig: unverändert als Literal
|
|
||||||
return BracedWord(content, pos=pos, end_pos=end_pos)
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,76 +1,200 @@
|
|||||||
from tclint.parser import Parser
|
import enum
|
||||||
from tclint.syntax_tree import Visitor, BareWord, VarSub, Comment, Command, Function
|
from typing import List
|
||||||
|
from tclint.syntax_tree import Visitor, QuotedWord, Command, BareWord
|
||||||
TOKEN_TYPES = {
|
from tclint.commands import get_commands
|
||||||
"command": 0,
|
import attrs
|
||||||
"variable": 1,
|
from common.load_data import standard_items
|
||||||
"function": 2,
|
import lsprotocol.types as lsp
|
||||||
"string": 3,
|
|
||||||
"number": 4,
|
|
||||||
"keyword": 5,
|
|
||||||
"comment": 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SemanticTokenCollector(Visitor):
|
class TokenModifier(enum.IntFlag):
|
||||||
def __init__(self):
|
deprecated = enum.auto()
|
||||||
self.tokens = []
|
readonly = enum.auto()
|
||||||
|
defaultLibrary = enum.auto()
|
||||||
|
definition = enum.auto()
|
||||||
|
declaration = enum.auto()
|
||||||
|
builtin = enum.auto()
|
||||||
|
|
||||||
def _add_token(self, node, token_type):
|
|
||||||
if not node.pos or not node.end_pos:
|
@attrs.define
|
||||||
|
class Token:
|
||||||
|
line: int
|
||||||
|
offset: int
|
||||||
|
lenght: int
|
||||||
|
|
||||||
|
tok_type: str = ""
|
||||||
|
tok_modifiers: List[TokenModifier] = attrs.field(factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self) -> int:
|
||||||
|
"""Compatibility alias for misspelled 'lenght' field."""
|
||||||
|
return self.lenght
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN_TYPES = [
|
||||||
|
"keyword",
|
||||||
|
"variable",
|
||||||
|
"function",
|
||||||
|
"operator",
|
||||||
|
"parameter",
|
||||||
|
"type",
|
||||||
|
"class",
|
||||||
|
"string",
|
||||||
|
"parameter",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _Highlighter(Visitor):
|
||||||
|
def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]):
|
||||||
|
self._commands = get_commands(plugins)
|
||||||
|
self._tokens = []
|
||||||
|
self.custom_functions = custom_functions
|
||||||
|
|
||||||
|
def _get_token_info(self, node):
|
||||||
|
"""Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren."""
|
||||||
|
if not hasattr(node, "pos"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Einfacher Fall: Node hat direkten value
|
||||||
|
if hasattr(node, "value") and node.value is not None:
|
||||||
|
line, col = node.pos
|
||||||
|
return (line - 1, col - 1), len(node.value)
|
||||||
|
|
||||||
|
# CompoundBareWord: versuche erstes Segment
|
||||||
|
if hasattr(node, "children") and node.children:
|
||||||
|
first_segment = node.children[0]
|
||||||
|
if hasattr(first_segment, "value") and first_segment.value is not None and hasattr(first_segment, "pos"):
|
||||||
|
line, col = first_segment.pos
|
||||||
|
return (line - 1, col - 1), len(first_segment.value)
|
||||||
|
|
||||||
|
# Fallback: Gesamtlänge aus Positionen berechnen
|
||||||
|
if hasattr(node, "end_pos"):
|
||||||
|
start_line, start_col = node.pos
|
||||||
|
end_line, end_col = node.end_pos
|
||||||
|
if start_line == end_line:
|
||||||
|
length = end_col - start_col
|
||||||
|
return (start_line - 1, start_col - 1), length
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def visit_quoted_word(self, word: QuotedWord):
|
||||||
|
if not word.contents:
|
||||||
return
|
return
|
||||||
|
line, col = word.contents_pos
|
||||||
line, col = node.pos
|
self._tokens.append(((line - 1, col - 1), len(word.contents), "string", []))
|
||||||
end_line, end_col = node.end_pos
|
pass
|
||||||
length = (end_col - col) if line == end_line else 1
|
|
||||||
|
|
||||||
self.tokens.append((line - 1, col - 1, length, token_type, 0))
|
|
||||||
|
|
||||||
def visit_command(self, command: Command):
|
|
||||||
self._add_token(command.routine, "command")
|
|
||||||
for arg in command.args:
|
|
||||||
arg.accept(self, recurse=True)
|
|
||||||
|
|
||||||
def visit_comment(self, comment: Comment):
|
|
||||||
self._add_token(comment, "comment")
|
|
||||||
|
|
||||||
def visit_bare_word(self, word: BareWord):
|
def visit_bare_word(self, word: BareWord):
|
||||||
if word.value.isdigit():
|
# Intentionally do not classify bare words as functions here.
|
||||||
self._add_token(word, "number")
|
# Function highlighting is handled in visit_command for the routine only,
|
||||||
else:
|
# using completion items (custom functions) as the source of truth.
|
||||||
self._add_token(word, "string")
|
return
|
||||||
|
|
||||||
def visit_var_sub(self, var_sub: VarSub):
|
def visit_command(self, command: Command):
|
||||||
self._add_token(var_sub, "variable")
|
routine = command.routine
|
||||||
|
|
||||||
def visit_function(self, function: Function):
|
# Highlight functions (custom or standard) when used as the routine
|
||||||
self._add_token(function.name, "function")
|
name = getattr(routine, "contents", None)
|
||||||
for arg in function.args:
|
if name:
|
||||||
arg.accept(self, recurse=True)
|
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":
|
||||||
|
line, col = routine.contents_pos
|
||||||
|
self._tokens.append(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(line - 1, col - 1),
|
||||||
|
len(routine.contents),
|
||||||
|
"function",
|
||||||
|
[TokenModifier.builtin],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if routine.contents == "set" and command.args:
|
||||||
|
first_arg = command.args[0]
|
||||||
|
token_info = self._get_token_info(first_arg)
|
||||||
|
if token_info:
|
||||||
|
(line, col), length = token_info
|
||||||
|
self._tokens.append(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(line, col),
|
||||||
|
length,
|
||||||
|
"variable",
|
||||||
|
[TokenModifier.declaration],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if routine.contents == "proc" and command.args:
|
||||||
|
first_arg = command.args[0]
|
||||||
|
if hasattr(first_arg, "pos") and hasattr(first_arg, "value"):
|
||||||
|
line, col = first_arg.pos
|
||||||
|
self._tokens.append(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(line - 1, col - 1),
|
||||||
|
len(first_arg.value),
|
||||||
|
"function",
|
||||||
|
[TokenModifier.declaration],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def collect_semantic_tokens(code: str):
|
if len(command.args) >= 2:
|
||||||
parser = Parser()
|
param_list = command.args[1]
|
||||||
tree = parser.parse(code)
|
|
||||||
visitor = SemanticTokenCollector()
|
|
||||||
tree.accept(visitor, recurse=True)
|
|
||||||
return visitor.tokens
|
|
||||||
|
|
||||||
|
# BracedWord oder Liste erwartet
|
||||||
|
if hasattr(param_list, "children"):
|
||||||
|
for child in param_list.children:
|
||||||
|
# Parameter kann einfaches Wort sein
|
||||||
|
if hasattr(child, "value") and child.value is not None:
|
||||||
|
line, col = child.pos
|
||||||
|
self._tokens.append(
|
||||||
|
(
|
||||||
|
(line - 1, col - 1),
|
||||||
|
len(child.value),
|
||||||
|
"parameter",
|
||||||
|
[TokenModifier.declaration],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def encode_tokens(tokens):
|
# Parameter mit Default-Wert ist meist eine List (z. B. {arg default})
|
||||||
tokens.sort()
|
elif hasattr(child, "children") and len(child.children) >= 1:
|
||||||
encoded = []
|
name_node = child.children[0]
|
||||||
|
if hasattr(name_node, "value") and hasattr(name_node, "pos"):
|
||||||
|
line, col = name_node.pos
|
||||||
|
self._tokens.append(
|
||||||
|
(
|
||||||
|
(line - 1, col - 1),
|
||||||
|
len(name_node.value),
|
||||||
|
"parameter",
|
||||||
|
[TokenModifier.declaration],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if routine.contents == "namespace" and command.args:
|
||||||
|
first_arg = command.args[1]
|
||||||
|
if hasattr(first_arg, "pos") and first_arg.value is not None:
|
||||||
|
line, col = first_arg.pos
|
||||||
|
self._tokens.append((((line - 1, col - 1), len(first_arg.value), "class", [])))
|
||||||
|
|
||||||
last_line = 0
|
def tokens(self) -> list[Token]:
|
||||||
last_char = 0
|
"""Encode tokens as described in
|
||||||
|
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens.
|
||||||
|
"""
|
||||||
|
tokens = []
|
||||||
|
last_line = 0
|
||||||
|
last_col = 0
|
||||||
|
for (line, col), length, tok_type, tok_modifier in sorted(self._tokens, key=lambda x: x[0]):
|
||||||
|
line_delta = line - last_line
|
||||||
|
col_delta = col
|
||||||
|
if line == last_line:
|
||||||
|
col_delta -= last_col
|
||||||
|
|
||||||
for line, char, length, token_type, modifiers in tokens:
|
tokens.append(Token(line_delta, col_delta, length, tok_type, tok_modifier))
|
||||||
delta_line = line - last_line
|
last_line, last_col = line, col
|
||||||
delta_start = char - last_char if delta_line == 0 else char
|
|
||||||
|
|
||||||
encoded.extend([delta_line, delta_start, length, token_type, modifiers])
|
return tokens
|
||||||
|
|
||||||
last_line = line
|
|
||||||
last_char = char if delta_line == 0 else 0
|
|
||||||
|
|
||||||
return encoded
|
|
||||||
|
|||||||
@@ -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,108 @@
|
|||||||
|
# tools/tcl_parser_client.py
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class ParserProcess:
|
||||||
|
"""
|
||||||
|
Async wrapper for the NDJSON stdin/stdout parser process.
|
||||||
|
Start once at server startup; call parse() concurrently.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cmd):
|
||||||
|
self.cmd = cmd
|
||||||
|
self.proc: Optional[asyncio.Process] = None
|
||||||
|
self._write_lock = asyncio.Lock()
|
||||||
|
self._pending: Dict[str, asyncio.Future] = {}
|
||||||
|
self._reader_task: Optional[asyncio.Task] = None
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
if self.proc:
|
||||||
|
return
|
||||||
|
self.proc = await asyncio.create_subprocess_exec(
|
||||||
|
*self.cmd,
|
||||||
|
stdin=asyncio.subprocess.PIPE,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
self._reader_task = asyncio.create_task(self._reader_loop())
|
||||||
|
asyncio.create_task(self._read_stderr())
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
if not self.proc:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.proc.terminate()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await self.proc.wait()
|
||||||
|
if self._reader_task:
|
||||||
|
self._reader_task.cancel()
|
||||||
|
self.proc = None
|
||||||
|
|
||||||
|
async def parse(self, tcl: str, timeout: float = 5.0) -> str:
|
||||||
|
"""
|
||||||
|
Send `tcl` to the parser and await the S-expression string result.
|
||||||
|
Raises on parse error or if the parser dies.
|
||||||
|
"""
|
||||||
|
if not self.proc:
|
||||||
|
await self.start()
|
||||||
|
|
||||||
|
req_id = uuid.uuid4().hex
|
||||||
|
payload = json.dumps({"id": req_id, "tcl": tcl}, separators=(",", ":"))
|
||||||
|
|
||||||
|
fut = asyncio.get_event_loop().create_future()
|
||||||
|
self._pending[req_id] = fut
|
||||||
|
|
||||||
|
async with self._write_lock:
|
||||||
|
assert self.proc and self.proc.stdin
|
||||||
|
self.proc.stdin.write((payload + "\n").encode("utf-8"))
|
||||||
|
await self.proc.stdin.drain()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await asyncio.wait_for(fut, timeout)
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
self._pending.pop(req_id, None)
|
||||||
|
|
||||||
|
async def _reader_loop(self):
|
||||||
|
assert self.proc and self.proc.stdout
|
||||||
|
reader = self.proc.stdout
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
line = await reader.readline()
|
||||||
|
if not line:
|
||||||
|
# parser process closed
|
||||||
|
for f in list(self._pending.values()):
|
||||||
|
if not f.done():
|
||||||
|
f.set_exception(RuntimeError("parser process terminated"))
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
obj = json.loads(line.decode("utf-8", "replace"))
|
||||||
|
except Exception as e:
|
||||||
|
# ignore malformed lines or log
|
||||||
|
continue
|
||||||
|
rid = obj.get("id")
|
||||||
|
fut = self._pending.get(rid)
|
||||||
|
if fut and not fut.done():
|
||||||
|
if obj.get("ok"):
|
||||||
|
fut.set_result(obj.get("result"))
|
||||||
|
else:
|
||||||
|
fut.set_exception(RuntimeError(obj.get("error") or "parse error"))
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
|
||||||
|
async def _read_stderr(self):
|
||||||
|
# optional: log parser stderr
|
||||||
|
assert self.proc and self.proc.stderr
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
line = await self.proc.stderr.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
# Replace this with your LSP server logging
|
||||||
|
print("[tcl_parser stderr]", line.decode("utf-8", "replace").rstrip())
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
@@ -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",
|
||||||
|
|||||||
Generated
+216
@@ -0,0 +1,216 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.99"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc"
|
||||||
|
dependencies = [
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.143"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tcl_parser"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tree-sitter",
|
||||||
|
"tree-sitter-tcl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree-sitter"
|
||||||
|
version = "0.25.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d7b8994f367f16e6fa14b5aebbcb350de5d7cbea82dc5b00ae997dd71680dd2"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"regex",
|
||||||
|
"regex-syntax",
|
||||||
|
"serde_json",
|
||||||
|
"streaming-iterator",
|
||||||
|
"tree-sitter-language",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree-sitter-language"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree-sitter-tcl"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "git+https://github.com/tree-sitter-grammars/tree-sitter-tcl#8f11ac7206a54ed11210491cee1e0657e2962c47"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"tree-sitter-language",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "tcl_parser"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tree-sitter = "0.25.8"
|
||||||
|
tree-sitter-tcl = { git = "https://github.com/tree-sitter-grammars/tree-sitter-tcl" }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
anyhow = "1.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "tcl_parser"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use tree_sitter::Parser;
|
||||||
|
|
||||||
|
pub fn parse_to_sexp(src: &str) -> Result<String, anyhow::Error> {
|
||||||
|
let lang = tree_sitter_tcl::LANGUAGE;
|
||||||
|
let mut parser = Parser::new();
|
||||||
|
parser.set_language(&lang.into())?;
|
||||||
|
let tree = parser
|
||||||
|
.parse(src.as_bytes(), None)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("failed to parse input"))?;
|
||||||
|
Ok(tree.root_node().to_sexp())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_sample() {
|
||||||
|
let src = r#"proc ProcName {arg1 arg2 {arg3 3}} {
|
||||||
|
# Comment
|
||||||
|
set var1 100
|
||||||
|
}"#;
|
||||||
|
let sexp = parse_to_sexp(src).expect("parse");
|
||||||
|
assert!(sexp.contains("proc"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use std::io::{ self, BufRead };
|
||||||
|
use serde::{ Deserialize, Serialize };
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
println!(
|
||||||
|
"tcl_parser server starting (NDJSON stdin). Send JSON lines with {{'id':..., 'tcl': '...'}}"
|
||||||
|
);
|
||||||
|
let stdin = io::stdin();
|
||||||
|
for line in stdin.lock().lines() {
|
||||||
|
let line = line?;
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let req: Request = match serde_json::from_str(&line) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
let resp = Response::error(None, format!("invalid request json: {}", e));
|
||||||
|
println!("{}", serde_json::to_string(&resp)?);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = req.id.clone();
|
||||||
|
match tcl_parser::parse_to_sexp(&req.tcl) {
|
||||||
|
Ok(sexp) => {
|
||||||
|
let resp = Response::ok(id, sexp);
|
||||||
|
println!("{}", serde_json::to_string(&resp)?);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let resp = Response::error(id, format!("parse error: {}", e));
|
||||||
|
println!("{}", serde_json::to_string(&resp)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Request {
|
||||||
|
id: Option<String>,
|
||||||
|
tcl: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
id: Option<String>,
|
||||||
|
ok: bool,
|
||||||
|
result: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
fn ok(id: Option<String>, result: String) -> Self {
|
||||||
|
Self { id, ok: true, result: Some(result), error: None }
|
||||||
|
}
|
||||||
|
fn error(id: Option<String>, err: String) -> Self {
|
||||||
|
Self { id, ok: false, result: None, error: Some(err) }
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
-7
@@ -1,13 +1,116 @@
|
|||||||
proc myProc {arg {opt 1}} {
|
set ::custom_flag(from_move,$::mom_path_name) 1
|
||||||
|
# set ::custom_flag(from_move,$::mom_path_name) 1
|
||||||
|
|
||||||
|
set te875st 11111
|
||||||
|
|
||||||
|
#set ::custom_flag(from_move,$::mom_path_name) 1
|
||||||
|
|
||||||
|
if {$main == 1 && 1 == 1} {
|
||||||
|
puts "main"
|
||||||
}
|
}
|
||||||
|
|
||||||
set myVar 1
|
proc test {} {
|
||||||
|
puts "main"
|
||||||
|
proc llll {} {}
|
||||||
|
set rrrrrrr
|
||||||
|
}
|
||||||
|
LIB_GE_command_buffer_edit_insert MOM_tool_change_LIB TOOL_CHANGE_AUTO {CUSTOM_after_tool_change_call} mytag after @TOOL_CHANGE_AUTO
|
||||||
|
|
||||||
namespace eval myNameSpace {
|
MOM_abort
|
||||||
proc namespaceProc {} {}
|
|
||||||
|
namespace eval myns {
|
||||||
|
proc add {a b} {
|
||||||
|
set sum [expr {$a + $b}]
|
||||||
|
return $sum
|
||||||
|
}
|
||||||
|
set config "debug"
|
||||||
}
|
}
|
||||||
|
|
||||||
set result [myNameSpace::namespaceProc]
|
#_________________________________________________________________________________________________
|
||||||
|
# <Documentation>
|
||||||
|
# Function to output a spacer line or empty line
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
proc SERVICE_spacer_output {type {length 20} {line_num 0} {output 1}} {
|
||||||
|
LIB_GE_message [string repeat $type $length] "output_$output" $line_num
|
||||||
|
}
|
||||||
|
|
||||||
MOM_abort_program "Test"
|
|
||||||
|
SERVICE_spacer_output "*" 2 0 0
|
||||||
|
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
# <Documentation>
|
||||||
|
# Function to delete the file
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
proc SERVICE_remove_file {file} {
|
||||||
|
if {![SERVICE_check_file_exists $file]} {return}
|
||||||
|
MOM_remove_file $file
|
||||||
|
}
|
||||||
|
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
# <Documentation>
|
||||||
|
# Function to check if the file exists
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
proc SERVICE_check_file_exists {file} {
|
||||||
|
if {[file exists $file]} {return 1}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
# <Documentation>
|
||||||
|
# Ask UDE Info for the Tool
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
proc SERVICE_ask_ude_tool {pos ude_name tool_name} {
|
||||||
|
MOM_ask_ude_info $tool_name "tool" $pos
|
||||||
|
|
||||||
|
if {[lsearch $::mom_result $ude_name] != -1} {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
# <Documentation>
|
||||||
|
# Ask UDE Info for the Operation
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
proc SERVICE_ask_ude_operation {pos ude_name path_name} {
|
||||||
|
MOM_ask_ude_info $path_name "operation" $pos
|
||||||
|
|
||||||
|
if {[lsearch $::mom_result $ude_name] != -1} {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
# <Documentation>
|
||||||
|
# output suppress or dont suppress
|
||||||
|
# [SERVICE_output_handling "ingore_output"] ignores the output
|
||||||
|
# arg options: ingore_output
|
||||||
|
# restore
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
proc SERVICE_output_handling {handler} {
|
||||||
|
set ::lib_ge(hidden_output) $handler
|
||||||
|
}
|
||||||
|
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
# <Documentation>
|
||||||
|
# write the mom_tool_data to store tool information
|
||||||
|
# this function is called in start of program
|
||||||
|
#_________________________________________________________________________________________________
|
||||||
|
proc SERVICE_get_tool_data {} {
|
||||||
|
global mom_tool_data
|
||||||
|
global mom_operation_info
|
||||||
|
|
||||||
|
set mom_tool_data(toollist) ""
|
||||||
|
set operations $::mom_operation_name_list
|
||||||
|
foreach operation $operations {
|
||||||
|
if {[lsearch -exact $mom_tool_data(toollist) $mom_operation_info($operation,tool_name)] == -1} {
|
||||||
|
lappend mom_tool_data(toollist) $mom_operation_info($operation,tool_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LIB_GE_command_buffer_edit_replace MOM_end_of_program_LIB END_OF_PROGRAM @END_OF_PROG {
|
||||||
|
MOM_do_template "end_of_program_rewind"
|
||||||
|
} EndOfProgramRewind
|
||||||
|
|||||||
Reference in New Issue
Block a user