Compare commits
25
Commits
2026.9.100
...
2026.9.501
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6228503fc | ||
|
|
4de05caac7 | ||
|
|
5c3ebc4c82 | ||
|
|
5e5c7dcc42 | ||
|
|
84821d2852 | ||
|
|
547da644ce | ||
|
|
757b885f28 | ||
|
|
3762a01d40 | ||
|
|
91b86f29cb | ||
|
|
f88a50d4ab | ||
|
|
c28836933c | ||
|
|
91c8c4aff3 | ||
|
|
d8611d7aea | ||
|
|
7e9a4359cd | ||
|
|
b7f28ab1a3 | ||
|
|
9b368ba761 | ||
|
|
82e13cf7ce | ||
|
|
d104906508 | ||
|
|
a5ff3b55ff | ||
|
|
26225eb8ed | ||
|
|
844138b383 | ||
|
|
d7eb72f417 | ||
|
|
af5acfc946 | ||
|
|
20f76b6a20 | ||
|
|
081b488fe3 |
+2
-1
@@ -9,4 +9,5 @@ __pycache__
|
||||
.nox
|
||||
*.g4
|
||||
.antlr
|
||||
.claude
|
||||
.claude
|
||||
/test/postprocessor/
|
||||
|
||||
Vendored
+28
-2
@@ -13,10 +13,23 @@
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
|
||||
"${env:TEMP}/nx-post-support-vscode-debug",
|
||||
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
|
||||
"${env:TEMP}/nx-post-support-vscode-debug/test/postprocessor"
|
||||
],
|
||||
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||
"linux": {
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}",
|
||||
"${workspaceFolder}/test/postprocessor"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
"!**/node_modules/**"
|
||||
]
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
||||
@@ -46,10 +59,23 @@
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
|
||||
"${env:TEMP}/nx-post-support-vscode-debug",
|
||||
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
|
||||
"${env:TEMP}/nx-post-support-vscode-debug/test/postprocessor"
|
||||
],
|
||||
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||
"linux": {
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}",
|
||||
"${workspaceFolder}/test/postprocessor"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
"!**/node_modules/**"
|
||||
]
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
||||
|
||||
Vendored
+7
@@ -15,6 +15,13 @@
|
||||
"${workspaceFolder}",
|
||||
"${env:TEMP}\\nx-post-support-vscode-debug"
|
||||
],
|
||||
"linux": {
|
||||
"command": "npm",
|
||||
"args": ["run", "compile:debug"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
## Unreleased
|
||||
|
||||
- Add Go to Definition for TclOO classes, constructors, and resolved methods, including PSC library definitions
|
||||
- Index PSC layer scripts (including external paths and legacy Windows encoding) and share TclOO class metadata across files for completion, signatures, inlay hints, and highlighting
|
||||
- Add document-local TclOO method completion for `new`/`create` instances, `my`, and statically inferred return chains
|
||||
- Suggest TclOO class names as classes and semantically highlight their declarations and calls
|
||||
- Show signature help and parameter inlay hints for resolved TclOO methods and constructors, including optional and variadic arguments
|
||||
- Add incoming and outgoing call hierarchy for custom TCL procedures and MOM event handlers
|
||||
- Add document highlights for procedure and variable occurrences
|
||||
- Make completion context-aware and prioritize local, current-file, workspace, and built-in symbols
|
||||
- Add command-aware completion for Tcl subcommands, fixed arguments, and valid options
|
||||
- Add semantic argument completion for variables, procedures, namespaces, and local file paths
|
||||
- Add placeholder-based snippets for common Tcl structures and `dict for`
|
||||
- Integrate the NX Tcl Remote Debugger directly into NX Postprocessor Support
|
||||
- Add `nx-tcl` attach configurations and breakpoint support for TCL and DEF files
|
||||
- Support breakpoints, stepping, stack frames, variables, watches, evaluation, logpoints, hit conditions, and Tcl error stops
|
||||
|
||||
@@ -12,7 +12,9 @@ A comprehensive VS Code extension providing language support and remote debuggin
|
||||
- **Signature Help** - Shows parameters and documentation for custom and NX procedures
|
||||
- **Call Hierarchy** - Traces incoming and outgoing calls between custom TCL procedures and MOM event handlers
|
||||
- **Document Highlights** - Highlights all reads, writes, and calls of the symbol under the cursor
|
||||
- **Context-aware Completion** - Prioritizes local symbols and suggests variables or commands based on cursor context
|
||||
- **Context-aware Completion** - Prioritizes local symbols and suggests variables, procedures, namespaces, paths, Tcl subcommands, valid argument values, and options based on cursor context
|
||||
- **PSC and TclOO Classes** - Indexes Tcl scripts referenced by PSC layers, including external folders. Classes from indexed files provide method completion, signature help, parameter hints, and class highlighting in other files. PSC script names may omit `.tcl`; relative folders resolve from the PSC directory, and environment-variable folders are supported. Missing scripts are reported in the output channel; encrypted libraries cannot supply static class metadata.
|
||||
- **Tcl Snippets** - Inserts placeholder-based structures for `if`, `foreach`, `proc`, `switch`, `try`, and `dict for`
|
||||
- **NX Tcl Remote Debugger** - Breakpoints, stepping, call stack, scopes, variables, watches, evaluation, logpoints, hit conditions, and Tcl error stops directly in a running NX Post process
|
||||
|
||||
## Supported File Types
|
||||
@@ -101,7 +103,8 @@ Simply open any supported file type and enjoy:
|
||||
- Signature help while entering procedure arguments
|
||||
- Incoming and outgoing call hierarchy for custom procedures and MOM event handlers
|
||||
- Document-wide highlights for procedure and variable occurrences
|
||||
- Context-aware completion with local symbols ranked before workspace and built-in symbols
|
||||
- Context-aware completion with local symbols ranked before workspace and built-in symbols, plus semantic arguments, local paths, Tcl subcommands, and options such as `string compare -nocase`
|
||||
- Placeholder-based snippets for common Tcl control structures and procedures
|
||||
- Remote NX Tcl debugging with breakpoints and full stepping
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface CdlEventHandler {
|
||||
eventName: string
|
||||
parameterNames: string[]
|
||||
toggleOffParameterNames?: string[]
|
||||
}
|
||||
|
||||
function structuralCode(line: string): string {
|
||||
@@ -33,18 +34,6 @@ function structuralCode(line: string): string {
|
||||
return result
|
||||
}
|
||||
|
||||
function braceDelta(line: string): number {
|
||||
let delta = 0
|
||||
for (const character of structuralCode(line)) {
|
||||
if (character === "{") {
|
||||
delta += 1
|
||||
} else if (character === "}") {
|
||||
delta -= 1
|
||||
}
|
||||
}
|
||||
return delta
|
||||
}
|
||||
|
||||
export function cdlEventHandlerAtLine(
|
||||
source: string,
|
||||
declarationLine: number
|
||||
@@ -61,35 +50,44 @@ export function cdlEventHandlerAtLine(
|
||||
}
|
||||
|
||||
const parameterNames: string[] = []
|
||||
const toggleOffParameterNames: string[] = []
|
||||
let currentParameter: string | undefined
|
||||
let eventOpened = false
|
||||
let depth = 0
|
||||
|
||||
for (let lineNumber = declarationLine; lineNumber < lines.length; lineNumber++) {
|
||||
eventLines: for (let lineNumber = declarationLine; lineNumber < lines.length; lineNumber++) {
|
||||
const line = lines[lineNumber]
|
||||
const code = structuralCode(line)
|
||||
|
||||
if (eventOpened && depth === 1) {
|
||||
const parameterMatch = /^\s*PARAM\s+([^\s{]+)/.exec(code)
|
||||
if (parameterMatch) {
|
||||
parameterNames.push(parameterMatch[1])
|
||||
}
|
||||
}
|
||||
|
||||
const delta = braceDelta(line)
|
||||
if (!eventOpened && delta > 0) {
|
||||
eventOpened = true
|
||||
}
|
||||
if (eventOpened) {
|
||||
depth += delta
|
||||
if (depth <= 0) {
|
||||
break
|
||||
const tokens = code.match(/[{}]|[^\s{}]+/g) ?? []
|
||||
for (let index = 0; index < tokens.length; index++) {
|
||||
const token = tokens[index]
|
||||
if (token === "{") {
|
||||
eventOpened = true
|
||||
depth++
|
||||
} else if (token === "}" && eventOpened) {
|
||||
depth--
|
||||
if (depth === 1) currentParameter = undefined
|
||||
if (depth <= 0) break eventLines
|
||||
} else if (depth === 1 && token === "PARAM") {
|
||||
const name = tokens[index + 1]
|
||||
if (name && name !== "{" && name !== "}") {
|
||||
currentParameter = name
|
||||
parameterNames.push(name)
|
||||
index++
|
||||
}
|
||||
} else if (depth === 2 && currentParameter && token === "TOGGLE") {
|
||||
if (tokens[index + 1]?.toLowerCase() === "off") {
|
||||
toggleOffParameterNames.push(currentParameter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
eventName: eventMatch[1],
|
||||
parameterNames
|
||||
parameterNames,
|
||||
toggleOffParameterNames
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,10 +100,14 @@ function momVariableName(parameterName: string): string {
|
||||
}
|
||||
|
||||
export function createCdlEventHandlerSnippet(handler: CdlEventHandler): string {
|
||||
const toggleOffParameters = new Set(handler.toggleOffParameterNames ?? [])
|
||||
const globals = [
|
||||
...new Set(handler.parameterNames.map((parameter) => momVariableName(parameter)))
|
||||
...new Set(handler.parameterNames.flatMap((parameter) => {
|
||||
const variable = momVariableName(parameter)
|
||||
return toggleOffParameters.has(parameter) ? [variable, `${variable}_defined`] : [variable]
|
||||
}))
|
||||
]
|
||||
const lines = [`proc ${momEventName(handler.eventName)} { } {`]
|
||||
const lines = [`proc ${momEventName(handler.eventName)} {args} {`]
|
||||
|
||||
if (globals.length > 0) {
|
||||
lines.push(...globals.map((variable) => ` global ${variable}`), "")
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import * as vscode from "vscode"
|
||||
import {
|
||||
cdlEventHandlerAtLine,
|
||||
createCdlEventHandlerSnippet
|
||||
} from "./cdlEventHandler"
|
||||
import { cdlEventHandlerAtLine, createCdlEventHandlerSnippet } from "./cdlEventHandler"
|
||||
|
||||
const MACHINE_HEADER_REGEX = /^MACHINE\s+\S+/
|
||||
|
||||
@@ -65,10 +62,7 @@ export function diagnosticHandler(document: vscode.TextDocument) {
|
||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||
const text = document.getText()
|
||||
if (!isFirstLineMachine(text)) {
|
||||
const range = new vscode.Range(
|
||||
document.positionAt(0),
|
||||
document.positionAt(text.length)
|
||||
)
|
||||
const range = new vscode.Range(document.positionAt(0), document.positionAt(text.length))
|
||||
const diagnostic = new vscode.Diagnostic(
|
||||
range,
|
||||
"The first line should contain 'MACHINE'.",
|
||||
@@ -82,7 +76,7 @@ export function diagnosticHandler(document: vscode.TextDocument) {
|
||||
|
||||
export function completionHandlerCdl(document: vscode.TextDocument, position: vscode.Position) {
|
||||
const linePrefix = document.lineAt(position).text.substring(0, position.character)
|
||||
const categories = ["MILL", "LATHE", "DRILL"]
|
||||
const categories = ["MILL", "LATHE", "DRILL", "INVALID"]
|
||||
|
||||
if (linePrefix.endsWith("TYPE ")) {
|
||||
return [
|
||||
@@ -193,8 +187,7 @@ export async function definitionCdlEventHandler(
|
||||
.filter(
|
||||
(symbol) =>
|
||||
symbol.kind === vscode.SymbolKind.Function &&
|
||||
(symbol.name === handlerName ||
|
||||
symbol.name.endsWith(`::${handlerName}`))
|
||||
(symbol.name === handlerName || symbol.name.endsWith(`::${handlerName}`))
|
||||
)
|
||||
.map((symbol) => symbol.location)
|
||||
if (indexedLocations.length > 0) {
|
||||
@@ -204,9 +197,7 @@ export async function definitionCdlEventHandler(
|
||||
// The Tcl language server may still be starting; use the file fallback below.
|
||||
}
|
||||
|
||||
const declaration = new RegExp(
|
||||
`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`
|
||||
)
|
||||
const declaration = new RegExp(`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`)
|
||||
const tclFiles = await vscode.workspace.findFiles(
|
||||
"**/*.tcl",
|
||||
"**/{.git,.nox,.venv,dist,node_modules,out}/**"
|
||||
@@ -233,12 +224,7 @@ export async function definitionCdlEventHandler(
|
||||
locations.push(
|
||||
new vscode.Location(
|
||||
uri,
|
||||
new vscode.Range(
|
||||
lineNumber,
|
||||
start,
|
||||
lineNumber,
|
||||
start + handlerName.length
|
||||
)
|
||||
new vscode.Range(lineNumber, start, lineNumber, start + handlerName.length)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ async function createServer(
|
||||
}
|
||||
|
||||
// Options to control the language client
|
||||
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.tcl")
|
||||
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.{tcl,psc}")
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
// Register the server for python documents
|
||||
documentSelector: isVirtualWorkspace()
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "nx-post-support",
|
||||
"displayName": "NX Postprocessor Support",
|
||||
"description": "VS Code extension for NX CAM postprocessor development with language support and remote Tcl debugging for CDL, TCL, and DEF files",
|
||||
"version": "2026.8.300",
|
||||
"version": "2026.9.500",
|
||||
"publisher": "Christoph",
|
||||
"icon": "images/nx-1.png",
|
||||
"activationEvents": [
|
||||
|
||||
+222
-134
@@ -38,13 +38,19 @@ update_sys_path(
|
||||
# Imports needed for the language server goes below this.
|
||||
# **********************************************************
|
||||
# pylint: disable=wrong-import-position,import-error
|
||||
import lsp_jsonrpc as jsonrpc
|
||||
import lsprotocol.types as lsp
|
||||
from common.load_data import standard_items
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from pygls import uris
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
from tools.completion_items import completion_context, ranked_completion_items
|
||||
|
||||
import lsp_jsonrpc as jsonrpc
|
||||
from common.load_data import standard_items
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from tools.completion_items import (
|
||||
CompletionContext,
|
||||
array_element_completions,
|
||||
completion_context,
|
||||
ranked_completion_items,
|
||||
)
|
||||
from tools.folding_ranges import build_folding_ranges
|
||||
from tools.inlay_hint import (
|
||||
InlayHintGenerator,
|
||||
@@ -69,37 +75,36 @@ from tools.semantic_tokens import (
|
||||
_Highlighter,
|
||||
)
|
||||
from tools.signature_help import build_signature_help
|
||||
from tools.tcloo_arguments import method_signature_help
|
||||
from tclint.lexer import TclSyntaxError
|
||||
from tools.tcloo_completion import parse_completion_source, tcloo_completions
|
||||
from tools.tcloo_symbols import class_completion_items
|
||||
from tools.tcloo_navigation import tcloo_definition
|
||||
from tools.tcl_command_completion import (
|
||||
TCL_COMMAND_ITEMS,
|
||||
TCL_COMMAND_NAMES,
|
||||
DynamicCompletionKind,
|
||||
path_completion_items,
|
||||
tcl_argument_completion,
|
||||
)
|
||||
|
||||
WORKSPACE_SETTINGS = {}
|
||||
GLOBAL_SETTINGS = {}
|
||||
|
||||
|
||||
MAX_WORKERS = 5
|
||||
LSP_SERVER = TclLanguageServer(
|
||||
name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS
|
||||
)
|
||||
LSP_SERVER = TclLanguageServer(name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS)
|
||||
|
||||
BUILTIN_PROC_NAMES = {
|
||||
item.label
|
||||
for item in standard_items.tcl_keyword_list + standard_items.nx_procs
|
||||
}
|
||||
BUILTIN_PROC_NAMES = {item.label for item in standard_items.tcl_keyword_list + standard_items.nx_procs} | set(TCL_COMMAND_NAMES)
|
||||
BUILTIN_VARIABLE_NAMES = {item.label for item in standard_items.nx_variables}
|
||||
STATIC_COMPLETION_ITEMS = tuple(
|
||||
standard_items.tcl_keyword_list
|
||||
+ standard_items.nx_procs
|
||||
+ standard_items.nx_variables
|
||||
)
|
||||
STATIC_VARIABLE_ITEMS = {
|
||||
item.label: item for item in standard_items.nx_variables
|
||||
}
|
||||
BUILTIN_INLAY_SIGNATURES = build_builtin_inlay_signatures(
|
||||
standard_items.json_data.get("MOM_procs", [])
|
||||
)
|
||||
_TCL_COMMAND_ITEMS_BY_LABEL = {item.label: item for item in TCL_COMMAND_ITEMS}
|
||||
_TCL_KEYWORD_ITEMS = [_TCL_COMMAND_ITEMS_BY_LABEL.get(item.label, item) for item in standard_items.tcl_keyword_list]
|
||||
_STATIC_TCL_LABELS = {item.label for item in standard_items.tcl_keyword_list}
|
||||
STATIC_COMPLETION_ITEMS = tuple(_TCL_KEYWORD_ITEMS + [item for item in TCL_COMMAND_ITEMS if item.label not in _STATIC_TCL_LABELS] + standard_items.nx_procs + standard_items.nx_variables)
|
||||
STATIC_VARIABLE_ITEMS = {item.label: item for item in standard_items.nx_variables}
|
||||
BUILTIN_INLAY_SIGNATURES = build_builtin_inlay_signatures(standard_items.json_data.get("MOM_procs", []))
|
||||
BUILTIN_HOVER_ITEMS = {}
|
||||
for _hover_item in (
|
||||
standard_items.json_data.get("MOM_procs", [])
|
||||
+ standard_items.json_data.get("mom_variables", [])
|
||||
):
|
||||
for _hover_item in standard_items.json_data.get("MOM_procs", []) + standard_items.json_data.get("mom_variables", []):
|
||||
BUILTIN_HOVER_ITEMS.setdefault(_hover_item.get("label"), _hover_item)
|
||||
|
||||
# **********************************************************
|
||||
@@ -210,12 +215,23 @@ def did_rename_files(params: lsp.RenameFilesParams) -> None:
|
||||
def did_change_watched_files(params: lsp.DidChangeWatchedFilesParams) -> None:
|
||||
"""Keep indexes for closed Tcl files synchronized with disk changes."""
|
||||
for change in params.changes:
|
||||
if pathlib.Path(uris.to_fs_path(change.uri)).suffix.lower() == ".psc":
|
||||
_refresh_psc_index()
|
||||
continue
|
||||
if change.type == lsp.FileChangeType.Deleted:
|
||||
LSP_SERVER.remove_file_state(change.uri)
|
||||
else:
|
||||
_index_tcl_file_from_disk(change.uri)
|
||||
|
||||
|
||||
def _refresh_psc_index():
|
||||
folders = LSP_SERVER.workspace.folders
|
||||
roots = [pathlib.Path(uris.to_fs_path(uri)) for uri in folders]
|
||||
if not roots and LSP_SERVER.workspace.root_path:
|
||||
roots = [pathlib.Path(LSP_SERVER.workspace.root_path)]
|
||||
LSP_SERVER.refresh_psc_scripts(roots, report=log_warning)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(
|
||||
lsp.TEXT_DOCUMENT_DIAGNOSTIC,
|
||||
lsp.DiagnosticOptions(
|
||||
@@ -229,9 +245,7 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
||||
uri = params.text_document.uri
|
||||
doc = LSP_SERVER.workspace.get_text_document(uri)
|
||||
diagnostic_state = LSP_SERVER.diagnostic_snapshot(uri)
|
||||
was_cached = (
|
||||
diagnostic_state is not None and diagnostic_state[0] == doc.version
|
||||
)
|
||||
was_cached = diagnostic_state is not None and diagnostic_state[0] == doc.version
|
||||
if not was_cached:
|
||||
LSP_SERVER.compute_diagnostics(doc)
|
||||
diagnostic_state = LSP_SERVER.diagnostic_snapshot(uri)
|
||||
@@ -250,16 +264,72 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
||||
|
||||
@LSP_SERVER.feature(
|
||||
lsp.TEXT_DOCUMENT_COMPLETION,
|
||||
lsp.CompletionOptions(trigger_characters=["$"]),
|
||||
lsp.CompletionOptions(trigger_characters=["$", " ", "-", "(", ","]),
|
||||
)
|
||||
def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
tree = LSP_SERVER.get_tree(doc)
|
||||
globals_set, procs_locals, proc_ranges = LSP_SERVER.variable_index_for_document(
|
||||
doc, tree
|
||||
)
|
||||
|
||||
position = params.position
|
||||
source_lines = LSP_SERVER.get_lines(doc)
|
||||
oo_items = tcloo_completions(source_lines, position, LSP_SERVER.class_snapshot(doc.path))
|
||||
if oo_items is not None:
|
||||
return lsp.CompletionList(is_incomplete=False, items=oo_items)
|
||||
array_items = array_element_completions(
|
||||
source_lines,
|
||||
position,
|
||||
LSP_SERVER.navigation_snapshot().values(),
|
||||
str(pathlib.Path(uris.to_fs_path(doc.uri))),
|
||||
)
|
||||
if array_items is not None:
|
||||
return lsp.CompletionList(is_incomplete=False, items=array_items)
|
||||
context = completion_context(source_lines, position)
|
||||
|
||||
# Variable completion wins inside command arguments. Otherwise prefer the
|
||||
# narrow command grammar when the cursor is at a known subcommand/option.
|
||||
argument_completion = None
|
||||
if context != CompletionContext.VARIABLE:
|
||||
argument_completion = tcl_argument_completion(source_lines, position)
|
||||
if argument_completion is not None and argument_completion.dynamic_kind is None:
|
||||
items = ranked_completion_items(
|
||||
((0, item) for item in argument_completion.items),
|
||||
CompletionContext.GENERAL,
|
||||
)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
if argument_completion is not None and argument_completion.dynamic_kind == DynamicCompletionKind.PATH:
|
||||
dynamic_items: tuple[lsp.CompletionItem, ...] = ()
|
||||
if doc.uri.startswith("file:"):
|
||||
document_path = pathlib.Path(uris.to_fs_path(doc.uri))
|
||||
dynamic_items = path_completion_items(
|
||||
document_path.parent,
|
||||
argument_completion,
|
||||
position,
|
||||
)
|
||||
path_candidates = [(0, item) for item in argument_completion.items]
|
||||
path_candidates.extend((10, item) for item in dynamic_items)
|
||||
items = ranked_completion_items(
|
||||
path_candidates,
|
||||
CompletionContext.GENERAL,
|
||||
)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
# Space and dash are registered only to open command-aware suggestions.
|
||||
# Do not display the broad fallback list when such a trigger has no match.
|
||||
if (
|
||||
argument_completion is None
|
||||
and params.context is not None
|
||||
and params.context.trigger_kind == lsp.CompletionTriggerKind.TriggerCharacter
|
||||
and params.context.trigger_character in {" ", "-", "(", ","}
|
||||
):
|
||||
return lsp.CompletionList(is_incomplete=False, items=[])
|
||||
|
||||
try:
|
||||
tree = LSP_SERVER.get_tree(doc)
|
||||
except TclSyntaxError:
|
||||
tree = parse_completion_source(doc.source)
|
||||
if tree is None:
|
||||
return lsp.CompletionList(is_incomplete=False, items=[])
|
||||
globals_set, procs_locals, proc_ranges = LSP_SERVER.variable_index_for_document(doc, tree)
|
||||
|
||||
local_names: set[str] = set()
|
||||
for proc_range in proc_ranges:
|
||||
end_line = proc_range.end_line or proc_range.start_line
|
||||
@@ -268,17 +338,16 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||
break
|
||||
|
||||
candidates: list[tuple[int, lsp.CompletionItem]] = []
|
||||
candidates.extend((0, item) for item in class_completion_items(tree))
|
||||
for name in sorted(local_names - globals_set):
|
||||
candidates.append(
|
||||
(
|
||||
0,
|
||||
lsp.CompletionItem(
|
||||
label=name,
|
||||
kind=lsp.CompletionItemKind.Variable,
|
||||
detail="Local variable",
|
||||
),
|
||||
)
|
||||
)
|
||||
candidates.append((
|
||||
0,
|
||||
lsp.CompletionItem(
|
||||
label=name,
|
||||
kind=lsp.CompletionItemKind.Variable,
|
||||
detail="Local variable",
|
||||
),
|
||||
))
|
||||
|
||||
for name in sorted(globals_set):
|
||||
item = STATIC_VARIABLE_ITEMS.get(name)
|
||||
@@ -288,12 +357,10 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||
kind=lsp.CompletionItemKind.Variable,
|
||||
detail="Workspace variable",
|
||||
)
|
||||
candidates.append(
|
||||
(
|
||||
100,
|
||||
item,
|
||||
)
|
||||
)
|
||||
candidates.append((
|
||||
100,
|
||||
item,
|
||||
))
|
||||
|
||||
filepath = str(pathlib.Path(uris.to_fs_path(doc.uri)))
|
||||
items_by_file = LSP_SERVER.completion_items_by_file_snapshot()
|
||||
@@ -301,8 +368,59 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||
priority = 100 if LSP_SERVER.paths_equal(item_path, filepath) else 200
|
||||
candidates.extend((priority, item) for item in items_by_file[item_path])
|
||||
|
||||
if argument_completion is not None:
|
||||
static_candidates = [(0, item) for item in argument_completion.items]
|
||||
if argument_completion.dynamic_kind == DynamicCompletionKind.VARIABLE:
|
||||
variable_candidates = list(candidates)
|
||||
variable_candidates.extend((300, item) for item in standard_items.nx_variables)
|
||||
items = ranked_completion_items(
|
||||
variable_candidates,
|
||||
CompletionContext.VARIABLE,
|
||||
)
|
||||
# Command options are valid alongside variables, even though they
|
||||
# are keywords and must not pass through the variable-only filter.
|
||||
items = [*ranked_completion_items(static_candidates, CompletionContext.GENERAL), *items]
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
if argument_completion.dynamic_kind == DynamicCompletionKind.PROCEDURE:
|
||||
procedure_kinds = {
|
||||
lsp.CompletionItemKind.Constructor,
|
||||
lsp.CompletionItemKind.Function,
|
||||
lsp.CompletionItemKind.Method,
|
||||
}
|
||||
procedure_candidates = [(priority, item) for priority, item in candidates if item.kind in procedure_kinds]
|
||||
procedure_candidates.extend((300, item) for item in standard_items.nx_procs)
|
||||
items = ranked_completion_items(
|
||||
[*static_candidates, *procedure_candidates],
|
||||
CompletionContext.GENERAL,
|
||||
)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
if argument_completion.dynamic_kind == DynamicCompletionKind.NAMESPACE:
|
||||
namespace_candidates = list(static_candidates)
|
||||
prefix_is_absolute = argument_completion.active_prefix.startswith("::")
|
||||
for index in LSP_SERVER.navigation_snapshot().values():
|
||||
priority = 100 if LSP_SERVER.paths_equal(index.path, filepath) else 200
|
||||
for occurrence in index.occurrences:
|
||||
if occurrence.identity.kind != "namespace":
|
||||
continue
|
||||
name = occurrence.identity.name
|
||||
label = name if prefix_is_absolute else name.removeprefix("::")
|
||||
namespace_candidates.append((
|
||||
priority,
|
||||
lsp.CompletionItem(
|
||||
label=label,
|
||||
kind=lsp.CompletionItemKind.Module,
|
||||
detail="Tcl namespace",
|
||||
),
|
||||
))
|
||||
items = ranked_completion_items(
|
||||
namespace_candidates,
|
||||
CompletionContext.GENERAL,
|
||||
)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
candidates.extend((300, item) for item in STATIC_COMPLETION_ITEMS)
|
||||
context = completion_context(LSP_SERVER.get_lines(doc), position)
|
||||
items = ranked_completion_items(candidates, context)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
@@ -316,11 +434,17 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||
)
|
||||
def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None:
|
||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
tree = LSP_SERVER.get_tree(document)
|
||||
method_help = method_signature_help(document.source, params.position, LSP_SERVER.class_snapshot(document.path))
|
||||
if method_help is not None:
|
||||
return method_help
|
||||
try:
|
||||
tree = LSP_SERVER.get_tree(document)
|
||||
except TclSyntaxError:
|
||||
tree = parse_completion_source(document.source)
|
||||
if tree is None:
|
||||
return None
|
||||
|
||||
custom_signatures, custom_docs = LSP_SERVER.proc_metadata_snapshot(
|
||||
document.path
|
||||
)
|
||||
custom_signatures, custom_docs = LSP_SERVER.proc_metadata_snapshot(document.path)
|
||||
|
||||
return build_signature_help(
|
||||
document.source,
|
||||
@@ -366,20 +490,17 @@ def inlay_hints(params: lsp.InlayHintParams):
|
||||
|
||||
# Built-in NX procedures are the fallback. Workspace procedures replace them,
|
||||
# and a declaration in the current file wins over duplicate workspace names.
|
||||
custom_signatures = LSP_SERVER.custom_inlay_signatures_snapshot(
|
||||
document.path
|
||||
)
|
||||
custom_signatures = LSP_SERVER.custom_inlay_signatures_snapshot(document.path)
|
||||
signatures = ChainMap(custom_signatures, BUILTIN_INLAY_SIGNATURES)
|
||||
|
||||
generator = InlayHintGenerator(
|
||||
document.source,
|
||||
signatures,
|
||||
external_classes=LSP_SERVER.class_snapshot(document.path),
|
||||
source_lines=LSP_SERVER.get_lines(document),
|
||||
requested_range=params.range,
|
||||
parameter_names=parameter_names,
|
||||
suppress_when_argument_matches_name=inlay_settings.get(
|
||||
"suppressWhenArgumentMatchesName", True
|
||||
),
|
||||
suppress_when_argument_matches_name=inlay_settings.get("suppressWhenArgumentMatchesName", True),
|
||||
)
|
||||
return generator.generate(tree)
|
||||
|
||||
@@ -400,19 +521,20 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
|
||||
|
||||
# Reuse cached AST
|
||||
tree = LSP_SERVER.get_tree(document)
|
||||
classes = LSP_SERVER.class_snapshot(document.path)
|
||||
hl.highlight_classes(tree, classes)
|
||||
hl.highlight_methods(tree, document.source, document.uri, classes)
|
||||
tree.accept(hl, recurse=True)
|
||||
|
||||
tokens = hl.tokens()
|
||||
for token in tokens:
|
||||
data.extend(
|
||||
[
|
||||
token.line,
|
||||
token.offset,
|
||||
token.length,
|
||||
TOKEN_TYPE_INDEX[token.tok_type],
|
||||
reduce(operator.or_, token.tok_modifiers, 0),
|
||||
]
|
||||
)
|
||||
data.extend([
|
||||
token.line,
|
||||
token.offset,
|
||||
token.length,
|
||||
TOKEN_TYPE_INDEX[token.tok_type],
|
||||
reduce(operator.or_, token.tok_modifiers, 0),
|
||||
])
|
||||
return lsp.SemanticTokens(data=data)
|
||||
|
||||
|
||||
@@ -454,9 +576,7 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
||||
if match and match.get("kind") == "function":
|
||||
label = match.get("label", "")
|
||||
parameters = match.get("parameters", [])
|
||||
param_lines = (
|
||||
"\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
||||
)
|
||||
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"])
|
||||
@@ -485,28 +605,30 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
||||
# Build a merged map of proc -> docs gathered during initialization and updates
|
||||
proc_doc = LSP_SERVER.proc_documentation(token, document.path)
|
||||
if proc_doc is not None:
|
||||
return lsp.Hover(
|
||||
lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=proc_doc)
|
||||
)
|
||||
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=proc_doc))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DEFINITION)
|
||||
def goto_definition(params: lsp.DefinitionParams):
|
||||
"""Resolve Tcl proc and variable definitions through the symbol index."""
|
||||
"""Resolve TclOO declarations, then indexed proc and variable definitions."""
|
||||
try:
|
||||
workspace = LSP_SERVER.workspace
|
||||
except RuntimeError:
|
||||
workspace = None
|
||||
if workspace is not None:
|
||||
document = workspace.get_text_document(params.text_document.uri)
|
||||
target = tcloo_definition(document.source, document.uri, params.position,
|
||||
LSP_SERVER.class_snapshot(document.path))
|
||||
if target is not None:
|
||||
return [target]
|
||||
context = _navigation_context(params.text_document.uri, params.position)
|
||||
if context is None:
|
||||
return None
|
||||
|
||||
indexes, definitions, _, identity = context
|
||||
locations = [
|
||||
lsp.Location(uri=index.uri, range=occurrence.range)
|
||||
for index, occurrence in matching_occurrences(
|
||||
identity, indexes, definitions
|
||||
)
|
||||
if occurrence.is_definition
|
||||
]
|
||||
locations = [lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in matching_occurrences(identity, indexes, definitions) if occurrence.is_definition]
|
||||
return _sorted_locations(locations) or None
|
||||
|
||||
|
||||
@@ -550,9 +672,7 @@ def references(params: lsp.ReferenceParams) -> list[lsp.Location]:
|
||||
indexes, definitions, _, identity = context
|
||||
locations = [
|
||||
lsp.Location(uri=index.uri, range=occurrence.range)
|
||||
for index, occurrence in matching_occurrences(
|
||||
identity, indexes, definitions
|
||||
)
|
||||
for index, occurrence in matching_occurrences(identity, indexes, definitions)
|
||||
if params.context.include_declaration or not occurrence.is_definition
|
||||
]
|
||||
return _sorted_locations(locations)
|
||||
@@ -584,11 +704,7 @@ def _is_renamable(
|
||||
if identity.kind == "proc":
|
||||
if basename in BUILTIN_PROC_NAMES:
|
||||
return False
|
||||
definition_count = sum(
|
||||
occurrence.is_definition and occurrence.identity == identity
|
||||
for index in indexes.values()
|
||||
for occurrence in index.occurrences
|
||||
)
|
||||
definition_count = sum(occurrence.is_definition and occurrence.identity == identity for index in indexes.values() for occurrence in index.occurrences)
|
||||
return definition_count == 1
|
||||
|
||||
return basename not in BUILTIN_VARIABLE_NAMES
|
||||
@@ -603,9 +719,7 @@ def prepare_rename(params: lsp.PrepareRenameParams):
|
||||
indexes, definitions, occurrence, identity = context
|
||||
if not _is_renamable(identity, indexes, definitions):
|
||||
return None
|
||||
return lsp.PrepareRenamePlaceholder(
|
||||
range=occurrence.range, placeholder=occurrence.placeholder
|
||||
)
|
||||
return lsp.PrepareRenamePlaceholder(range=occurrence.range, placeholder=occurrence.placeholder)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(
|
||||
@@ -637,9 +751,7 @@ def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None:
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
changes.setdefault(index.uri, []).append(
|
||||
lsp.TextEdit(range=occurrence.range, new_text=params.new_name)
|
||||
)
|
||||
changes.setdefault(index.uri, []).append(lsp.TextEdit(range=occurrence.range, new_text=params.new_name))
|
||||
|
||||
for edits in changes.values():
|
||||
edits.sort(
|
||||
@@ -750,12 +862,8 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
||||
|
||||
settings = params.initialization_options["settings"]
|
||||
_update_workspace_settings(settings)
|
||||
log_to_output(
|
||||
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"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")
|
||||
semantic_tokens_legend = lsp.SemanticTokensLegend(
|
||||
token_types=TOKEN_TYPES,
|
||||
token_modifiers=[m.name for m in TokenModifier],
|
||||
@@ -764,9 +872,7 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
||||
capabilities=lsp.ServerCapabilities(
|
||||
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
|
||||
folding_range_provider=True,
|
||||
semantic_tokens_provider=lsp.SemanticTokensOptions(
|
||||
legend=semantic_tokens_legend, full=True, range=False
|
||||
),
|
||||
semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
|
||||
definition_provider=True,
|
||||
document_highlight_provider=True,
|
||||
references_provider=True,
|
||||
@@ -800,19 +906,10 @@ def initialized(_params: lsp.InitializedParams):
|
||||
"node_modules",
|
||||
"out",
|
||||
}
|
||||
tcl_files = (
|
||||
path
|
||||
for path in root_path.rglob("*.tcl")
|
||||
if not any(
|
||||
part.casefold() in skipped_directories
|
||||
for part in path.relative_to(root_path).parts[:-1]
|
||||
)
|
||||
)
|
||||
tcl_files = (path for path in root_path.rglob("*.tcl") if not any(part.casefold() in skipped_directories for part in path.relative_to(root_path).parts[:-1]))
|
||||
for filepath in sorted(tcl_files, key=lambda path: str(path).casefold()):
|
||||
try:
|
||||
document = TextDocument(
|
||||
uri=filepath.as_uri(), language_id="tcl"
|
||||
)
|
||||
document = TextDocument(uri=filepath.as_uri(), language_id="tcl")
|
||||
LSP_SERVER.update_poco_completion_for_file(
|
||||
document,
|
||||
cache_tree=False,
|
||||
@@ -820,6 +917,7 @@ def initialized(_params: lsp.InitializedParams):
|
||||
)
|
||||
except Exception as error:
|
||||
log_to_output(f"Fehler beim Parsen von {filepath}: {error}")
|
||||
_refresh_psc_index()
|
||||
log_to_output("Background indexing completed.")
|
||||
except Exception as e:
|
||||
log_to_output(f"Background indexing failed: {e}")
|
||||
@@ -926,36 +1024,26 @@ def _get_settings_by_document(document: TextDocument | None):
|
||||
# *****************************************************
|
||||
# Logging and notification.
|
||||
# *****************************************************
|
||||
def log_to_output(
|
||||
message: str, msg_type: lsp.MessageType = lsp.MessageType.Log
|
||||
) -> None:
|
||||
LSP_SERVER.window_log_message(
|
||||
lsp.LogMessageParams(message=message, type=msg_type)
|
||||
)
|
||||
def log_to_output(message: str, msg_type: lsp.MessageType = lsp.MessageType.Log) -> None:
|
||||
LSP_SERVER.window_log_message(lsp.LogMessageParams(message=message, type=msg_type))
|
||||
|
||||
|
||||
def log_error(message: str) -> None:
|
||||
log_to_output(message, lsp.MessageType.Error)
|
||||
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onError", "onWarning", "always"]:
|
||||
LSP_SERVER.window_show_message(
|
||||
lsp.ShowMessageParams(message=message, type=lsp.MessageType.Error)
|
||||
)
|
||||
LSP_SERVER.window_show_message(lsp.ShowMessageParams(message=message, type=lsp.MessageType.Error))
|
||||
|
||||
|
||||
def log_warning(message: str) -> None:
|
||||
log_to_output(message, lsp.MessageType.Warning)
|
||||
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onWarning", "always"]:
|
||||
LSP_SERVER.window_show_message(
|
||||
lsp.ShowMessageParams(message=message, type=lsp.MessageType.Warning)
|
||||
)
|
||||
LSP_SERVER.window_show_message(lsp.ShowMessageParams(message=message, type=lsp.MessageType.Warning))
|
||||
|
||||
|
||||
def log_always(message: str) -> None:
|
||||
log_to_output(message, lsp.MessageType.Info)
|
||||
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["always"]:
|
||||
LSP_SERVER.window_show_message(
|
||||
lsp.ShowMessageParams(message=message, type=lsp.MessageType.Info)
|
||||
)
|
||||
LSP_SERVER.window_show_message(lsp.ShowMessageParams(message=message, type=lsp.MessageType.Info))
|
||||
|
||||
|
||||
# *****************************************************
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import threading
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
@@ -14,6 +15,9 @@ from tclint.lexer import TclSyntaxError
|
||||
from tclint.violations import Violation
|
||||
from tools import checks, parser
|
||||
from tools.completion_items import CompletionCollector
|
||||
from tools.tcloo_symbols import class_completion_items
|
||||
from tools.tcloo_completion import indexed_classes
|
||||
from tools.file_sourcing import get_all_psc_files, psc_script_files
|
||||
from tools.formatter import NxFormatter as Formatter
|
||||
from tools.inlay_hint import InlayHintSignature, build_custom_inlay_signatures
|
||||
from tools.navigation import FileSymbolIndex, build_file_symbol_index
|
||||
@@ -34,6 +38,10 @@ class TclLanguageServer(LanguageServer):
|
||||
self.poco_completion: dict = {}
|
||||
self.proc_signatures: dict = {}
|
||||
self.proc_docs: dict = {}
|
||||
self.class_indexes: dict = {}
|
||||
self.psc_script_paths: list[str] = []
|
||||
self._psc_files: dict[str, list[pathlib.Path]] = {}
|
||||
self._psc_lock = threading.RLock()
|
||||
self.navigation_indexes: dict[str, FileSymbolIndex] = {}
|
||||
self.variable_indexes: dict[
|
||||
str,
|
||||
@@ -170,6 +178,74 @@ class TclLanguageServer(LanguageServer):
|
||||
self._workspace_completion_cache = (self._index_generation, items)
|
||||
return items
|
||||
|
||||
def class_snapshot(self, current_path) -> dict:
|
||||
"""Share class metadata, without stale definitions from the active file.
|
||||
|
||||
PSC scripts override other workspace files in their listed load order.
|
||||
The request parser adds the current document's declarations last.
|
||||
"""
|
||||
with self._index_lock:
|
||||
indexes = {self._normalized_path(path): classes for path, classes in self.class_indexes.items()}
|
||||
paths = sorted(self.class_indexes, key=str.casefold)
|
||||
psc_paths = {self._normalized_path(path) for path in self.psc_script_paths}
|
||||
paths = [path for path in paths if self._normalized_path(path) not in psc_paths]
|
||||
paths.extend(self.psc_script_paths)
|
||||
classes = {}
|
||||
for path in paths:
|
||||
if not self.paths_equal(path, current_path):
|
||||
classes.update(indexes.get(self._normalized_path(path), {}))
|
||||
return classes
|
||||
|
||||
def refresh_psc_scripts(self, roots, report=LOGGER.warning):
|
||||
"""Index PSC dependencies through the same pipeline as workspace procs."""
|
||||
with self._psc_lock:
|
||||
discovered = {}
|
||||
for root in roots:
|
||||
for psc in get_all_psc_files(root):
|
||||
try:
|
||||
discovered[str(psc)] = psc_script_files(psc)
|
||||
except (OSError, ET.ParseError) as error:
|
||||
report(f"Could not read PSC {psc}: {error}")
|
||||
discovered[str(psc)] = self._psc_files.get(str(psc), [])
|
||||
paths = [str(path) for scripts in discovered.values() for path in scripts]
|
||||
with self._index_lock:
|
||||
previous = set(self.psc_script_paths)
|
||||
self.psc_script_paths = paths
|
||||
self._psc_files = discovered
|
||||
try:
|
||||
open_documents = {
|
||||
self._normalized_path(document.path): document
|
||||
for document in self.workspace.text_documents.values()
|
||||
}
|
||||
except RuntimeError:
|
||||
open_documents = {}
|
||||
for removed in previous - set(paths):
|
||||
path = pathlib.Path(removed)
|
||||
if (not any(self._is_same_or_child(path, root) for root in roots)
|
||||
and self._normalized_path(path) not in open_documents):
|
||||
self.remove_file_state(path.as_uri())
|
||||
for path_string in dict.fromkeys(paths):
|
||||
path = pathlib.Path(path_string)
|
||||
uri = path.as_uri()
|
||||
document = open_documents.get(self._normalized_path(path))
|
||||
if document is None and not path.is_file():
|
||||
self.remove_file_state(uri)
|
||||
report(f"PSC script not found: {path}")
|
||||
continue
|
||||
try:
|
||||
if document is None:
|
||||
data = path.read_bytes()
|
||||
try:
|
||||
source = data.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
# Older Windows NX layers use the ANSI code page.
|
||||
source = data.decode("cp1252")
|
||||
document = TextDocument(uri=uri, source=source, language_id="tcl")
|
||||
if not self.update_poco_completion_for_file(document, cache_tree=False):
|
||||
report(f"Could not index PSC script: {path}")
|
||||
except (OSError, UnicodeError) as error:
|
||||
report(f"Could not read PSC script {path}: {error}")
|
||||
|
||||
def completion_items_by_file_snapshot(
|
||||
self,
|
||||
) -> dict[str, tuple[lsp.CompletionItem, ...]]:
|
||||
@@ -190,6 +266,7 @@ class TclLanguageServer(LanguageServer):
|
||||
item.label
|
||||
for path_items in self.poco_completion.values()
|
||||
for item in path_items
|
||||
if item.kind != lsp.CompletionItemKind.Class
|
||||
)
|
||||
self._custom_function_names_cache = (self._index_generation, names)
|
||||
return names
|
||||
@@ -453,6 +530,7 @@ class TclLanguageServer(LanguageServer):
|
||||
self.poco_completion.pop(filepath, None)
|
||||
self.proc_signatures.pop(filepath, None)
|
||||
self.proc_docs.pop(filepath, None)
|
||||
self.class_indexes.pop(filepath, None)
|
||||
self.navigation_indexes.pop(filepath, None)
|
||||
self.variable_indexes.pop(filepath, None)
|
||||
self._committed_index_versions.pop(filepath, None)
|
||||
@@ -484,6 +562,7 @@ class TclLanguageServer(LanguageServer):
|
||||
self.poco_completion,
|
||||
self.proc_signatures,
|
||||
self.proc_docs,
|
||||
self.class_indexes,
|
||||
self.navigation_indexes,
|
||||
self.variable_indexes,
|
||||
self._index_tokens,
|
||||
@@ -542,6 +621,8 @@ class TclLanguageServer(LanguageServer):
|
||||
else self.parse_source(document.source)
|
||||
)
|
||||
tree.accept(collector, recurse=True)
|
||||
collector.custom_functions.extend(class_completion_items(tree))
|
||||
classes = indexed_classes(tree, document.uri, document.source)
|
||||
docs = build_proc_docs(tree, document.source)
|
||||
navigation_index = build_file_symbol_index(
|
||||
filepath, document.uri, tree
|
||||
@@ -562,6 +643,7 @@ class TclLanguageServer(LanguageServer):
|
||||
self.poco_completion[filepath] = list(collector.custom_functions)
|
||||
self.proc_signatures[filepath] = dict(collector.proc_signatures)
|
||||
self.proc_docs[filepath] = docs
|
||||
self.class_indexes[filepath] = classes
|
||||
self.navigation_indexes[filepath] = navigation_index
|
||||
self.variable_indexes[filepath] = (document.version, variable_index)
|
||||
self._committed_index_versions[filepath] = document.version
|
||||
|
||||
@@ -6,9 +6,17 @@ from collections.abc import Iterable, Sequence
|
||||
from enum import Enum
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
from common.load_data import standard_items
|
||||
from tclint.syntax_tree import BareWord, Command, List, Visitor
|
||||
|
||||
from common.load_data import standard_items
|
||||
from tools.navigation import FileSymbolIndex
|
||||
from tools.variable_names import variable_name
|
||||
from tools.tcl_command_completion import (
|
||||
DynamicCompletionKind,
|
||||
line_prefix_at_position,
|
||||
tcl_argument_completion,
|
||||
)
|
||||
|
||||
BUILTIN_VAR_LABELS = {ci.label for ci in standard_items.nx_variables}
|
||||
BUILTIN_PROC_LABELS = {ci.label for ci in standard_items.nx_procs}
|
||||
|
||||
@@ -25,37 +33,112 @@ VARIABLE_KINDS = {
|
||||
lsp.CompletionItemKind.Constant,
|
||||
}
|
||||
COMMAND_KINDS = {
|
||||
lsp.CompletionItemKind.Class,
|
||||
lsp.CompletionItemKind.Function,
|
||||
lsp.CompletionItemKind.Method,
|
||||
lsp.CompletionItemKind.Constructor,
|
||||
lsp.CompletionItemKind.Keyword,
|
||||
lsp.CompletionItemKind.Snippet,
|
||||
}
|
||||
|
||||
_VARIABLE_PREFIX_RE = re.compile(r"(?<!\\)\$(?:\{)?[A-Za-z0-9_:]*$")
|
||||
_COMMAND_PREFIX_RE = re.compile(r"(?:^|[;\[\{])\s*[^\s;\[\]\{\}]*$")
|
||||
_ARRAY_PREFIX_RE = re.compile(
|
||||
r"(?P<name>(?:::)?[A-Za-z_][A-Za-z0-9_:]*)\((?P<key>[^()\n]*)$"
|
||||
)
|
||||
|
||||
|
||||
def _codepoint_offset(line: str, utf16_offset: int) -> int:
|
||||
"""Translate an LSP UTF-16 character offset into a Python string offset."""
|
||||
if utf16_offset <= 0:
|
||||
return 0
|
||||
|
||||
units = 0
|
||||
for offset, character in enumerate(line):
|
||||
units += 2 if ord(character) > 0xFFFF else 1
|
||||
if units >= utf16_offset:
|
||||
return offset + 1
|
||||
return len(line)
|
||||
def array_element_completions(
|
||||
source_lines: Sequence[str],
|
||||
position: lsp.Position,
|
||||
indexes: Iterable[FileSymbolIndex],
|
||||
current_path: str,
|
||||
) -> list[lsp.CompletionItem] | None:
|
||||
"""Complete literal array keys, replacing only the text inside parentheses."""
|
||||
prefix = line_prefix_at_position(source_lines, position)
|
||||
if prefix is None:
|
||||
return None
|
||||
match = _ARRAY_PREFIX_RE.search(prefix)
|
||||
if match is None:
|
||||
return None
|
||||
before = prefix[:match.start()]
|
||||
if not before.endswith("$"):
|
||||
argument = tcl_argument_completion(source_lines, position)
|
||||
if argument is None or argument.dynamic_kind != DynamicCompletionKind.VARIABLE:
|
||||
return None
|
||||
name, key_prefix = match.group("name", "key")
|
||||
part_index = key_prefix.count(",")
|
||||
key_prefix = key_prefix.rsplit(",", 1)[-1]
|
||||
if any(char in key_prefix for char in "$[]{}\\"):
|
||||
return None
|
||||
indexes = list(indexes)
|
||||
local_scope = None
|
||||
for index in indexes:
|
||||
if index.path != current_path:
|
||||
continue
|
||||
for occurrence in index.occurrences:
|
||||
span = occurrence.declaration_range
|
||||
if (
|
||||
occurrence.identity.kind == "proc"
|
||||
and occurrence.is_definition
|
||||
and span is not None
|
||||
and span.start.line <= position.line <= span.end.line
|
||||
):
|
||||
local_scope = f"{index.path}::proc::{occurrence.identity.name}"
|
||||
keys: dict[str, set[tuple[str | None, ...]]] = {}
|
||||
for index in indexes:
|
||||
for occurrence in index.occurrences:
|
||||
parts = occurrence.array_parts
|
||||
key = parts[part_index] if part_index < len(parts) else None
|
||||
if (
|
||||
key
|
||||
and (
|
||||
occurrence.identity.scope is None
|
||||
or occurrence.identity.scope == local_scope
|
||||
)
|
||||
and key.startswith(key_prefix)
|
||||
and not any(char in key for char in "$[]\\")
|
||||
and occurrence.identity.name.removeprefix("::") == name.removeprefix("::")
|
||||
):
|
||||
keys.setdefault(key, set()).add(occurrence.array_template_parts[part_index + 1:])
|
||||
line = source_lines[position.line]
|
||||
suffix = line[len(prefix):]
|
||||
remaining = re.match(r"[^(),\s$\[\]{}]*", suffix).group()
|
||||
has_close = suffix[len(remaining):].startswith((")", ","))
|
||||
start = position.character - len(key_prefix.encode("utf-16-le")) // 2
|
||||
end = position.character + len(remaining.encode("utf-16-le")) // 2
|
||||
items = []
|
||||
for key in sorted(keys):
|
||||
new_text = key
|
||||
# Fill missing index components only; keep an existing comma and suffix.
|
||||
if not suffix[len(remaining):].startswith(","):
|
||||
tails = sorted(keys[key], key=lambda tail: (len(tail), repr(tail)))
|
||||
tail = tails[0]
|
||||
if all(part is not None for part in tail) and any("$" in part for part in tail):
|
||||
new_text = ",".join([key, *tail])
|
||||
new_text += "" if has_close else ")"
|
||||
items.append(lsp.CompletionItem(
|
||||
label=key,
|
||||
kind=lsp.CompletionItemKind.Field,
|
||||
detail=f"{name}({key})",
|
||||
insert_text_format=lsp.InsertTextFormat.PlainText,
|
||||
text_edit=lsp.TextEdit(
|
||||
range=lsp.Range(
|
||||
start=lsp.Position(line=position.line, character=start),
|
||||
end=lsp.Position(line=position.line, character=end),
|
||||
),
|
||||
new_text=new_text,
|
||||
),
|
||||
))
|
||||
return items
|
||||
|
||||
|
||||
def completion_context(
|
||||
source_lines: Sequence[str], position: lsp.Position
|
||||
) -> CompletionContext:
|
||||
if position.line < 0 or position.line >= len(source_lines):
|
||||
prefix = line_prefix_at_position(source_lines, position)
|
||||
if prefix is None:
|
||||
return CompletionContext.GENERAL
|
||||
|
||||
line = source_lines[position.line]
|
||||
prefix = line[: _codepoint_offset(line, position.character)]
|
||||
if _VARIABLE_PREFIX_RE.search(prefix):
|
||||
return CompletionContext.VARIABLE
|
||||
if _COMMAND_PREFIX_RE.search(prefix):
|
||||
@@ -179,8 +262,8 @@ class CompletionCollector(Visitor):
|
||||
# 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
|
||||
var_name = variable_name(first)
|
||||
if var_name:
|
||||
if var_name.startswith("::"):
|
||||
base_name = var_name.split("(", 1)[0]
|
||||
clean_name = base_name[2:] # remove leading '::' for completion display
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
@@ -14,6 +15,9 @@ class SourcedFile:
|
||||
def read_psc_file(psc_file: Path) -> List[SourcedFile]:
|
||||
tree = ET.parse(psc_file)
|
||||
root = tree.getroot()
|
||||
# PSC exports may use a default XML namespace.
|
||||
for element in root.iter():
|
||||
element.tag = element.tag.rsplit("}", 1)[-1]
|
||||
|
||||
layers = root.findall(".//Layer")
|
||||
|
||||
@@ -37,15 +41,23 @@ def read_psc_file(psc_file: Path) -> List[SourcedFile]:
|
||||
|
||||
|
||||
def get_all_psc_files(root_path: Path) -> list[Path]:
|
||||
return [path for path in root_path.rglob("*.psc")]
|
||||
return sorted(root_path.rglob("*.psc"), key=lambda path: str(path).casefold())
|
||||
|
||||
|
||||
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)
|
||||
def psc_script_files(psc_file: Path) -> list[Path]:
|
||||
"""Resolve layer script paths relative to the PSC, preserving load order."""
|
||||
def expanded(value):
|
||||
return Path(os.path.expandvars(value).replace("\\", "/"))
|
||||
|
||||
paths = []
|
||||
for layer in read_psc_file(psc_file):
|
||||
folder = layer.subfolder or "."
|
||||
base = psc_file.parent / expanded(os.environ.get(folder, folder))
|
||||
for name in layer.files:
|
||||
filename = expanded(name)
|
||||
if not filename.suffix:
|
||||
filename = filename.with_suffix(".tcl")
|
||||
path = (base / filename).resolve()
|
||||
if path.suffix.lower() == ".tcl":
|
||||
paths.append(path)
|
||||
return paths
|
||||
|
||||
@@ -9,6 +9,8 @@ from typing import Any
|
||||
import lsprotocol.types as lsp
|
||||
from tclint.syntax_tree import Command, VarSub, Visitor
|
||||
from tools.navigation import FileSymbolIndex
|
||||
from tools.tcloo_arguments import method_parameters
|
||||
from tools.tcloo_completion import resolved_method_calls
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -156,10 +158,13 @@ class InlayHintGenerator(Visitor):
|
||||
requested_range: lsp.Range | None = None,
|
||||
parameter_names: str = "all",
|
||||
suppress_when_argument_matches_name: bool = True,
|
||||
external_classes=None,
|
||||
):
|
||||
self.source_lines = (
|
||||
source_lines if source_lines is not None else source.splitlines()
|
||||
)
|
||||
self.source = source
|
||||
self.external_classes = external_classes
|
||||
self.proc_signatures = proc_signatures
|
||||
self.requested_range = requested_range
|
||||
self.parameter_names = parameter_names
|
||||
@@ -194,6 +199,17 @@ class InlayHintGenerator(Visitor):
|
||||
walk(child)
|
||||
|
||||
walk(tree)
|
||||
if self.parameter_names != "none":
|
||||
for call in resolved_method_calls(self.source, self.external_classes):
|
||||
if not self._node_intersects_requested_range(call.command):
|
||||
continue
|
||||
parameters = method_parameters(call.parameters)
|
||||
signature = InlayHintSignature(
|
||||
parameters=tuple(InlayHintParameter(p.name, variadic=p.variadic) for p in parameters),
|
||||
display_label=" ".join([call.label, *(p.label for p in parameters)]),
|
||||
)
|
||||
self._argument_hints(signature, call.command.args[call.argument_offset:])
|
||||
self.hints.sort(key=lambda hint: (hint.position.line, hint.position.character))
|
||||
return self.hints
|
||||
|
||||
def _position(self, line: int, column: int) -> lsp.Position:
|
||||
@@ -238,7 +254,10 @@ class InlayHintGenerator(Visitor):
|
||||
if signature is None or self.parameter_names == "none":
|
||||
return
|
||||
|
||||
for argument_index, argument in enumerate(command.args):
|
||||
self._argument_hints(signature, command.args)
|
||||
|
||||
def _argument_hints(self, signature, arguments):
|
||||
for argument_index, argument in enumerate(arguments):
|
||||
parameter = self._parameter_for_argument(signature, argument_index)
|
||||
if parameter is None:
|
||||
break
|
||||
|
||||
@@ -4,7 +4,8 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
from tclint.syntax_tree import Command, List, Node, Script, VarSub
|
||||
from tclint.syntax_tree import Command, List, Node, QuotedWord, Script, VarSub
|
||||
from tools.variable_names import array_key_parts, variable_name
|
||||
|
||||
ROOT_NAMESPACE = "::"
|
||||
|
||||
@@ -27,6 +28,9 @@ class SymbolOccurrence:
|
||||
fallback_identity: SymbolIdentity | None = None
|
||||
caller: SymbolIdentity | None = None
|
||||
declaration_range: lsp.Range | None = None
|
||||
array_element: str | None = None
|
||||
array_parts: tuple[str | None, ...] = ()
|
||||
array_template_parts: tuple[str | None, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -96,6 +100,8 @@ def _name_range(node: Node, raw_name: str, *, variable_sub: bool = False) -> lsp
|
||||
column += 2 if getattr(node, "braced", False) else 1
|
||||
else:
|
||||
position = getattr(node, "contents_pos", None) or node.pos
|
||||
if isinstance(node, QuotedWord) and node.contents is None and node.children:
|
||||
position = node.children[0].pos
|
||||
line, column = position
|
||||
|
||||
normalized = _without_array_index(raw_name)
|
||||
@@ -312,6 +318,13 @@ def build_file_symbol_index(
|
||||
is_definition=is_definition,
|
||||
symbol_kind=lsp.SymbolKind.Variable,
|
||||
container_name=_container_name(symbol_identity),
|
||||
array_element=(
|
||||
raw_name.split("(", 1)[1][:-1]
|
||||
if "(" in raw_name and raw_name.endswith(")")
|
||||
else None
|
||||
),
|
||||
array_parts=array_key_parts(node),
|
||||
array_template_parts=array_key_parts(node, preserve_variables=True),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -444,7 +457,7 @@ def build_file_symbol_index(
|
||||
for node, is_definition in _variable_command_nodes(command):
|
||||
if id(node) in declaration_ids:
|
||||
continue
|
||||
raw_name = _static_contents(node)
|
||||
raw_name = variable_name(node)
|
||||
if raw_name:
|
||||
add_variable(
|
||||
node,
|
||||
|
||||
@@ -4,7 +4,10 @@ from typing import List
|
||||
import attrs
|
||||
from common.load_data import standard_items
|
||||
from tclint.commands.plugins import PluginManager
|
||||
from tclint.syntax_tree import BareWord, Command, QuotedWord, Visitor
|
||||
from tclint.syntax_tree import BareWord, BracedWord, Command, QuotedWord, Visitor
|
||||
from tools.variable_names import variable_name
|
||||
from tools.tcloo_symbols import class_symbols
|
||||
from tools.tcloo_completion import _analyze
|
||||
|
||||
# Constructing a PluginManager scans entry points, and get_commands() rebuilds
|
||||
# the builtin command set on every call. Semantic tokens are requested often, so
|
||||
@@ -69,12 +72,10 @@ class _Highlighter(Visitor):
|
||||
def __init__(self, plugins, custom_functions):
|
||||
self._commands = _load_commands(plugins)
|
||||
self._tokens = []
|
||||
self._class_tokens = {}
|
||||
self._method_tokens = {}
|
||||
if isinstance(custom_functions, dict):
|
||||
self._custom_function_names = frozenset(
|
||||
item.label
|
||||
for items in custom_functions.values()
|
||||
for item in items
|
||||
)
|
||||
self._custom_function_names = frozenset(item.label for items in custom_functions.values() for item in items)
|
||||
else:
|
||||
self._custom_function_names = frozenset(custom_functions)
|
||||
|
||||
@@ -83,6 +84,20 @@ class _Highlighter(Visitor):
|
||||
return
|
||||
self._tokens.append((position, length, tok_type, modifiers or []))
|
||||
|
||||
def highlight_classes(self, tree, external_classes=None):
|
||||
declarations, references = class_symbols(tree, external_classes)
|
||||
for node, modifiers in [
|
||||
*((node, [TokenModifier.declaration]) for node in declarations.values()),
|
||||
*((node, []) for node in references),
|
||||
]:
|
||||
line, col = node.contents_pos
|
||||
self._class_tokens[(line - 1, col - 1)] = (
|
||||
(line - 1, col - 1),
|
||||
len(node.contents),
|
||||
"class",
|
||||
modifiers,
|
||||
)
|
||||
|
||||
def _get_token_info(self, node):
|
||||
"""Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren."""
|
||||
if not hasattr(node, "pos"):
|
||||
@@ -110,6 +125,28 @@ class _Highlighter(Visitor):
|
||||
|
||||
return None
|
||||
|
||||
def highlight_methods(self, tree, source, uri, external_classes=None):
|
||||
"""Use the same function token as procs for resolved TclOO methods."""
|
||||
classes, _, calls = _analyze(tree, external_classes=external_classes, uri=uri, source=source)
|
||||
lines = source.splitlines()
|
||||
for info in classes.values():
|
||||
for location in info.method_definitions.values():
|
||||
if location.uri != uri:
|
||||
continue
|
||||
start, end = location.range.start, location.range.end
|
||||
encoded = lines[start.line].encode("utf-16-le")
|
||||
column = len(encoded[:start.character * 2].decode("utf-16-le"))
|
||||
length = len(encoded[start.character * 2:end.character * 2].decode("utf-16-le"))
|
||||
position = (start.line, column)
|
||||
self._method_tokens[position] = (position, length, "function", [TokenModifier.declaration])
|
||||
for call in calls:
|
||||
node = call.command.args[0]
|
||||
if node.contents is None or node.contents_pos is None:
|
||||
continue
|
||||
line, column = node.contents_pos
|
||||
position = (line - 1, column - 1)
|
||||
self._method_tokens[position] = (position, len(node.contents), "function", [])
|
||||
|
||||
def visit_quoted_word(self, word: QuotedWord):
|
||||
if not word.contents:
|
||||
return
|
||||
@@ -136,6 +173,15 @@ class _Highlighter(Visitor):
|
||||
def visit_command(self, command: Command):
|
||||
routine = command.routine
|
||||
|
||||
# Stored procedure names in braced lappend arguments use the same
|
||||
# highlighting as calls, without treating the literal as executable Tcl.
|
||||
if routine.contents in {"lappend", "::lappend"}:
|
||||
for argument in command.args:
|
||||
if (isinstance(argument, BracedWord)
|
||||
and argument.contents in self._custom_function_names | _STANDARD_PROC_NAMES):
|
||||
line, col = argument.contents_pos
|
||||
self._append_token((line - 1, col - 1), len(argument.contents), "function", [])
|
||||
|
||||
# Highlight functions (custom or standard) when used as the routine
|
||||
name = getattr(routine, "contents", None)
|
||||
if name:
|
||||
@@ -156,9 +202,14 @@ class _Highlighter(Visitor):
|
||||
if routine.contents == "puts":
|
||||
line, col = routine.contents_pos
|
||||
self._append_token((line - 1, col - 1), len(routine.contents), "function", [TokenModifier.builtin])
|
||||
if routine.contents == "set" and command.args:
|
||||
if routine.contents in ["set", "append", "lappend"] and command.args:
|
||||
first_arg = command.args[0]
|
||||
token_info = self._get_token_info(first_arg)
|
||||
if first_arg.contents is None:
|
||||
name = variable_name(first_arg)
|
||||
if name:
|
||||
line, col = first_arg.children[0].pos
|
||||
token_info = ((line - 1, col - 1), len(name))
|
||||
if token_info:
|
||||
(line, col), length = token_info
|
||||
self._append_token((line, col), length, "variable", [TokenModifier.declaration])
|
||||
@@ -198,7 +249,10 @@ class _Highlighter(Visitor):
|
||||
tokens = []
|
||||
last_line = 0
|
||||
last_col = 0
|
||||
for (line, col), length, tok_type, tok_modifier in sorted(self._tokens, key=lambda x: x[0]):
|
||||
overrides = {**self._method_tokens, **self._class_tokens}
|
||||
raw_tokens = [token for token in self._tokens if token[0] not in overrides]
|
||||
raw_tokens.extend(overrides.values())
|
||||
for (line, col), length, tok_type, tok_modifier in sorted(raw_tokens, key=lambda x: x[0]):
|
||||
line_delta = line - last_line
|
||||
col_delta = col
|
||||
if line == last_line:
|
||||
|
||||
@@ -0,0 +1,998 @@
|
||||
"""Command-aware completion data and cursor parsing for Tcl commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from lsprotocol.types import (
|
||||
CompletionItem,
|
||||
CompletionItemKind,
|
||||
InsertTextFormat,
|
||||
Position,
|
||||
Range,
|
||||
TextEdit,
|
||||
)
|
||||
|
||||
|
||||
class DynamicCompletionKind(Enum):
|
||||
"""Workspace- or filesystem-backed completion requested by the grammar."""
|
||||
|
||||
VARIABLE = "variable"
|
||||
PROCEDURE = "procedure"
|
||||
NAMESPACE = "namespace"
|
||||
PATH = "path"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TclArgumentCompletion:
|
||||
"""Static suggestions plus an optional dynamic completion category."""
|
||||
|
||||
items: tuple[CompletionItem, ...] = ()
|
||||
dynamic_kind: DynamicCompletionKind | None = None
|
||||
active_prefix: str = ""
|
||||
path_extensions: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DynamicCompletionRule:
|
||||
path: tuple[str, ...]
|
||||
argument_indices: frozenset[int]
|
||||
kind: DynamicCompletionKind
|
||||
path_extensions: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OptionSpec:
|
||||
"""A command option and whether it consumes the following word."""
|
||||
|
||||
label: str
|
||||
takes_value: bool = False
|
||||
values: tuple[str, ...] = ()
|
||||
|
||||
|
||||
STRING_CLASSES = (
|
||||
"alnum",
|
||||
"alpha",
|
||||
"ascii",
|
||||
"boolean",
|
||||
"control",
|
||||
"digit",
|
||||
"double",
|
||||
"entier",
|
||||
"false",
|
||||
"graph",
|
||||
"integer",
|
||||
"list",
|
||||
"lower",
|
||||
"print",
|
||||
"punct",
|
||||
"space",
|
||||
"true",
|
||||
"upper",
|
||||
"wideinteger",
|
||||
"wordchar",
|
||||
"xdigit",
|
||||
)
|
||||
|
||||
|
||||
# Keys are complete command paths. Their values are valid words immediately
|
||||
# following that path. Keeping this declarative makes Tcl-version additions easy.
|
||||
SUBCOMMANDS_BY_PATH: dict[tuple[str, ...], tuple[str, ...]] = {
|
||||
("array",): (
|
||||
"anymore",
|
||||
"donesearch",
|
||||
"exists",
|
||||
"get",
|
||||
"names",
|
||||
"nextelement",
|
||||
"set",
|
||||
"size",
|
||||
"startsearch",
|
||||
"statistics",
|
||||
"unset",
|
||||
),
|
||||
("binary",): ("decode", "encode", "format", "scan"),
|
||||
("binary", "decode"): ("base64", "hex", "uuencode"),
|
||||
("binary", "encode"): ("base64", "hex", "uuencode"),
|
||||
("chan",): (
|
||||
"blocked",
|
||||
"close",
|
||||
"configure",
|
||||
"copy",
|
||||
"create",
|
||||
"eof",
|
||||
"event",
|
||||
"flush",
|
||||
"gets",
|
||||
"names",
|
||||
"pending",
|
||||
"pipe",
|
||||
"pop",
|
||||
"postevent",
|
||||
"push",
|
||||
"puts",
|
||||
"read",
|
||||
"seek",
|
||||
"tell",
|
||||
"truncate",
|
||||
),
|
||||
("clock",): (
|
||||
"add",
|
||||
"clicks",
|
||||
"format",
|
||||
"microseconds",
|
||||
"milliseconds",
|
||||
"scan",
|
||||
"seconds",
|
||||
),
|
||||
("dict",): (
|
||||
"append",
|
||||
"create",
|
||||
"exists",
|
||||
"filter",
|
||||
"for",
|
||||
"get",
|
||||
"incr",
|
||||
"info",
|
||||
"keys",
|
||||
"lappend",
|
||||
"map",
|
||||
"merge",
|
||||
"remove",
|
||||
"replace",
|
||||
"set",
|
||||
"size",
|
||||
"unset",
|
||||
"update",
|
||||
"values",
|
||||
"with",
|
||||
),
|
||||
("dict", "filter"): ("key", "script", "value"),
|
||||
("encoding",): ("convertfrom", "convertto", "dirs", "names", "system"),
|
||||
("file",): (
|
||||
"atime",
|
||||
"attributes",
|
||||
"channels",
|
||||
"copy",
|
||||
"delete",
|
||||
"dirname",
|
||||
"executable",
|
||||
"exists",
|
||||
"extension",
|
||||
"isdirectory",
|
||||
"isfile",
|
||||
"join",
|
||||
"link",
|
||||
"lstat",
|
||||
"mkdir",
|
||||
"mtime",
|
||||
"nativename",
|
||||
"normalize",
|
||||
"owned",
|
||||
"pathtype",
|
||||
"readable",
|
||||
"readlink",
|
||||
"rename",
|
||||
"rootname",
|
||||
"separator",
|
||||
"size",
|
||||
"split",
|
||||
"stat",
|
||||
"system",
|
||||
"tail",
|
||||
"tempfile",
|
||||
"type",
|
||||
"volumes",
|
||||
"writable",
|
||||
),
|
||||
("info",): (
|
||||
"args",
|
||||
"body",
|
||||
"class",
|
||||
"cmdcount",
|
||||
"commands",
|
||||
"complete",
|
||||
"coroutine",
|
||||
"default",
|
||||
"errorstack",
|
||||
"exists",
|
||||
"frame",
|
||||
"functions",
|
||||
"globals",
|
||||
"hostname",
|
||||
"level",
|
||||
"library",
|
||||
"loaded",
|
||||
"locals",
|
||||
"nameofexecutable",
|
||||
"object",
|
||||
"patchlevel",
|
||||
"procs",
|
||||
"script",
|
||||
"sharedlibextension",
|
||||
"tclversion",
|
||||
"vars",
|
||||
),
|
||||
("namespace",): (
|
||||
"children",
|
||||
"code",
|
||||
"current",
|
||||
"delete",
|
||||
"ensemble",
|
||||
"eval",
|
||||
"exists",
|
||||
"export",
|
||||
"forget",
|
||||
"import",
|
||||
"inscope",
|
||||
"origin",
|
||||
"parent",
|
||||
"path",
|
||||
"qualifiers",
|
||||
"tail",
|
||||
"unknown",
|
||||
"upvar",
|
||||
"which",
|
||||
),
|
||||
("namespace", "ensemble"): ("configure", "create", "exists"),
|
||||
("package",): (
|
||||
"forget",
|
||||
"ifneeded",
|
||||
"names",
|
||||
"prefer",
|
||||
"present",
|
||||
"provide",
|
||||
"require",
|
||||
"unknown",
|
||||
"vcompare",
|
||||
"versions",
|
||||
"vsatisfies",
|
||||
),
|
||||
("string",): (
|
||||
"bytelength",
|
||||
"cat",
|
||||
"compare",
|
||||
"equal",
|
||||
"first",
|
||||
"index",
|
||||
"is",
|
||||
"last",
|
||||
"length",
|
||||
"map",
|
||||
"match",
|
||||
"range",
|
||||
"repeat",
|
||||
"replace",
|
||||
"reverse",
|
||||
"tolower",
|
||||
"totitle",
|
||||
"toupper",
|
||||
"trim",
|
||||
"trimleft",
|
||||
"trimright",
|
||||
"wordend",
|
||||
"wordstart",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _options(*labels: str) -> tuple[OptionSpec, ...]:
|
||||
return tuple(OptionSpec(label) for label in labels)
|
||||
|
||||
|
||||
OPTIONS_BY_PATH: dict[tuple[str, ...], tuple[OptionSpec, ...]] = {
|
||||
("unset",): _options("nocomplain"),
|
||||
("binary", "decode", "base64"): (OptionSpec("-strict"),),
|
||||
("binary", "encode", "base64"): (
|
||||
OptionSpec("-maxlen", takes_value=True),
|
||||
OptionSpec("-wrapchar", takes_value=True),
|
||||
),
|
||||
("clock", "format"): (
|
||||
OptionSpec("-format", takes_value=True),
|
||||
OptionSpec("-gmt", takes_value=True, values=("0", "1")),
|
||||
OptionSpec("-locale", takes_value=True),
|
||||
OptionSpec("-timezone", takes_value=True),
|
||||
),
|
||||
("clock", "scan"): (
|
||||
OptionSpec("-base", takes_value=True),
|
||||
OptionSpec("-format", takes_value=True),
|
||||
OptionSpec("-gmt", takes_value=True, values=("0", "1")),
|
||||
OptionSpec("-locale", takes_value=True),
|
||||
OptionSpec("-timezone", takes_value=True),
|
||||
),
|
||||
("exec",): _options("-ignorestderr", "-keepnewline", "--"),
|
||||
("file", "copy"): _options("-force", "--"),
|
||||
("file", "delete"): _options("-force", "--"),
|
||||
("file", "link"): _options("-symbolic", "-hard"),
|
||||
("file", "rename"): _options("-force", "--"),
|
||||
("glob",): (
|
||||
OptionSpec("-directory", takes_value=True),
|
||||
OptionSpec("-join"),
|
||||
OptionSpec("-nocomplain"),
|
||||
OptionSpec("-path", takes_value=True),
|
||||
OptionSpec("-tails"),
|
||||
OptionSpec("-types", takes_value=True),
|
||||
OptionSpec("--"),
|
||||
),
|
||||
("lsearch",): (
|
||||
OptionSpec("-all"),
|
||||
OptionSpec("-ascii"),
|
||||
OptionSpec("-bisect"),
|
||||
OptionSpec("-decreasing"),
|
||||
OptionSpec("-dictionary"),
|
||||
OptionSpec("-exact"),
|
||||
OptionSpec("-glob"),
|
||||
OptionSpec("-increasing"),
|
||||
OptionSpec("-index", takes_value=True),
|
||||
OptionSpec("-inline"),
|
||||
OptionSpec("-integer"),
|
||||
OptionSpec("-nocase"),
|
||||
OptionSpec("-not"),
|
||||
OptionSpec("-real"),
|
||||
OptionSpec("-regexp"),
|
||||
OptionSpec("-sorted"),
|
||||
OptionSpec("-start", takes_value=True),
|
||||
OptionSpec("-subindices"),
|
||||
),
|
||||
("lsort",): (
|
||||
OptionSpec("-ascii"),
|
||||
OptionSpec("-command", takes_value=True),
|
||||
OptionSpec("-decreasing"),
|
||||
OptionSpec("-dictionary"),
|
||||
OptionSpec("-increasing"),
|
||||
OptionSpec("-index", takes_value=True),
|
||||
OptionSpec("-indices"),
|
||||
OptionSpec("-integer"),
|
||||
OptionSpec("-nocase"),
|
||||
OptionSpec("-real"),
|
||||
OptionSpec("-stride", takes_value=True),
|
||||
OptionSpec("-unique"),
|
||||
),
|
||||
("namespace", "which"): _options("-command", "-variable"),
|
||||
("package", "present"): _options("-exact"),
|
||||
("package", "require"): _options("-exact"),
|
||||
("puts",): _options("-nonewline"),
|
||||
("regexp",): (
|
||||
OptionSpec("-about"),
|
||||
OptionSpec("-all"),
|
||||
OptionSpec("-expanded"),
|
||||
OptionSpec("-indices"),
|
||||
OptionSpec("-inline"),
|
||||
OptionSpec("-line"),
|
||||
OptionSpec("-lineanchor"),
|
||||
OptionSpec("-linestop"),
|
||||
OptionSpec("-nocase"),
|
||||
OptionSpec("-start", takes_value=True),
|
||||
OptionSpec("--"),
|
||||
),
|
||||
("regsub",): (
|
||||
OptionSpec("-all"),
|
||||
OptionSpec("-command"),
|
||||
OptionSpec("-expanded"),
|
||||
OptionSpec("-line"),
|
||||
OptionSpec("-lineanchor"),
|
||||
OptionSpec("-linestop"),
|
||||
OptionSpec("-nocase"),
|
||||
OptionSpec("-start", takes_value=True),
|
||||
OptionSpec("--"),
|
||||
),
|
||||
("return",): (
|
||||
OptionSpec(
|
||||
"-code",
|
||||
takes_value=True,
|
||||
values=("ok", "error", "return", "break", "continue"),
|
||||
),
|
||||
OptionSpec("-errorcode", takes_value=True),
|
||||
OptionSpec("-errorinfo", takes_value=True),
|
||||
OptionSpec("-errorstack", takes_value=True),
|
||||
OptionSpec("-level", takes_value=True),
|
||||
OptionSpec("-options", takes_value=True),
|
||||
),
|
||||
("source",): (OptionSpec("-encoding", takes_value=True),),
|
||||
("string", "compare"): (
|
||||
OptionSpec("-nocase"),
|
||||
OptionSpec("-length", takes_value=True),
|
||||
),
|
||||
("string", "equal"): (
|
||||
OptionSpec("-nocase"),
|
||||
OptionSpec("-length", takes_value=True),
|
||||
),
|
||||
("string", "map"): _options("-nocase"),
|
||||
("string", "match"): _options("-nocase"),
|
||||
("switch",): (
|
||||
OptionSpec("-exact"),
|
||||
OptionSpec("-glob"),
|
||||
OptionSpec("-indexvar", takes_value=True),
|
||||
OptionSpec("-matchvar", takes_value=True),
|
||||
OptionSpec("-nocase"),
|
||||
OptionSpec("-regexp"),
|
||||
OptionSpec("--"),
|
||||
),
|
||||
}
|
||||
|
||||
# ``string is`` takes its class before its options, so each class is a concrete
|
||||
# command path for the generic option resolver below.
|
||||
for _string_class in STRING_CLASSES:
|
||||
OPTIONS_BY_PATH[("string", "is", _string_class)] = (
|
||||
OptionSpec("-strict"),
|
||||
OptionSpec("-failindex", takes_value=True),
|
||||
)
|
||||
|
||||
|
||||
VALUES_BY_POSITION: dict[tuple[tuple[str, ...], int], tuple[str, ...]] = {
|
||||
(("array", "names"), 3): ("-exact", "-glob", "-regexp"),
|
||||
(("close",), 2): ("read", "write"),
|
||||
(("open",), 2): ("r", "r+", "w", "w+", "a", "a+"),
|
||||
(("package", "prefer"), 2): ("latest", "stable"),
|
||||
(("seek",), 3): ("start", "current", "end"),
|
||||
(("string", "is"), 2): STRING_CLASSES,
|
||||
}
|
||||
|
||||
|
||||
_REPEATED_ARGUMENTS = frozenset(range(1, 33))
|
||||
_REPEATED_SUBCOMMAND_ARGUMENTS = frozenset(range(2, 33))
|
||||
|
||||
DYNAMIC_COMPLETION_RULES = (
|
||||
# Variable-taking commands.
|
||||
DynamicCompletionRule(("append",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("array", "exists"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("array", "get"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("array", "names"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("array", "set"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("array", "size"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("array", "statistics"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("array", "unset"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("catch",), frozenset({2, 3}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("dict", "append"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("dict", "incr"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("dict", "lappend"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("dict", "set"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("dict", "unset"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("dict", "update"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("dict", "with"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("global",), _REPEATED_ARGUMENTS, DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("incr",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("info", "exists"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("lappend",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("set",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("unset",), _REPEATED_ARGUMENTS, DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("variable",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||
DynamicCompletionRule(("vwait",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||
# Procedure-taking commands.
|
||||
DynamicCompletionRule(("info", "args"), frozenset({2}), DynamicCompletionKind.PROCEDURE),
|
||||
DynamicCompletionRule(("info", "body"), frozenset({2}), DynamicCompletionKind.PROCEDURE),
|
||||
DynamicCompletionRule(("info", "default"), frozenset({2}), DynamicCompletionKind.PROCEDURE),
|
||||
DynamicCompletionRule(("namespace", "origin"), frozenset({2}), DynamicCompletionKind.PROCEDURE),
|
||||
DynamicCompletionRule(("rename",), frozenset({1}), DynamicCompletionKind.PROCEDURE),
|
||||
# Namespace-taking commands.
|
||||
DynamicCompletionRule(("namespace", "children"), frozenset({2}), DynamicCompletionKind.NAMESPACE),
|
||||
DynamicCompletionRule(
|
||||
("namespace", "delete"),
|
||||
_REPEATED_SUBCOMMAND_ARGUMENTS,
|
||||
DynamicCompletionKind.NAMESPACE,
|
||||
),
|
||||
DynamicCompletionRule(("namespace", "eval"), frozenset({2}), DynamicCompletionKind.NAMESPACE),
|
||||
DynamicCompletionRule(("namespace", "exists"), frozenset({2}), DynamicCompletionKind.NAMESPACE),
|
||||
DynamicCompletionRule(("namespace", "parent"), frozenset({2}), DynamicCompletionKind.NAMESPACE),
|
||||
# Path-taking commands. Source files are narrowed to Tcl while directories
|
||||
# remain visible so users can continue navigating.
|
||||
DynamicCompletionRule(("cd",), frozenset({1}), DynamicCompletionKind.PATH),
|
||||
DynamicCompletionRule(("load",), frozenset({1}), DynamicCompletionKind.PATH, (".dll", ".so", ".dylib")),
|
||||
DynamicCompletionRule(("open",), frozenset({1}), DynamicCompletionKind.PATH),
|
||||
DynamicCompletionRule(("source",), frozenset({1, 3}), DynamicCompletionKind.PATH, (".tcl",)),
|
||||
*(
|
||||
DynamicCompletionRule(
|
||||
("file", subcommand),
|
||||
_REPEATED_SUBCOMMAND_ARGUMENTS,
|
||||
DynamicCompletionKind.PATH,
|
||||
)
|
||||
for subcommand in ("copy", "delete", "join", "link", "mkdir", "rename")
|
||||
),
|
||||
*(
|
||||
DynamicCompletionRule(
|
||||
("file", subcommand),
|
||||
frozenset({2}),
|
||||
DynamicCompletionKind.PATH,
|
||||
)
|
||||
for subcommand in (
|
||||
"atime",
|
||||
"attributes",
|
||||
"dirname",
|
||||
"executable",
|
||||
"exists",
|
||||
"extension",
|
||||
"isdirectory",
|
||||
"isfile",
|
||||
"lstat",
|
||||
"mtime",
|
||||
"nativename",
|
||||
"normalize",
|
||||
"owned",
|
||||
"pathtype",
|
||||
"readable",
|
||||
"readlink",
|
||||
"rootname",
|
||||
"separator",
|
||||
"size",
|
||||
"split",
|
||||
"stat",
|
||||
"system",
|
||||
"tail",
|
||||
"type",
|
||||
"writable",
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _snippet_item(label: str, insert_text: str, detail: str) -> CompletionItem:
|
||||
return CompletionItem(
|
||||
label=label,
|
||||
kind=CompletionItemKind.Snippet,
|
||||
detail=detail,
|
||||
insert_text=insert_text,
|
||||
insert_text_format=InsertTextFormat.Snippet,
|
||||
)
|
||||
|
||||
|
||||
TCL_COMMAND_SNIPPET_ITEMS = {
|
||||
"foreach": _snippet_item(
|
||||
"foreach",
|
||||
"foreach ${1:item} ${2:list} {\n\t${0}\n}",
|
||||
"Tcl foreach loop",
|
||||
),
|
||||
"if": _snippet_item(
|
||||
"if",
|
||||
"if {${1:condition}} {\n\t${0}\n}",
|
||||
"Tcl if block",
|
||||
),
|
||||
"proc": _snippet_item(
|
||||
"proc",
|
||||
"proc ${1:name} {${2:arguments}} {\n\t${0}\n}",
|
||||
"Tcl procedure",
|
||||
),
|
||||
"switch": _snippet_item(
|
||||
"switch",
|
||||
"switch -- ${1:value} {\n\t${2:pattern} {\n\t\t${0}\n\t}\n}",
|
||||
"Tcl switch block",
|
||||
),
|
||||
"try": _snippet_item(
|
||||
"try",
|
||||
"try {\n\t${1}\n} on error {${2:message} ${3:options}} {\n\t${0}\n}",
|
||||
"Tcl try/on error block",
|
||||
),
|
||||
}
|
||||
|
||||
ARGUMENT_SNIPPETS_BY_PATH = {
|
||||
("dict", "for"): _snippet_item(
|
||||
"dict for loop",
|
||||
"{${1:key} ${2:value}} ${3:dictionary} {\n\t${0}\n}",
|
||||
"Arguments and body for dict for",
|
||||
),
|
||||
("foreach",): _snippet_item(
|
||||
"foreach loop",
|
||||
"${1:item} ${2:list} {\n\t${0}\n}",
|
||||
"Arguments and body for foreach",
|
||||
),
|
||||
("if",): _snippet_item(
|
||||
"if block",
|
||||
"{${1:condition}} {\n\t${0}\n}",
|
||||
"Condition and body for if",
|
||||
),
|
||||
("proc",): _snippet_item(
|
||||
"procedure",
|
||||
"${1:name} {${2:arguments}} {\n\t${0}\n}",
|
||||
"Name, arguments, and body for proc",
|
||||
),
|
||||
("switch",): _snippet_item(
|
||||
"switch block",
|
||||
"-- ${1:value} {\n\t${2:pattern} {\n\t\t${0}\n\t}\n}",
|
||||
"Value, patterns, and body for switch",
|
||||
),
|
||||
("try",): _snippet_item(
|
||||
"try/on error block",
|
||||
"{\n\t${1}\n} on error {${2:message} ${3:options}} {\n\t${0}\n}",
|
||||
"Body and error handler for try",
|
||||
),
|
||||
}
|
||||
|
||||
SUBCOMMAND_SNIPPET_ITEMS = {
|
||||
("dict", "for"): _snippet_item(
|
||||
"for",
|
||||
"for {${1:key} ${2:value}} ${3:dictionary} {\n\t${0}\n}",
|
||||
"dict for loop",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
TCL_COMMAND_NAMES = tuple(
|
||||
sorted({path[0] for path in SUBCOMMANDS_BY_PATH} | {path[0] for path in OPTIONS_BY_PATH} | set(TCL_COMMAND_SNIPPET_ITEMS) | {rule.path[0] for rule in DYNAMIC_COMPLETION_RULES})
|
||||
)
|
||||
|
||||
TCL_COMMAND_ITEMS = tuple(
|
||||
TCL_COMMAND_SNIPPET_ITEMS.get(
|
||||
command,
|
||||
CompletionItem(
|
||||
label=command,
|
||||
kind=CompletionItemKind.Function,
|
||||
detail="Tcl command",
|
||||
insert_text=command,
|
||||
),
|
||||
)
|
||||
for command in TCL_COMMAND_NAMES
|
||||
)
|
||||
|
||||
|
||||
def line_prefix_at_position(source_lines: Sequence[str], position: Position) -> str | None:
|
||||
"""Return the current line before an LSP UTF-16 position."""
|
||||
|
||||
if position.line < 0 or position.line >= len(source_lines):
|
||||
return None
|
||||
|
||||
line = source_lines[position.line]
|
||||
codepoint_offset = _codepoint_offset(line, position.character)
|
||||
if codepoint_offset is None:
|
||||
return None
|
||||
return line[:codepoint_offset]
|
||||
|
||||
|
||||
def tcl_argument_completion(source_lines: Sequence[str], position: Position) -> TclArgumentCompletion | None:
|
||||
"""Describe static and dynamic argument completion at ``position``.
|
||||
|
||||
``None`` means that the cursor is not at a command-specific completion
|
||||
position and the caller should fall back to normal symbol completion.
|
||||
"""
|
||||
|
||||
line_prefix = line_prefix_at_position(source_lines, position)
|
||||
if line_prefix is None:
|
||||
return None
|
||||
|
||||
segment = _current_command_segment(line_prefix)
|
||||
if not segment.strip() or segment.lstrip().startswith("#"):
|
||||
return None
|
||||
|
||||
words = _tokenize_command_segment(segment)
|
||||
if not words:
|
||||
return None
|
||||
|
||||
words[0] = words[0].removeprefix("::")
|
||||
active_index = len(words) - 1
|
||||
active_prefix = words[active_index]
|
||||
completed_path = tuple(words[:active_index])
|
||||
dynamic_completion = _dynamic_completion(words, active_index, active_prefix)
|
||||
|
||||
subcommands = SUBCOMMANDS_BY_PATH.get(completed_path)
|
||||
if subcommands is not None:
|
||||
items = tuple(
|
||||
SUBCOMMAND_SNIPPET_ITEMS.get(
|
||||
(*completed_path, label),
|
||||
CompletionItem(
|
||||
label=label,
|
||||
kind=CompletionItemKind.EnumMember,
|
||||
detail=f"{' '.join(completed_path)} subcommand",
|
||||
insert_text=label,
|
||||
),
|
||||
)
|
||||
for label in subcommands
|
||||
)
|
||||
return TclArgumentCompletion(items=items, active_prefix=active_prefix)
|
||||
|
||||
argument_snippet = ARGUMENT_SNIPPETS_BY_PATH.get(completed_path)
|
||||
argument_items = (argument_snippet,) if argument_snippet is not None else ()
|
||||
|
||||
for (path, argument_index), values in VALUES_BY_POSITION.items():
|
||||
if active_index == argument_index and tuple(words[: len(path)]) == path:
|
||||
return TclArgumentCompletion(
|
||||
items=_completion_items(
|
||||
values,
|
||||
CompletionItemKind.Value,
|
||||
f"{' '.join(path)} value",
|
||||
),
|
||||
active_prefix=active_prefix,
|
||||
)
|
||||
|
||||
for path in sorted(OPTIONS_BY_PATH, key=len, reverse=True):
|
||||
if active_index < len(path) or tuple(words[: len(path)]) != path:
|
||||
continue
|
||||
|
||||
option_completion = _option_completion(
|
||||
path,
|
||||
OPTIONS_BY_PATH[path],
|
||||
words[len(path) : active_index],
|
||||
active_prefix,
|
||||
)
|
||||
if option_completion is not None:
|
||||
if active_prefix.startswith("-") or (path == ("unset",) and active_index == 1 and not active_prefix):
|
||||
return option_completion
|
||||
return _merge_dynamic_completion(
|
||||
(*argument_items, *option_completion.items),
|
||||
dynamic_completion,
|
||||
active_prefix,
|
||||
)
|
||||
|
||||
if argument_items:
|
||||
return _merge_dynamic_completion(argument_items, dynamic_completion, active_prefix)
|
||||
return dynamic_completion
|
||||
|
||||
|
||||
def _option_completion(
|
||||
path: tuple[str, ...],
|
||||
options: tuple[OptionSpec, ...],
|
||||
completed_arguments: Sequence[str],
|
||||
active_prefix: str,
|
||||
) -> TclArgumentCompletion | None:
|
||||
option_by_label = {option.label: option for option in options}
|
||||
used_options: set[str] = set()
|
||||
argument_index = 0
|
||||
|
||||
while argument_index < len(completed_arguments):
|
||||
argument = completed_arguments[argument_index]
|
||||
if argument == "--":
|
||||
return None
|
||||
|
||||
option = option_by_label.get(argument)
|
||||
if option is None:
|
||||
# Tcl options precede normal operands for the commands covered here.
|
||||
return None
|
||||
|
||||
used_options.add(option.label)
|
||||
argument_index += 1
|
||||
if not option.takes_value:
|
||||
continue
|
||||
|
||||
if argument_index >= len(completed_arguments):
|
||||
if option.values:
|
||||
return TclArgumentCompletion(
|
||||
items=_completion_items(
|
||||
option.values,
|
||||
CompletionItemKind.Value,
|
||||
f"{option.label} value",
|
||||
),
|
||||
active_prefix=active_prefix,
|
||||
)
|
||||
return None
|
||||
argument_index += 1
|
||||
|
||||
if active_prefix and not active_prefix.startswith("-"):
|
||||
return None
|
||||
|
||||
remaining_options = tuple(option.label for option in options if option.label not in used_options)
|
||||
return TclArgumentCompletion(
|
||||
items=_completion_items(
|
||||
remaining_options,
|
||||
CompletionItemKind.Keyword,
|
||||
f"{' '.join(path)} option",
|
||||
),
|
||||
active_prefix=active_prefix,
|
||||
)
|
||||
|
||||
|
||||
def _dynamic_completion(words: Sequence[str], active_index: int, active_prefix: str) -> TclArgumentCompletion | None:
|
||||
for rule in sorted(DYNAMIC_COMPLETION_RULES, key=lambda item: len(item.path), reverse=True):
|
||||
if active_index in rule.argument_indices and tuple(words[: len(rule.path)]) == rule.path:
|
||||
return TclArgumentCompletion(
|
||||
dynamic_kind=rule.kind,
|
||||
active_prefix=active_prefix,
|
||||
path_extensions=rule.path_extensions,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _merge_dynamic_completion(
|
||||
items: Sequence[CompletionItem],
|
||||
dynamic_completion: TclArgumentCompletion | None,
|
||||
active_prefix: str,
|
||||
) -> TclArgumentCompletion:
|
||||
if dynamic_completion is None:
|
||||
return TclArgumentCompletion(items=tuple(items), active_prefix=active_prefix)
|
||||
return TclArgumentCompletion(
|
||||
items=tuple(items),
|
||||
dynamic_kind=dynamic_completion.dynamic_kind,
|
||||
active_prefix=active_prefix,
|
||||
path_extensions=dynamic_completion.path_extensions,
|
||||
)
|
||||
|
||||
|
||||
def _completion_items(labels: Sequence[str], kind: CompletionItemKind, detail: str) -> tuple[CompletionItem, ...]:
|
||||
return tuple(
|
||||
CompletionItem(
|
||||
label=label,
|
||||
kind=kind,
|
||||
detail=detail,
|
||||
insert_text=label,
|
||||
)
|
||||
for label in labels
|
||||
)
|
||||
|
||||
|
||||
def path_completion_items(
|
||||
base_directory: Path,
|
||||
completion: TclArgumentCompletion,
|
||||
position: Position,
|
||||
*,
|
||||
limit: int = 200,
|
||||
) -> tuple[CompletionItem, ...]:
|
||||
"""Complete one filesystem path relative to the current Tcl document."""
|
||||
|
||||
raw_prefix = completion.active_prefix
|
||||
separator_index = max(raw_prefix.rfind("/"), raw_prefix.rfind("\\"))
|
||||
typed_directory = raw_prefix[: separator_index + 1]
|
||||
name_prefix = raw_prefix[separator_index + 1 :]
|
||||
normalized_directory = typed_directory.replace("\\", "/")
|
||||
filesystem_directory = Path(normalized_directory)
|
||||
if not filesystem_directory.is_absolute():
|
||||
filesystem_directory = base_directory / filesystem_directory
|
||||
|
||||
try:
|
||||
entries = sorted(
|
||||
filesystem_directory.iterdir(),
|
||||
key=lambda entry: (not entry.is_dir(), entry.name.casefold()),
|
||||
)
|
||||
except (OSError, ValueError):
|
||||
return ()
|
||||
|
||||
allowed_extensions = {extension.casefold() for extension in completion.path_extensions}
|
||||
replace_start = max(
|
||||
0,
|
||||
position.character - len(raw_prefix.encode("utf-16-le")) // 2,
|
||||
)
|
||||
replace_range = Range(
|
||||
start=Position(line=position.line, character=replace_start),
|
||||
end=position,
|
||||
)
|
||||
items: list[CompletionItem] = []
|
||||
|
||||
for entry in entries:
|
||||
if not entry.name.casefold().startswith(name_prefix.casefold()):
|
||||
continue
|
||||
try:
|
||||
is_directory = entry.is_dir()
|
||||
except OSError:
|
||||
continue
|
||||
if not is_directory and allowed_extensions and entry.suffix.casefold() not in allowed_extensions:
|
||||
continue
|
||||
|
||||
escaped_name = "".join(f"\\{character}" if character.isspace() else character for character in entry.name)
|
||||
new_text = f"{normalized_directory}{escaped_name}"
|
||||
if is_directory:
|
||||
new_text += "/"
|
||||
items.append(
|
||||
CompletionItem(
|
||||
label=new_text,
|
||||
kind=(CompletionItemKind.Folder if is_directory else CompletionItemKind.File),
|
||||
detail="Directory" if is_directory else "File",
|
||||
text_edit=TextEdit(range=replace_range, new_text=new_text),
|
||||
)
|
||||
)
|
||||
if len(items) >= limit:
|
||||
break
|
||||
|
||||
return tuple(items)
|
||||
|
||||
|
||||
def _current_command_segment(line_prefix: str) -> str:
|
||||
"""Select the innermost unfinished command from a line prefix."""
|
||||
|
||||
brace_starts: list[int] = []
|
||||
bracket_starts: list[int] = []
|
||||
last_command_delimiter = -1
|
||||
in_quote = False
|
||||
escaped = False
|
||||
|
||||
for index, char in enumerate(line_prefix):
|
||||
if escaped:
|
||||
escaped = False
|
||||
continue
|
||||
if char == "\\":
|
||||
escaped = True
|
||||
continue
|
||||
|
||||
if char == '"' and not brace_starts:
|
||||
in_quote = not in_quote
|
||||
continue
|
||||
|
||||
if not in_quote:
|
||||
if char == "{":
|
||||
brace_starts.append(index)
|
||||
continue
|
||||
if char == "}" and brace_starts:
|
||||
brace_starts.pop()
|
||||
continue
|
||||
|
||||
if not brace_starts:
|
||||
if char == "[":
|
||||
bracket_starts.append(index)
|
||||
continue
|
||||
if char == "]" and bracket_starts:
|
||||
bracket_starts.pop()
|
||||
continue
|
||||
if char == ";" and not in_quote:
|
||||
last_command_delimiter = index
|
||||
|
||||
starts = [last_command_delimiter + 1]
|
||||
if brace_starts:
|
||||
starts.append(brace_starts[-1] + 1)
|
||||
if bracket_starts:
|
||||
starts.append(bracket_starts[-1] + 1)
|
||||
start = max(starts)
|
||||
if brace_starts and start == brace_starts[-1] + 1:
|
||||
# An unfinished braced body or expression can itself contain commands
|
||||
# (for example, ``if {[string compare ...``). Scan that inner context
|
||||
# independently, while keeping completed braced arguments opaque.
|
||||
return _current_command_segment(line_prefix[start:])
|
||||
return line_prefix[start:]
|
||||
|
||||
|
||||
def _tokenize_command_segment(segment: str) -> list[str]:
|
||||
words: list[str] = []
|
||||
current: list[str] = []
|
||||
brace_depth = 0
|
||||
in_quote = False
|
||||
escaped = False
|
||||
ended_with_separator = False
|
||||
word_started = False
|
||||
|
||||
for char in segment:
|
||||
if escaped:
|
||||
current.append(char)
|
||||
escaped = False
|
||||
ended_with_separator = False
|
||||
word_started = True
|
||||
continue
|
||||
if char == "\\":
|
||||
current.append(char)
|
||||
escaped = True
|
||||
ended_with_separator = False
|
||||
word_started = True
|
||||
continue
|
||||
if char == '"' and brace_depth == 0:
|
||||
in_quote = not in_quote
|
||||
ended_with_separator = False
|
||||
word_started = True
|
||||
continue
|
||||
if not in_quote and char == "{":
|
||||
brace_depth += 1
|
||||
ended_with_separator = False
|
||||
word_started = True
|
||||
continue
|
||||
if not in_quote and char == "}" and brace_depth:
|
||||
brace_depth -= 1
|
||||
ended_with_separator = False
|
||||
continue
|
||||
if char.isspace() and not in_quote and brace_depth == 0:
|
||||
if current or word_started:
|
||||
words.append("".join(current))
|
||||
current = []
|
||||
word_started = False
|
||||
ended_with_separator = bool(words)
|
||||
continue
|
||||
|
||||
current.append(char)
|
||||
ended_with_separator = False
|
||||
word_started = True
|
||||
|
||||
if current or word_started:
|
||||
words.append("".join(current))
|
||||
elif ended_with_separator:
|
||||
words.append("")
|
||||
return words
|
||||
|
||||
|
||||
def _codepoint_offset(text: str, utf16_offset: int) -> int | None:
|
||||
if utf16_offset < 0:
|
||||
return None
|
||||
|
||||
consumed = 0
|
||||
for index, char in enumerate(text):
|
||||
if consumed == utf16_offset:
|
||||
return index
|
||||
consumed += 2 if ord(char) > 0xFFFF else 1
|
||||
if consumed > utf16_offset:
|
||||
return None
|
||||
|
||||
if consumed == utf16_offset:
|
||||
return len(text)
|
||||
return None
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Parameter presentation for statically resolved TclOO calls."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
from tclint.lexer import TclSyntaxError
|
||||
from tclint.syntax_tree import BracedWord, Command
|
||||
|
||||
from tools.parser import CustomParser
|
||||
from tools.signature_help import _active_argument, _contains_cursor
|
||||
from tools.tcloo_completion import resolved_method_calls
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MethodParameter:
|
||||
name: str
|
||||
label: str
|
||||
variadic: bool = False
|
||||
|
||||
|
||||
def method_parameters(parameters: str) -> list[MethodParameter]:
|
||||
parser = CustomParser()
|
||||
try:
|
||||
words = parser.parse_list(BracedWord(parameters, pos=(1, 1))).children
|
||||
result = []
|
||||
for index, word in enumerate(words):
|
||||
parts = parser.parse_list(word).children
|
||||
if not parts or len(parts) > 2:
|
||||
return []
|
||||
name = parts[0].contents
|
||||
if name is None:
|
||||
return []
|
||||
variadic = name == "args" and len(parts) == 1 and index == len(words) - 1
|
||||
label = "{" + word.contents + "}" if len(parts) == 2 else name
|
||||
result.append(MethodParameter(name, label, variadic))
|
||||
return result
|
||||
except TclSyntaxError:
|
||||
return []
|
||||
|
||||
|
||||
def method_signature_help(source: str, position: lsp.Position, external_classes=None) -> lsp.SignatureHelp | None:
|
||||
lines = source.split("\n")
|
||||
if position.line >= len(lines):
|
||||
return None
|
||||
# AST columns are codepoints; LSP columns are UTF-16 code units.
|
||||
prefix = lines[position.line].encode("utf-16-le")[:position.character * 2].decode("utf-16-le", errors="ignore")
|
||||
cursor = (position.line, len(prefix))
|
||||
candidates = [call for call in resolved_method_calls(source, external_classes)
|
||||
if _contains_cursor(call.command, lines, cursor)]
|
||||
if not candidates:
|
||||
return None
|
||||
call = max(candidates, key=lambda candidate: candidate.command.pos)
|
||||
|
||||
def nested_active(node):
|
||||
return any(
|
||||
isinstance(child, Command) and _contains_cursor(child, lines, cursor)
|
||||
or nested_active(child)
|
||||
for child in node.children
|
||||
)
|
||||
|
||||
# Let the inner command's own signature provider handle its arguments.
|
||||
if nested_active(call.command):
|
||||
return None
|
||||
argument = _active_argument(call.command, cursor) - call.argument_offset
|
||||
if argument < 0:
|
||||
return None
|
||||
parameters = method_parameters(call.parameters)
|
||||
label = call.label
|
||||
infos = []
|
||||
for parameter in parameters:
|
||||
label += " "
|
||||
start = len(label.encode("utf-16-le")) // 2
|
||||
label += parameter.label
|
||||
infos.append(lsp.ParameterInformation(label=(start, len(label.encode("utf-16-le")) // 2)))
|
||||
active = min(argument, len(parameters) - 1) if parameters else None
|
||||
return lsp.SignatureHelp(
|
||||
signatures=[lsp.SignatureInformation(label=label, parameters=infos, active_parameter=active)],
|
||||
active_signature=0, active_parameter=active,
|
||||
)
|
||||
@@ -0,0 +1,263 @@
|
||||
"""Static TclOO inference using local and indexed classes, without executing Tcl."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
import re
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
from tclint.lexer import TclSyntaxError
|
||||
from tclint.syntax_tree import BracedWord, Command, CommandSub, Script, VarSub
|
||||
|
||||
from tools.parser import CustomParser
|
||||
from tools.tcl_command_completion import line_prefix_at_position
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClassInfo:
|
||||
methods: dict[str, tuple[str, Script | None]] = field(default_factory=dict)
|
||||
namespace: str = ""
|
||||
constructor: str = ""
|
||||
definition: lsp.Location | None = None
|
||||
method_definitions: dict[str, lsp.Location] = field(default_factory=dict)
|
||||
constructor_definition: lsp.Location | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MethodCall:
|
||||
command: Command
|
||||
label: str
|
||||
parameters: str
|
||||
argument_offset: int = 1
|
||||
definition: lsp.Location | None = None
|
||||
|
||||
|
||||
def name_location(node, uri, source):
|
||||
"""Locate the literal name, excluding braces/quotes, using UTF-16 columns."""
|
||||
if not uri or node.contents is None or node.contents_pos is None:
|
||||
return None
|
||||
line, column = node.contents_pos
|
||||
lines = source.splitlines()
|
||||
prefix = lines[line - 1][:column - 1] if line <= len(lines) else ""
|
||||
start = len(prefix.encode("utf-16-le")) // 2
|
||||
end = start + len(node.contents.encode("utf-16-le")) // 2
|
||||
return lsp.Location(uri=uri, range=lsp.Range(
|
||||
start=lsp.Position(line=line - 1, character=start),
|
||||
end=lsp.Position(line=line - 1, character=end)))
|
||||
|
||||
|
||||
def parse_completion_source(source, pos=None):
|
||||
# Complete open delimiters while editing; never evaluate the user's code.
|
||||
for _ in range(16):
|
||||
try:
|
||||
return CustomParser().parse(source, pos=pos)
|
||||
except TclSyntaxError as error:
|
||||
message = str(error)
|
||||
closing = next((char for text, char in (
|
||||
("end of command substitution", "]"),
|
||||
("match for brace", "}"), ("match for quote", '"'),
|
||||
) if text in message), None)
|
||||
if closing is None:
|
||||
return None
|
||||
source += closing
|
||||
return None
|
||||
|
||||
|
||||
def _body(node):
|
||||
if isinstance(node, Script):
|
||||
return node
|
||||
if isinstance(node, BracedWord):
|
||||
return parse_completion_source(node.contents, node.contents_pos)
|
||||
return None
|
||||
|
||||
|
||||
def _qualified(name, namespace):
|
||||
return name if name.startswith("::") else f"{namespace}::{name}"
|
||||
|
||||
|
||||
def tcloo_completions(
|
||||
source_lines: Sequence[str], position: lsp.Position, external_classes=None,
|
||||
) -> list[lsp.CompletionItem] | None:
|
||||
"""Return receiver-specific methods, or None outside a known OO context."""
|
||||
prefix = line_prefix_at_position(source_lines, position)
|
||||
if prefix is None:
|
||||
return None
|
||||
match = re.search(r"[\w:]*$", prefix)
|
||||
typed = match.group()
|
||||
# Only a method word, never a variable substitution or method argument.
|
||||
word_start = len(prefix) - len(typed)
|
||||
if word_start == 0 or prefix[word_start - 1] not in " \t":
|
||||
return None
|
||||
marker = "__nx_tcloo_completion_cursor__"
|
||||
lines = list(source_lines)
|
||||
suffix = lines[position.line][len(prefix):]
|
||||
remaining = re.match(r"[\w:]*", suffix).group()
|
||||
lines[position.line] = prefix + marker + suffix[len(remaining):]
|
||||
tree = parse_completion_source("\n".join(lines))
|
||||
if tree is None:
|
||||
return None
|
||||
classes, result, _ = _analyze(tree, typed, marker, external_classes)
|
||||
if result is None:
|
||||
return None
|
||||
cls, internal = result
|
||||
methods = classes[cls].methods if cls else {"new": ("args", None), "create": ("name args", None)}
|
||||
methods = dict(methods)
|
||||
if cls:
|
||||
methods.setdefault("destroy", ("", None))
|
||||
suffix = source_lines[position.line][len(prefix):]
|
||||
remaining = re.match(r"[\w:]*", suffix).group()
|
||||
start = position.character - len(typed.encode("utf-16-le")) // 2
|
||||
end = position.character + len(remaining.encode("utf-16-le")) // 2
|
||||
return [lsp.CompletionItem(
|
||||
label=name, kind=lsp.CompletionItemKind.Method,
|
||||
detail=f"{cls or 'class'} {name} {signature}".rstrip(),
|
||||
text_edit=lsp.TextEdit(range=lsp.Range(
|
||||
start=lsp.Position(line=position.line, character=start),
|
||||
end=lsp.Position(line=position.line, character=end)), new_text=name),
|
||||
) for name, (signature, _) in sorted(methods.items())
|
||||
if name.startswith(typed) and (internal or not name.startswith("_") and not name[:1].isupper())]
|
||||
|
||||
|
||||
def _collect_classes(tree, classes, uri=None, source=""):
|
||||
contexts = []
|
||||
def collect(script, namespace=""):
|
||||
if script is None:
|
||||
return
|
||||
for cmd in script.children:
|
||||
if not isinstance(cmd, Command):
|
||||
continue
|
||||
args = cmd.args
|
||||
routine = (cmd.routine.contents or "").removeprefix("::")
|
||||
if routine == "namespace" and len(args) == 3 and args[0].contents == "eval" and args[1].contents:
|
||||
collect(_body(args[2]), _qualified(args[1].contents, namespace))
|
||||
elif routine == "oo::class" and len(args) == 3 and args[0].contents == "create" and args[1].contents:
|
||||
name = _qualified(args[1].contents, namespace)
|
||||
info = ClassInfo(namespace=namespace)
|
||||
info.definition = name_location(args[1], uri, source)
|
||||
classes[name] = info
|
||||
body = _body(args[2])
|
||||
if body is None:
|
||||
continue
|
||||
for method in body.children:
|
||||
if not isinstance(method, Command):
|
||||
continue
|
||||
ma = method.args
|
||||
if method.routine.contents == "method" and len(ma) == 3 and ma[0].contents:
|
||||
method_body = _body(ma[2])
|
||||
info.methods[ma[0].contents] = (ma[1].contents or "", method_body)
|
||||
location = name_location(ma[0], uri, source)
|
||||
if location is not None:
|
||||
info.method_definitions[ma[0].contents] = location
|
||||
contexts.append((method_body, namespace, name))
|
||||
elif method.routine.contents in {"constructor", "destructor"} and ma:
|
||||
if method.routine.contents == "constructor" and len(ma) == 2:
|
||||
info.constructor = ma[0].contents or ""
|
||||
info.constructor_definition = name_location(method.routine, uri, source)
|
||||
contexts.append((_body(ma[-1]), namespace, name))
|
||||
|
||||
collect(tree)
|
||||
return contexts
|
||||
|
||||
|
||||
def indexed_classes(tree, uri=None, source=""):
|
||||
classes = {}
|
||||
_collect_classes(tree, classes, uri, source)
|
||||
return classes
|
||||
|
||||
|
||||
def _analyze(tree, typed="", marker="", external_classes=None, uri=None, source=""):
|
||||
classes = dict(external_classes or {})
|
||||
calls = []
|
||||
|
||||
contexts = _collect_classes(tree, classes, uri, source)
|
||||
result = None
|
||||
|
||||
def receiver(node, env, objects, namespace, owner, depth=0):
|
||||
if depth > 12:
|
||||
return None
|
||||
if isinstance(node, VarSub):
|
||||
return env.get(node.value)
|
||||
if isinstance(node, CommandSub) and len(node.children) == 1:
|
||||
return returned(node.children[0], env, objects, namespace, owner, depth + 1)
|
||||
name = node.contents
|
||||
return objects.get(_qualified(name, namespace)) if name else None
|
||||
|
||||
def returned(cmd, env, objects, namespace, owner, depth=0):
|
||||
if not isinstance(cmd, Command) or depth > 12:
|
||||
return None
|
||||
args = cmd.args
|
||||
name = cmd.routine.contents
|
||||
if name == "self" and not args:
|
||||
return owner
|
||||
qualified = _qualified(name, namespace) if name else None
|
||||
if qualified in classes and args and args[0].contents in {"new", "create"}:
|
||||
return qualified
|
||||
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner, depth + 1)
|
||||
if cls not in classes or not args:
|
||||
return None
|
||||
method = classes[cls].methods.get(args[0].contents)
|
||||
if method is None or method[1] is None:
|
||||
return None
|
||||
# Only infer unconditional final returns; conditional results stay unknown.
|
||||
commands = [c for c in method[1].children if isinstance(c, Command)]
|
||||
if commands and commands[-1].routine.contents == "return" and len(commands[-1].args) == 1:
|
||||
return receiver(commands[-1].args[0], {}, objects, classes[cls].namespace, cls, depth + 1)
|
||||
return None
|
||||
|
||||
def walk(script, env, objects, namespace="", owner=None):
|
||||
nonlocal result
|
||||
if script is None:
|
||||
return
|
||||
for cmd in script.children:
|
||||
if not isinstance(cmd, Command):
|
||||
continue
|
||||
args = cmd.args
|
||||
name = cmd.routine.contents
|
||||
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner)
|
||||
method_name = args[0].contents if args else None
|
||||
if cls in classes and method_name in classes[cls].methods:
|
||||
calls.append(MethodCall(cmd, f"{cls} {method_name}", classes[cls].methods[method_name][0],
|
||||
definition=classes[cls].method_definitions.get(method_name)))
|
||||
elif name and _qualified(name, namespace) in classes and method_name in {"new", "create"}:
|
||||
cls = _qualified(name, namespace)
|
||||
parameters = classes[cls].constructor
|
||||
if method_name == "create":
|
||||
parameters = "objectName " + parameters
|
||||
calls.append(MethodCall(cmd, f"{cls} {method_name}", parameters,
|
||||
definition=classes[cls].constructor_definition or classes[cls].definition))
|
||||
if marker and args and args[0].contents == typed + marker:
|
||||
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner)
|
||||
if cls in classes:
|
||||
result = (cls, name == "my")
|
||||
elif name and _qualified(name, namespace) in classes:
|
||||
result = (None, False)
|
||||
return
|
||||
# Command substitutions can contain the completion receiver.
|
||||
for node in cmd.children:
|
||||
if isinstance(node, CommandSub):
|
||||
walk(node, env, objects, namespace, owner)
|
||||
if name == "set" and len(args) == 2 and args[0].contents:
|
||||
env[args[0].contents] = receiver(args[1], env, objects, namespace, owner)
|
||||
elif name == "unset":
|
||||
for arg in args:
|
||||
env.pop(arg.contents, None)
|
||||
elif name == "proc" and len(args) == 3:
|
||||
walk(_body(args[2]), {}, objects.copy(), namespace)
|
||||
elif name == "namespace" and len(args) == 3 and args[0].contents == "eval" and args[1].contents:
|
||||
walk(_body(args[2]), {}, objects, _qualified(args[1].contents, namespace))
|
||||
elif name and _qualified(name, namespace) in classes and len(args) >= 2 and args[0].contents == "create" and args[1].contents:
|
||||
objects[_qualified(args[1].contents, namespace)] = _qualified(name, namespace)
|
||||
else:
|
||||
for arg in args:
|
||||
if isinstance(arg, Script):
|
||||
# Branch-local facts are not propagated beyond the branch.
|
||||
walk(arg, env.copy(), objects.copy(), namespace, owner)
|
||||
|
||||
walk(tree, {}, {})
|
||||
for body, namespace, owner in contexts:
|
||||
walk(body, {}, {}, namespace, owner)
|
||||
return classes, result, calls
|
||||
|
||||
|
||||
def resolved_method_calls(source, external_classes=None):
|
||||
tree = parse_completion_source(source)
|
||||
return _analyze(tree, external_classes=external_classes)[2] if tree is not None else []
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Definition targets for literal TclOO classes and resolved method calls."""
|
||||
|
||||
from tools.tcloo_completion import _analyze, name_location, parse_completion_source
|
||||
from tools.tcloo_symbols import class_symbols
|
||||
|
||||
|
||||
def tcloo_definition(source, uri, position, external_classes=None):
|
||||
tree = parse_completion_source(source)
|
||||
if tree is None:
|
||||
return None
|
||||
classes, _, calls = _analyze(tree, external_classes=external_classes, uri=uri, source=source)
|
||||
|
||||
def contains(location):
|
||||
if location is None:
|
||||
return False
|
||||
start, end = location.range.start, location.range.end
|
||||
return (start.line, start.character) <= (position.line, position.character) < (end.line, end.character)
|
||||
|
||||
targets = {}
|
||||
declarations, references = class_symbols(tree, classes, targets)
|
||||
for node in references:
|
||||
if contains(name_location(node, uri, source)):
|
||||
return classes[targets[node.pos]].definition
|
||||
for name, node in declarations.items():
|
||||
if contains(name_location(node, uri, source)):
|
||||
return classes[name].definition
|
||||
for call in calls:
|
||||
if call.command.args and contains(name_location(call.command.args[0], uri, source)):
|
||||
return call.definition
|
||||
# F12 on a declaration itself should stay on that declaration.
|
||||
for info in classes.values():
|
||||
for location in [*info.method_definitions.values(), info.constructor_definition]:
|
||||
if location is not None and location.uri == uri and contains(location):
|
||||
return location
|
||||
return None
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Class declarations and references shared by completion and highlighting."""
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
from tclint.syntax_tree import Command
|
||||
|
||||
from tools.tcloo_completion import _body, _qualified
|
||||
|
||||
|
||||
def class_symbols(tree, external_classes=None, reference_targets=None):
|
||||
"""Return qualified class declarations and statically resolved name nodes."""
|
||||
declarations = {}
|
||||
commands = []
|
||||
|
||||
def walk(node, namespace="", in_class=False):
|
||||
if node is None:
|
||||
return
|
||||
if isinstance(node, Command):
|
||||
args = node.args
|
||||
name = (node.routine.contents or "").removeprefix("::")
|
||||
commands.append((node, namespace))
|
||||
if (name == "namespace" and len(args) == 3
|
||||
and args[0].contents == "eval" and args[1].contents):
|
||||
walk(_body(args[2]), _qualified(args[1].contents, namespace), in_class)
|
||||
return
|
||||
if (name == "oo::class" and len(args) == 3
|
||||
and args[0].contents == "create" and args[1].contents):
|
||||
declarations[_qualified(args[1].contents, namespace)] = args[1]
|
||||
walk(_body(args[2]), namespace, True)
|
||||
return
|
||||
if in_class and name in {"method", "constructor", "destructor"} and args:
|
||||
walk(_body(args[-1]), namespace, True)
|
||||
return
|
||||
for child in node.children:
|
||||
walk(child, namespace, in_class)
|
||||
|
||||
walk(tree)
|
||||
known_classes = set(external_classes or ()) | declarations.keys()
|
||||
references = []
|
||||
for command, namespace in commands:
|
||||
name = command.routine.contents
|
||||
target = next((candidate for candidate in (
|
||||
_qualified(name, namespace), _qualified(name, ""),
|
||||
) if candidate in known_classes), None) if name else None
|
||||
if target:
|
||||
references.append(command.routine)
|
||||
if reference_targets is not None:
|
||||
reference_targets[command.routine.pos] = target
|
||||
return declarations, references
|
||||
|
||||
|
||||
def class_completion_items(tree):
|
||||
declarations, _ = class_symbols(tree)
|
||||
return [lsp.CompletionItem(
|
||||
label=name.removeprefix("::"),
|
||||
kind=lsp.CompletionItemKind.Class,
|
||||
detail=f"TclOO class {name}",
|
||||
) for name in sorted(declarations)]
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Set
|
||||
|
||||
from tclint.syntax_tree import Command, Node, Script
|
||||
from tclint.syntax_tree import List as TclList
|
||||
from tools.variable_names import variable_name
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -18,13 +19,14 @@ def _normalize_var_name(raw_name: str | None) -> str | None:
|
||||
return None
|
||||
|
||||
base = raw_name.split("(", 1)[0]
|
||||
if base.startswith("::"):
|
||||
base = base[2:]
|
||||
base = base.removeprefix("::")
|
||||
|
||||
return base or None
|
||||
|
||||
|
||||
def build_variable_index(source: str, tree: Node | None = None) -> tuple[Set[str], Dict[str, Set[str]], List[ProcRange]]:
|
||||
def build_variable_index(
|
||||
source: str, tree: Node | None = None
|
||||
) -> tuple[set[str], dict[str, set[str]], list[ProcRange]]:
|
||||
"""
|
||||
Parse Tcl source text and build:
|
||||
- globals: set of variable names considered global suggestions
|
||||
@@ -40,9 +42,9 @@ def build_variable_index(source: str, tree: Node | None = None) -> tuple[Set[str
|
||||
"""
|
||||
_ = source # Kept for signature compatibility with callers.
|
||||
|
||||
globals_set: Set[str] = set()
|
||||
procs: Dict[str, Set[str]] = {}
|
||||
proc_ranges: List[ProcRange] = []
|
||||
globals_set: set[str] = set()
|
||||
procs: dict[str, set[str]] = {}
|
||||
proc_ranges: list[ProcRange] = []
|
||||
|
||||
if tree is None:
|
||||
return globals_set, procs, proc_ranges
|
||||
@@ -54,7 +56,16 @@ def build_variable_index(source: str, tree: Node | None = None) -> tuple[Set[str
|
||||
if routine == "proc" and len(node.args) >= 3 and isinstance(node.args[2], Script):
|
||||
proc_name = _normalize_var_name(getattr(node.args[0], "contents", None))
|
||||
if proc_name is not None:
|
||||
procs.setdefault(proc_name, set())
|
||||
local_variables = procs.setdefault(proc_name, set())
|
||||
for parameter in getattr(node.args[1], "children", []):
|
||||
parameter_node = parameter
|
||||
if isinstance(parameter, TclList) and parameter.children:
|
||||
parameter_node = parameter.children[0]
|
||||
parameter_name = _normalize_var_name(
|
||||
getattr(parameter_node, "contents", None)
|
||||
)
|
||||
if parameter_name is not None:
|
||||
local_variables.add(parameter_name)
|
||||
proc_ranges.append(
|
||||
ProcRange(
|
||||
name=proc_name,
|
||||
@@ -71,7 +82,7 @@ def build_variable_index(source: str, tree: Node | None = None) -> tuple[Set[str
|
||||
return
|
||||
|
||||
if routine == "set" and node.args:
|
||||
raw_name = getattr(node.args[0], "contents", None)
|
||||
raw_name = variable_name(node.args[0])
|
||||
base = _normalize_var_name(raw_name)
|
||||
if base is not None:
|
||||
if raw_name and raw_name.startswith("::"):
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Extract statically known variable names without evaluating Tcl substitutions."""
|
||||
|
||||
from tclint.syntax_tree import BareWord, CompoundBareWord, Node, QuotedWord, VarSub
|
||||
|
||||
|
||||
def variable_name(node: Node) -> str | None:
|
||||
contents = node.contents
|
||||
if isinstance(contents, str):
|
||||
return contents
|
||||
if isinstance(node, (CompoundBareWord, QuotedWord)) and node.children:
|
||||
first = node.children[0]
|
||||
last = node.children[-1]
|
||||
if (
|
||||
isinstance(first, BareWord)
|
||||
and isinstance(last, BareWord)
|
||||
and "(" in first.value
|
||||
and last.value.endswith(")")
|
||||
):
|
||||
# Substitutions in an array index do not change the array's name.
|
||||
return first.value.split("(", 1)[0] or None
|
||||
return None
|
||||
|
||||
|
||||
def array_key_parts(node: Node, *, preserve_variables: bool = False) -> tuple[str | None, ...]:
|
||||
"""Keep comma-separated literal index components; substitutions are unknown."""
|
||||
contents = node.contents
|
||||
if not isinstance(contents, str):
|
||||
if not isinstance(node, (CompoundBareWord, QuotedWord)) or not variable_name(node):
|
||||
return ()
|
||||
chunks = []
|
||||
for child in node.children:
|
||||
if isinstance(child, BareWord):
|
||||
chunks.append(child.value)
|
||||
elif preserve_variables and isinstance(child, VarSub) and not child.children:
|
||||
chunks.append("${" + child.value + "}" if child.braced else "$" + child.value)
|
||||
else:
|
||||
chunks.append("\0")
|
||||
contents = "".join(chunks)
|
||||
if "(" not in contents or not contents.endswith(")"):
|
||||
return ()
|
||||
return tuple(
|
||||
part if part and not any(char in part for char in ("\0[]\\" if preserve_variables else "\0$[]\\")) else None
|
||||
for part in contents.split("(", 1)[1][:-1].split(",")
|
||||
)
|
||||
@@ -6,18 +6,24 @@ SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
import lsp_server
|
||||
import lsprotocol.types as lsp # type: ignore
|
||||
from common.load_data import standard_items
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from pygls.workspace import Workspace
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
|
||||
import lsp_server
|
||||
from common.load_data import standard_items
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from tools.completion_items import (
|
||||
COMMAND_KINDS,
|
||||
VARIABLE_KINDS,
|
||||
CompletionContext,
|
||||
completion_context,
|
||||
)
|
||||
from tools.tcl_command_completion import (
|
||||
DynamicCompletionKind,
|
||||
path_completion_items,
|
||||
tcl_argument_completion,
|
||||
)
|
||||
|
||||
|
||||
def _position_after(source: str, token: str, occurrence: int = 0) -> lsp.Position:
|
||||
@@ -90,6 +96,169 @@ def _complete(document: TextDocument, position: lsp.Position):
|
||||
).items
|
||||
|
||||
|
||||
def _argument_completion_labels(source: str) -> set[str] | None:
|
||||
completion = _argument_completion_request(source)
|
||||
if completion is None:
|
||||
return None
|
||||
return {item.label for item in completion.items}
|
||||
|
||||
|
||||
def test_unset_space_shows_options_then_variables(tmp_path, monkeypatch):
|
||||
server, document, _ = _completion_server(tmp_path, monkeypatch)
|
||||
document = server.workspace.get_text_document(document.uri)
|
||||
for version, tail in enumerate(["unset ", "unset -", "unset -nocomplain ", "unset -- ", "unset global"], start=2):
|
||||
source = "set globalValue 1\n" + tail
|
||||
document._source = source
|
||||
document.version = version
|
||||
items = _complete(document, lsp.Position(line=1, character=len(tail)))
|
||||
labels = {item.label for item in items}
|
||||
if tail in {"unset ", "unset -"}:
|
||||
assert labels == {"-nocomplain", "--"}
|
||||
else:
|
||||
assert "globalValue" in labels
|
||||
assert "-nocomplain" not in labels
|
||||
if tail == "unset -nocomplain ":
|
||||
assert "--" in labels
|
||||
else:
|
||||
assert "--" not in labels
|
||||
|
||||
|
||||
def test_array_keys_complete_in_set_and_substitution(tmp_path: Path, monkeypatch):
|
||||
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||
workspace = _document(
|
||||
tmp_path / "arrays.tcl",
|
||||
"set ::lib_flag(enabled) 1\nset ::lib_flag(external) 1\n",
|
||||
)
|
||||
assert server.update_poco_completion_for_file(workspace)
|
||||
source = (
|
||||
"set lib_flag(enabled) 0\n"
|
||||
"set lib_flag(empty) 1\n"
|
||||
"set other(wrong) 1\n"
|
||||
"proc hidden {} { set lib_flag(private) 1 }\n"
|
||||
"set lib_flag()\n"
|
||||
"puts $lib_flag(en)\n"
|
||||
"puts 😀; set lib_flag(em\n"
|
||||
)
|
||||
current = _document(tmp_path / "arrays-current.tcl", source)
|
||||
server.workspace.put_text_document(lsp.TextDocumentItem(
|
||||
uri=current.uri, language_id="tcl", version=1, text=source,
|
||||
))
|
||||
assert server.update_poco_completion_for_file(current)
|
||||
items = _complete(current, _position_after(source, "set lib_flag(", 3))
|
||||
assert [item.label for item in items] == ["empty", "enabled", "external"]
|
||||
assert all(item.text_edit.new_text == item.label for item in items)
|
||||
items = _complete(current, _position_after(source, "puts $lib_flag(en"))
|
||||
assert [item.label for item in items] == ["enabled"]
|
||||
assert items[0].text_edit.new_text == "enabled"
|
||||
position = lsp.Position(line=6, character=len(source.splitlines()[6].encode("utf-16-le")) // 2)
|
||||
items = _complete(current, position)
|
||||
assert [item.label for item in items] == ["empty"]
|
||||
assert items[0].text_edit.new_text == "empty)"
|
||||
assert items[0].text_edit.range.start.character == position.character - 2
|
||||
|
||||
|
||||
def test_dynamic_array_index_keeps_variable_identity(tmp_path: Path, monkeypatch):
|
||||
from tools.navigation import build_file_symbol_index
|
||||
from tools.parser import CustomParser
|
||||
from tools.semantic_tokens import _Highlighter
|
||||
from tools.variable_index import build_variable_index
|
||||
from tools.variable_names import variable_name
|
||||
|
||||
source = (
|
||||
"set custom_flag(from_move,$::mom_path_name) 1\n"
|
||||
'set "::quoted_flag(from_move,$::mom_path_name)" 1\n'
|
||||
"set ::command_flag([info hostname]) 1\n"
|
||||
"set ${dynamic_name}(entry) 1\n"
|
||||
"proc example {} { set local_flag($::mom_path_name) 1 }\n"
|
||||
"puts $custom_flag\n"
|
||||
)
|
||||
tree = CustomParser().parse(source)
|
||||
globals_, locals_, _ = build_variable_index(source, tree)
|
||||
assert {"custom_flag", "quoted_flag", "command_flag"} <= globals_
|
||||
assert locals_["example"] == {"local_flag"}
|
||||
assert variable_name(tree.children[3].args[0]) is None
|
||||
path = tmp_path / "dynamic.tcl"
|
||||
index = build_file_symbol_index(str(path), path.as_uri(), tree)
|
||||
definition = next(
|
||||
item for item in index.occurrences
|
||||
if item.identity.name == "::custom_flag" and item.is_definition
|
||||
)
|
||||
assert definition.range.start.character == 4
|
||||
assert definition.range.end.character == 15
|
||||
assert definition.array_element is None
|
||||
assert any(item.identity.name == "::mom_path_name" for item in index.occurrences)
|
||||
highlighter = _Highlighter([], {})
|
||||
tree.accept(highlighter, recurse=True)
|
||||
assert any(
|
||||
position == (0, 4) and length == 11 and kind == "variable"
|
||||
for position, length, kind, _ in highlighter._tokens
|
||||
)
|
||||
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||
current = _document(path, source)
|
||||
server.workspace.put_text_document(lsp.TextDocumentItem(
|
||||
uri=current.uri, language_id="tcl", version=1, text=source,
|
||||
))
|
||||
assert server.update_poco_completion_for_file(current)
|
||||
items = _complete(current, _position_after(source, "puts $custom"))
|
||||
assert "custom_flag" in {item.label for item in items}
|
||||
workspace_items = next(
|
||||
items for item_path, items in server.completion_items_by_file_snapshot().items()
|
||||
if server.paths_equal(item_path, str(path))
|
||||
)
|
||||
assert {"quoted_flag", "command_flag"} <= {item.label for item in workspace_items}
|
||||
|
||||
|
||||
def test_literal_array_components_complete_around_substitutions(tmp_path: Path, monkeypatch):
|
||||
from tools.completion_items import array_element_completions
|
||||
|
||||
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||
source = (
|
||||
"set custom_flag(from_move,$::mom_path_name) 1\n"
|
||||
"set custom_flag(to_move,$::mom_path_name) 1\n"
|
||||
"set custom_flag($::mom_path_name,finished) 1\n"
|
||||
"set custom_flag(prefix_$::mom_path_name,other) 1\n"
|
||||
"set custom_flag([info hostname],command_tail) 1\n"
|
||||
"set other_flag(wrong,$::mom_path_name) 1\n"
|
||||
"set multi_flag(move,$first,axis,$second) 1\n"
|
||||
)
|
||||
document = _document(tmp_path / "components.tcl", source)
|
||||
assert server.update_poco_completion_for_file(document)
|
||||
cases = [
|
||||
("set custom_flag(|,$::mom_path_name)", {"from_move", "to_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||
("set custom_flag(fr|om_old,$::mom_path_name)", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||
("puts $custom_flag($::mom_path_name,fi|)", {"finished"}, "finished", "puts $custom_flag($::mom_path_name,finished)"),
|
||||
("set custom_flag($::mom_path_name,|)", {"finished", "other", "command_tail"}, "other", "set custom_flag($::mom_path_name,other)"),
|
||||
("set custom_flag(fr|", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||
("set custom_flag(fr|)", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||
("set multi_flag(m|)", {"move"}, "move", "set multi_flag(move,$first,axis,$second)"),
|
||||
]
|
||||
for marked, labels, selected, expected in cases:
|
||||
offset = marked.index("|")
|
||||
line = marked.replace("|", "")
|
||||
items = array_element_completions(
|
||||
[line], lsp.Position(line=0, character=offset),
|
||||
server.navigation_snapshot().values(), str(tmp_path / "caller.tcl"),
|
||||
)
|
||||
assert {item.label for item in items} == labels
|
||||
edit = next(item.text_edit for item in items if item.label == selected)
|
||||
item = next(item for item in items if item.label == selected)
|
||||
assert item.insert_text_format == lsp.InsertTextFormat.PlainText
|
||||
assert line[:edit.range.start.character] + edit.new_text + line[edit.range.end.character:] == expected
|
||||
assert array_element_completions(
|
||||
["set custom_flag(from_move,$::mom"], lsp.Position(line=0, character=31),
|
||||
server.navigation_snapshot().values(), str(tmp_path / "caller.tcl"),
|
||||
) is None
|
||||
|
||||
|
||||
def _argument_completion_request(source: str):
|
||||
lines = source.split("\n")
|
||||
character = len(lines[-1].encode("utf-16-le")) // 2
|
||||
return tcl_argument_completion(
|
||||
lines,
|
||||
lsp.Position(line=len(lines) - 1, character=character),
|
||||
)
|
||||
|
||||
|
||||
def test_variable_completion_filters_and_ranks_candidates(tmp_path: Path, monkeypatch):
|
||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||
items = _complete(current, _position_after(source, "$local"))
|
||||
@@ -150,3 +319,339 @@ def test_completion_context_handles_nested_commands_and_utf16():
|
||||
completion_context(["puts value"], lsp.Position(line=0, character=10))
|
||||
== CompletionContext.GENERAL
|
||||
)
|
||||
|
||||
|
||||
def test_string_subcommands_and_compare_options_are_context_aware():
|
||||
subcommands = _argument_completion_labels("string ")
|
||||
assert subcommands is not None
|
||||
assert {"compare", "equal", "is", "map", "match"} <= subcommands
|
||||
|
||||
assert _argument_completion_labels("string compare ") == {
|
||||
"-length",
|
||||
"-nocase",
|
||||
}
|
||||
assert _argument_completion_labels("string compare -nocase ") == {
|
||||
"-length"
|
||||
}
|
||||
assert _argument_completion_labels("string compare -length ") is None
|
||||
|
||||
|
||||
def test_string_completion_handles_nested_commands_and_is_values():
|
||||
assert _argument_completion_labels("set result [string compare -") == {
|
||||
"-length",
|
||||
"-nocase",
|
||||
}
|
||||
|
||||
classes = _argument_completion_labels("string is ")
|
||||
assert classes is not None
|
||||
assert {"boolean", "double", "integer", "wideinteger"} <= classes
|
||||
assert _argument_completion_labels("string is integer ") == {
|
||||
"-failindex",
|
||||
"-strict",
|
||||
}
|
||||
|
||||
|
||||
def test_string_completion_inside_braced_conditions_and_bodies():
|
||||
for prefix in ("if {", "while {", "proc example {} { if {"):
|
||||
subcommands = _argument_completion_labels(prefix + "[string ")
|
||||
assert subcommands is not None
|
||||
assert {"compare", "equal", "is"} <= subcommands
|
||||
assert _argument_completion_labels(prefix + "[string compare -") == {
|
||||
"-length", "-nocase"
|
||||
}
|
||||
assert _argument_completion_labels(
|
||||
prefix + "[string compare -nocase "
|
||||
) == {"-length"}
|
||||
|
||||
|
||||
def test_closed_braced_arguments_do_not_change_completion_context():
|
||||
assert _argument_completion_labels("puts {[string compare }") is None
|
||||
assert _argument_completion_labels(
|
||||
"if {[string equal a b]} {string compare "
|
||||
) == {"-length", "-nocase"}
|
||||
assert _argument_completion_labels(
|
||||
"if {[string equal a b] && [string is integer "
|
||||
) == {"-failindex", "-strict"}
|
||||
|
||||
|
||||
def test_dict_array_namespace_file_and_info_subcommands():
|
||||
dict_items = _argument_completion_labels("dict ")
|
||||
assert dict_items is not None
|
||||
assert {"create", "filter", "get", "set", "with"} <= dict_items
|
||||
assert _argument_completion_labels("dict filter ") == {
|
||||
"key",
|
||||
"script",
|
||||
"value",
|
||||
}
|
||||
|
||||
assert _argument_completion_labels("array names values ") == {
|
||||
"-exact",
|
||||
"-glob",
|
||||
"-regexp",
|
||||
}
|
||||
|
||||
namespace_items = _argument_completion_labels("namespace ")
|
||||
assert namespace_items is not None
|
||||
assert {"children", "ensemble", "eval", "which"} <= namespace_items
|
||||
assert _argument_completion_labels("namespace which ") == {
|
||||
"-command",
|
||||
"-variable",
|
||||
}
|
||||
assert _argument_completion_labels("namespace ensemble ") == {
|
||||
"configure",
|
||||
"create",
|
||||
"exists",
|
||||
}
|
||||
|
||||
file_items = _argument_completion_labels("file ")
|
||||
assert file_items is not None
|
||||
assert {"copy", "delete", "exists", "normalize", "rename"} <= file_items
|
||||
assert _argument_completion_labels("file copy ") == {"--", "-force"}
|
||||
|
||||
info_items = _argument_completion_labels("info ")
|
||||
assert info_items is not None
|
||||
assert {"args", "body", "commands", "exists", "procs", "vars"} <= info_items
|
||||
|
||||
|
||||
def test_variable_context_still_takes_priority_inside_tcl_command(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||
command_source = source.replace(
|
||||
" puts $local\n",
|
||||
" puts $local\n string compare $localValue other\n",
|
||||
)
|
||||
current = _document(tmp_path / "current.tcl", command_source)
|
||||
lsp_server.LSP_SERVER.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=2,
|
||||
text=command_source,
|
||||
)
|
||||
)
|
||||
items = _complete(current, _position_after(command_source, "$localValue"))
|
||||
|
||||
assert items
|
||||
assert all(item.kind in VARIABLE_KINDS for item in items)
|
||||
assert "localValue" in {item.label for item in items}
|
||||
|
||||
|
||||
def test_lsp_completion_returns_only_matching_command_options(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||
source = "string compare "
|
||||
current = _document(tmp_path / "current.tcl", source)
|
||||
lsp_server.LSP_SERVER.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=2,
|
||||
text=source,
|
||||
)
|
||||
)
|
||||
|
||||
items = _complete(current, _position_after(source, source))
|
||||
|
||||
assert {item.label for item in items} == {"-length", "-nocase"}
|
||||
assert all(item.kind == lsp.CompletionItemKind.Keyword for item in items)
|
||||
assert all(item.sort_text.startswith("000:") for item in items)
|
||||
|
||||
|
||||
def test_space_trigger_does_not_open_broad_fallback_completion(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||
source = "set value "
|
||||
current = _document(tmp_path / "current.tcl", source)
|
||||
lsp_server.LSP_SERVER.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=2,
|
||||
text=source,
|
||||
)
|
||||
)
|
||||
|
||||
result = lsp_server.on_completion(
|
||||
lsp.CompletionParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=current.uri),
|
||||
position=_position_after(source, source),
|
||||
context=lsp.CompletionContext(
|
||||
trigger_kind=lsp.CompletionTriggerKind.TriggerCharacter,
|
||||
trigger_character=" ",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
assert result.items == []
|
||||
|
||||
|
||||
def test_argument_values_are_suggested_at_the_expected_position():
|
||||
assert _argument_completion_labels("return -code ") == {
|
||||
"break",
|
||||
"continue",
|
||||
"error",
|
||||
"ok",
|
||||
"return",
|
||||
}
|
||||
assert _argument_completion_labels("open output.txt ") == {
|
||||
"a",
|
||||
"a+",
|
||||
"r",
|
||||
"r+",
|
||||
"w",
|
||||
"w+",
|
||||
}
|
||||
assert _argument_completion_labels("seek channel 0 ") == {
|
||||
"current",
|
||||
"end",
|
||||
"start",
|
||||
}
|
||||
|
||||
|
||||
def test_dynamic_argument_categories_are_detected():
|
||||
variable = _argument_completion_request("set ")
|
||||
procedure = _argument_completion_request("info body ")
|
||||
namespace = _argument_completion_request("namespace eval ")
|
||||
path = _argument_completion_request("source scripts/")
|
||||
|
||||
assert variable is not None
|
||||
assert variable.dynamic_kind == DynamicCompletionKind.VARIABLE
|
||||
assert procedure is not None
|
||||
assert procedure.dynamic_kind == DynamicCompletionKind.PROCEDURE
|
||||
assert namespace is not None
|
||||
assert namespace.dynamic_kind == DynamicCompletionKind.NAMESPACE
|
||||
assert path is not None
|
||||
assert path.dynamic_kind == DynamicCompletionKind.PATH
|
||||
assert path.path_extensions == (".tcl",)
|
||||
|
||||
|
||||
def test_path_completion_is_relative_filtered_and_tcl_safe(tmp_path: Path):
|
||||
scripts = tmp_path / "scripts"
|
||||
scripts.mkdir()
|
||||
(scripts / "first file.tcl").write_text("puts ok", encoding="utf-8")
|
||||
(scripts / "ignored.txt").write_text("ignored", encoding="utf-8")
|
||||
|
||||
source = "source scripts/f"
|
||||
completion = _argument_completion_request(source)
|
||||
assert completion is not None
|
||||
items = path_completion_items(
|
||||
tmp_path,
|
||||
completion,
|
||||
_position_after(source, source),
|
||||
)
|
||||
|
||||
assert [item.label for item in items] == ["scripts/first\\ file.tcl"]
|
||||
assert items[0].kind == lsp.CompletionItemKind.File
|
||||
assert items[0].text_edit is not None
|
||||
assert items[0].text_edit.new_text == "scripts/first\\ file.tcl"
|
||||
assert items[0].text_edit.range.start.character == len("source ")
|
||||
|
||||
|
||||
def test_lsp_source_completion_reads_paths_from_document_directory(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
server, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||
scripts = tmp_path / "scripts"
|
||||
scripts.mkdir()
|
||||
(scripts / "helper.tcl").write_text("proc helper {} {}", encoding="utf-8")
|
||||
(scripts / "ignored.txt").write_text("ignored", encoding="utf-8")
|
||||
source = "source scripts/"
|
||||
current = _document(tmp_path / "current.tcl", source)
|
||||
server.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=2,
|
||||
text=source,
|
||||
)
|
||||
)
|
||||
|
||||
items = _complete(current, _position_after(source, source))
|
||||
labels = {item.label for item in items}
|
||||
|
||||
assert "scripts/helper.tcl" in labels
|
||||
assert "scripts/ignored.txt" not in labels
|
||||
|
||||
|
||||
def test_command_and_dict_for_snippets_use_lsp_snippet_placeholders(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||
command_items = _complete(current, _position_after(source, "localP", occurrence=1))
|
||||
command_by_label = {item.label: item for item in command_items}
|
||||
|
||||
assert command_by_label["if"].kind == lsp.CompletionItemKind.Snippet
|
||||
assert command_by_label["if"].insert_text_format == lsp.InsertTextFormat.Snippet
|
||||
assert "${1:condition}" in (command_by_label["if"].insert_text or "")
|
||||
|
||||
dict_for = _argument_completion_request("dict f")
|
||||
assert dict_for is not None
|
||||
for_item = next(item for item in dict_for.items if item.label == "for")
|
||||
assert for_item.kind == lsp.CompletionItemKind.Snippet
|
||||
assert "${3:dictionary}" in (for_item.insert_text or "")
|
||||
|
||||
switch_arguments = _argument_completion_request("switch ")
|
||||
assert switch_arguments is not None
|
||||
assert {"switch block", "-exact", "-glob", "-regexp"} <= {
|
||||
item.label for item in switch_arguments.items
|
||||
}
|
||||
|
||||
|
||||
def test_semantic_variable_and_procedure_argument_completion(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||
|
||||
variable_items = _complete(current, _position_after(source, " set "))
|
||||
assert variable_items
|
||||
assert all(item.kind in VARIABLE_KINDS for item in variable_items)
|
||||
assert {"argument", "localValue"} <= {item.label for item in variable_items}
|
||||
|
||||
procedure_source = source + "info body localProc\n"
|
||||
current = _document(tmp_path / "current.tcl", procedure_source)
|
||||
lsp_server.LSP_SERVER.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=2,
|
||||
text=procedure_source,
|
||||
)
|
||||
)
|
||||
procedure_items = _complete(
|
||||
current,
|
||||
_position_after(procedure_source, "info body "),
|
||||
)
|
||||
procedure_labels = {item.label for item in procedure_items}
|
||||
|
||||
assert "localProc" in procedure_labels
|
||||
assert "workspaceProc" in procedure_labels
|
||||
assert "MOM_abort" in procedure_labels
|
||||
assert "localValue" not in procedure_labels
|
||||
assert "string" not in procedure_labels
|
||||
|
||||
|
||||
def test_namespace_argument_completion_uses_navigation_index(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
server, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||
namespace_source = "namespace eval tools { proc helper {} { return } }\n"
|
||||
namespace_document = _document(tmp_path / "namespaces.tcl", namespace_source)
|
||||
assert server.update_poco_completion_for_file(namespace_document)
|
||||
|
||||
source = "namespace eval "
|
||||
current = _document(tmp_path / "current.tcl", source)
|
||||
server.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=2,
|
||||
text=source,
|
||||
)
|
||||
)
|
||||
items = _complete(current, _position_after(source, source))
|
||||
|
||||
assert "tools" in {item.label for item in items}
|
||||
assert all(item.kind == lsp.CompletionItemKind.Module for item in items)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
from pathlib import Path
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
from pygls.workspace import Workspace
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
|
||||
import lsp_server
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from tools.file_sourcing import psc_script_files
|
||||
from tools.tcloo_completion import tcloo_completions
|
||||
from tools.semantic_tokens import TOKEN_TYPE_INDEX
|
||||
|
||||
|
||||
CLASS = '''oo::class create MCS {
|
||||
method initOrg {dx dy dz} {return [self]}
|
||||
method toStr {{precision 7}} {}
|
||||
}
|
||||
proc helper {value} {}
|
||||
'''
|
||||
|
||||
|
||||
def setup_project(tmp_path, monkeypatch):
|
||||
root = tmp_path / "project"
|
||||
root.mkdir()
|
||||
library = tmp_path / "external library"
|
||||
library.mkdir()
|
||||
script = library / "geometry.tcl"
|
||||
script.write_text(CLASS, encoding="utf-8")
|
||||
psc = root / "post.psc"
|
||||
psc.write_text('''<Post><Layer Name="Geometry" SubFolder="../external library">
|
||||
<Scripts><Filename Name="geometry" /></Scripts>
|
||||
</Layer></Post>''', encoding="utf-8")
|
||||
server = TclLanguageServer(name="psc-test", version="1")
|
||||
server.protocol._workspace = Workspace(root_uri=root.as_uri(), sync_kind=lsp.TextDocumentSyncKind.Incremental,
|
||||
workspace_folders=[], position_encoding=lsp.PositionEncodingKind.Utf16)
|
||||
monkeypatch.setattr(lsp_server, "LSP_SERVER", server)
|
||||
return server, root, psc, script
|
||||
|
||||
|
||||
def caller(server, root, source):
|
||||
uri = (root / "caller.tcl").as_uri()
|
||||
server.clear_cache_for_uri(uri)
|
||||
server.workspace.put_text_document(lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source))
|
||||
return uri, lsp.Position(line=len(source.splitlines()) - 1, character=len(source.splitlines()[-1]))
|
||||
|
||||
|
||||
def test_psc_external_class_available_in_all_language_features(tmp_path, monkeypatch):
|
||||
server, root, _, script = setup_project(tmp_path, monkeypatch)
|
||||
lsp_server._refresh_psc_index()
|
||||
assert any(server.paths_equal(script, path) for path in server.class_indexes)
|
||||
assert {"MCS", "helper"} <= {item.label for item in server.completion_items_snapshot()}
|
||||
|
||||
uri, position = caller(server, root, "set mcs [MCS new]\n$mcs ")
|
||||
result = lsp_server.on_completion(lsp.CompletionParams(text_document=lsp.TextDocumentIdentifier(uri=uri), position=position))
|
||||
assert {"initOrg", "toStr"} <= {item.label for item in result.items}
|
||||
|
||||
uri, position = caller(server, root, "set mcs [MCS new]\n$mcs initOrg 1 ")
|
||||
result = lsp_server.signature_help(lsp.SignatureHelpParams(text_document=lsp.TextDocumentIdentifier(uri=uri), position=position))
|
||||
assert result.signatures[0].label == "::MCS initOrg dx dy dz"
|
||||
assert result.active_parameter == 1
|
||||
|
||||
uri, _ = caller(server, root, "set mcs [MCS new]\n$mcs initOrg 1 2 3")
|
||||
monkeypatch.setattr(lsp_server, "_get_settings_by_document", lambda doc: {"inlayHint": True})
|
||||
hints = lsp_server.inlay_hints(lsp.InlayHintParams(text_document=lsp.TextDocumentIdentifier(uri=uri), range=lsp.Range(
|
||||
start=lsp.Position(line=0, character=0), end=lsp.Position(line=2, character=0))))
|
||||
assert [hint.label[0].value for hint in hints] == ["dx:", "dy:", "dz:"]
|
||||
tokens = lsp_server.semantic_tokens(lsp.SemanticTokensParams(text_document=lsp.TextDocumentIdentifier(uri=uri))).data
|
||||
assert TOKEN_TYPE_INDEX["class"] in tokens[3::5]
|
||||
|
||||
uri, position = caller(server, root, "set mcs [MC")
|
||||
result = lsp_server.on_completion(lsp.CompletionParams(text_document=lsp.TextDocumentIdentifier(uri=uri), position=position))
|
||||
assert any(item.label == "MCS" and item.kind == lsp.CompletionItemKind.Class for item in result.items)
|
||||
|
||||
|
||||
def test_psc_refresh_removes_unlinked_external_classes(tmp_path, monkeypatch):
|
||||
server, root, psc, script = setup_project(tmp_path, monkeypatch)
|
||||
server.refresh_psc_scripts([root])
|
||||
psc.write_text("<Post/>", encoding="utf-8")
|
||||
lsp_server.did_change_watched_files(lsp.DidChangeWatchedFilesParams(changes=[
|
||||
lsp.FileEvent(uri=psc.as_uri(), type=lsp.FileChangeType.Changed)]))
|
||||
assert "::MCS" not in server.class_snapshot(root / "caller.tcl")
|
||||
assert not any(server.paths_equal(script, path) for path in server.class_indexes)
|
||||
assert "MCS" not in {item.label for item in server.completion_items_snapshot()}
|
||||
|
||||
|
||||
def test_class_metadata_updates_and_local_override_does_not_mutate_index(tmp_path, monkeypatch):
|
||||
server, root, _, script = setup_project(tmp_path, monkeypatch)
|
||||
server.refresh_psc_scripts([root])
|
||||
classes = server.class_snapshot(root / "caller.tcl")
|
||||
source = "oo::class create MCS {method local {} {}}\nset mcs [MCS new]\n$mcs "
|
||||
items = tcloo_completions(source.splitlines(), lsp.Position(line=2, character=5), classes)
|
||||
assert {item.label for item in items} == {"local", "destroy"}
|
||||
assert "local" not in classes["::MCS"].methods
|
||||
document = TextDocument(uri=script.as_uri(), source="oo::class create MCS {method changed {} {}}", version=2)
|
||||
assert server.update_poco_completion_for_file(document)
|
||||
assert set(server.class_snapshot(root / "caller.tcl")["::MCS"].methods) == {"changed"}
|
||||
server.remove_file_state(script.as_uri())
|
||||
assert "::MCS" not in server.class_snapshot(root / "caller.tcl")
|
||||
|
||||
|
||||
def test_psc_load_order_missing_files_and_xml_namespace(tmp_path, monkeypatch):
|
||||
server, root, psc, script = setup_project(tmp_path, monkeypatch)
|
||||
override = root / "override.tcl"
|
||||
override.write_text("oo::class create MCS {method override {} {}}", encoding="utf-8")
|
||||
psc.write_text(f'''<Post xmlns="urn:psc">
|
||||
<Layer Name="Base" SubFolder="..\\external library"><Scripts><Filename Name="geometry.tcl"/></Scripts></Layer>
|
||||
<Layer Name="Custom"><Scripts><Filename Name="{override.as_posix()}"/><Filename Name="missing.tcl"/></Scripts></Layer>
|
||||
</Post>''', encoding="utf-8")
|
||||
assert psc_script_files(psc) == [script, override, root / "missing.tcl"]
|
||||
messages = []
|
||||
server.refresh_psc_scripts([root], messages.append)
|
||||
assert set(server.class_snapshot(root / "caller.tcl")["::MCS"].methods) == {"override"}
|
||||
assert any("missing.tcl" in message for message in messages)
|
||||
|
||||
|
||||
def test_psc_refresh_preserves_unsaved_open_library(tmp_path, monkeypatch):
|
||||
server, root, _, script = setup_project(tmp_path, monkeypatch)
|
||||
server.workspace.put_text_document(lsp.TextDocumentItem(uri=script.as_uri(), language_id="tcl", version=3,
|
||||
text="oo::class create MCS {method unsaved {} {}}"))
|
||||
server.refresh_psc_scripts([root])
|
||||
assert set(server.class_snapshot(root / "caller.tcl")["::MCS"].methods) == {"unsaved"}
|
||||
|
||||
|
||||
def test_psc_environment_folder_and_legacy_encoding(tmp_path, monkeypatch):
|
||||
server, root, psc, script = setup_project(tmp_path, monkeypatch)
|
||||
monkeypatch.setenv("UGII_CAM_SHOP_DOC_DIR", str(script.parent))
|
||||
psc.write_text('''<Post><Layer SubFolder="UGII_CAM_SHOP_DOC_DIR">
|
||||
<Scripts><Filename Name="geometry"/></Scripts></Layer></Post>''', encoding="utf-8")
|
||||
script.write_bytes(("# Ältere Bibliothek\n" + CLASS).encode("cp1252"))
|
||||
server.refresh_psc_scripts([root])
|
||||
assert "::MCS" in server.class_snapshot(root / "caller.tcl")
|
||||
@@ -0,0 +1,100 @@
|
||||
import lsprotocol.types as lsp
|
||||
import pytest
|
||||
|
||||
from tools.inlay_hint import InlayHintGenerator
|
||||
from tools.parser import CustomParser
|
||||
from tools.tcloo_arguments import method_parameters, method_signature_help
|
||||
|
||||
|
||||
CLASS = """oo::class create MCS {
|
||||
constructor {name {size 3}} {}
|
||||
method initValue {i value} {return [self]}
|
||||
method format {value {precision 7}} {}
|
||||
method many {first args} {}
|
||||
method empty {} {}
|
||||
method internal {} {my initValue 0 10}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def signature(source):
|
||||
prefix, suffix = source.split("|")
|
||||
position = lsp.Position(line=prefix.count("\n"), character=len(prefix.rsplit("\n", 1)[-1].encode("utf-16-le")) // 2)
|
||||
return method_signature_help(prefix + suffix, position)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tail, label, active", [
|
||||
("set mcs [MCS new test]\n$mcs initValue |", "::MCS initValue i value", 0),
|
||||
("set mcs [MCS new test]\n$mcs initValue 0 |", "::MCS initValue i value", 1),
|
||||
("set mcs [MCS new test]\n$mcs format 12 |", "::MCS format value {precision 7}", 1),
|
||||
("set mcs [MCS new test]\n$mcs many 1 2 3 |", "::MCS many first args", 1),
|
||||
("set mcs [MCS new test]\n$mcs empty |", "::MCS empty", None),
|
||||
("MCS create instance test\ninstance initValue 0 |", "::MCS initValue i value", 1),
|
||||
("set mcs [[MCS new test] initValue 0 0]\n$mcs initValue |", "::MCS initValue i value", 0),
|
||||
("set mcs [MCS new test]\nputs [$mcs initValue 0 |", "::MCS initValue i value", 1),
|
||||
("set mcs [MCS new |", "::MCS new name {size 3}", 0),
|
||||
("MCS create instance |", "::MCS create objectName name {size 3}", 1),
|
||||
("set mcs [MCS new test]\nputs 😀; $mcs initValue 0 |", "::MCS initValue i value", 1),
|
||||
])
|
||||
def test_method_signatures(tail, label, active):
|
||||
result = signature(CLASS + tail)
|
||||
assert result is not None
|
||||
assert result.signatures[0].label == label
|
||||
assert result.active_parameter == active
|
||||
|
||||
|
||||
def test_my_signature_and_parameter_spans():
|
||||
result = signature(CLASS.replace("my initValue 0 10", "my initValue 0 |"))
|
||||
assert result.active_parameter == 1
|
||||
info = result.signatures[0]
|
||||
assert [info.label[start:end] for start, end in (p.label for p in info.parameters)] == ["i", "value"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tail", [
|
||||
"$unknown initValue |",
|
||||
"set mcs [MCS new test]\nset mcs text\n$mcs initValue |",
|
||||
"set mcs [MCS new test]\n$mcs initValue [unknown |] 3",
|
||||
"set mcs [MCS new test]\n$mcs initValue|",
|
||||
])
|
||||
def test_unknown_receivers_and_nested_commands(tail):
|
||||
assert signature(CLASS + tail) is None
|
||||
|
||||
|
||||
def test_optional_parameter_with_spaced_default():
|
||||
parameters = method_parameters('value {description {hello world}} args')
|
||||
assert [p.name for p in parameters] == ["value", "description", "args"]
|
||||
assert parameters[1].label == "{description {hello world}}"
|
||||
assert parameters[-1].variadic
|
||||
|
||||
|
||||
def test_method_inlay_hints_and_existing_preferences():
|
||||
source = CLASS + "set mcs [MCS new test]\n$mcs initValue 0 10\n$mcs many 1 2 3\n$mcs initValue $i $other"
|
||||
tree = CustomParser().parse(source)
|
||||
start = len(CLASS.splitlines()) + 1
|
||||
requested = lsp.Range(start=lsp.Position(line=start, character=0), end=lsp.Position(line=start + 2, character=100))
|
||||
generator = InlayHintGenerator(source, {}, requested_range=requested)
|
||||
hints = generator.generate(tree)
|
||||
assert [hint.label[0].value for hint in hints] == ["i:", "value:", "first:", "args:", "args:", "value:"]
|
||||
assert [source.splitlines()[hint.position.line][hint.position.character:] for hint in hints[:2]] == ["0 10", "10"]
|
||||
assert "::MCS initValue i value" in hints[0].tooltip.value
|
||||
generator = InlayHintGenerator(source, {}, requested_range=requested, parameter_names="literals")
|
||||
assert len(generator.generate(tree)) == 5
|
||||
generator = InlayHintGenerator(source, {}, parameter_names="none")
|
||||
assert generator.generate(tree) == []
|
||||
|
||||
|
||||
def test_lsp_signature_help_with_unfinished_bracket(tmp_path, monkeypatch):
|
||||
import lsp_server
|
||||
from test_completion_context import _completion_server
|
||||
|
||||
server, document, _ = _completion_server(tmp_path, monkeypatch)
|
||||
source = CLASS + "set mcs [MCS new test]\nputs [$mcs initValue 0 "
|
||||
document = server.workspace.get_text_document(document.uri)
|
||||
document._source = source
|
||||
document.version = 2
|
||||
result = lsp_server.signature_help(lsp.SignatureHelpParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
|
||||
position=lsp.Position(line=len(source.splitlines()) - 1, character=len(source.splitlines()[-1])),
|
||||
))
|
||||
assert result.active_parameter == 1
|
||||
assert result.signatures[0].label == "::MCS initValue i value"
|
||||
@@ -0,0 +1,166 @@
|
||||
import lsprotocol.types as lsp
|
||||
import pytest
|
||||
|
||||
from tools.tcloo_completion import tcloo_completions
|
||||
|
||||
|
||||
CLASS = """oo::class create MCS {
|
||||
constructor {} {my initValue 0 0}
|
||||
method initValue {i value} {return [self]}
|
||||
method initOrg {x y z} {return [my initValue 0 $x]}
|
||||
method toLst {} {return {1 2 3}}
|
||||
method _private {} {return [self]}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def complete(source):
|
||||
offset = source.index("|")
|
||||
before = source[:offset]
|
||||
position = lsp.Position(line=before.count("\n"), character=len(before.rsplit("\n", 1)[-1].encode("utf-16-le")) // 2)
|
||||
return tcloo_completions(source.replace("|", "").splitlines(), position)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [
|
||||
"set mcs [MCS new]\n$mcs |",
|
||||
"set mcs [::MCS new]\n$mcs |",
|
||||
"MCS create instance\ninstance |",
|
||||
"set mcs [MCS create instance]\n$mcs |",
|
||||
"set mcs [[MCS new] initValue 0 0]\n$mcs |",
|
||||
"set mcs [[MCS new] initOrg 1 2 3]\n$mcs |",
|
||||
"set a [MCS new]\nset mcs $a\n$mcs |",
|
||||
"proc run {} {set mcs [MCS new]; $mcs |}",
|
||||
"set mcs [MCS new]\nputs [$mcs |]",
|
||||
"set mcs [MCS new]\nputs [$mcs |",
|
||||
"[MCS new] |",
|
||||
])
|
||||
def test_instances_and_chains(code):
|
||||
items = complete(CLASS + code)
|
||||
assert {item.label for item in items} == {"initValue", "initOrg", "toLst", "destroy"}
|
||||
assert next(item for item in items if item.label == "initValue").detail == "::MCS initValue i value"
|
||||
|
||||
|
||||
def test_my_and_constructor():
|
||||
for source in [CLASS.replace("my initValue 0 0", "my |"), CLASS.replace("return {1 2 3}", "my |")]:
|
||||
assert "_private" in {item.label for item in complete(source)}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [
|
||||
"set mcs [MCS new]\nset mcs text\n$mcs |",
|
||||
"set mcs [MCS new]\nunset mcs\n$mcs |",
|
||||
"proc a {} {set mcs [MCS new]}\nproc b {} {$mcs |}",
|
||||
"set mcs [MCS new]\nproc b {mcs} {$mcs |}",
|
||||
"set mcs [[MCS new] toLst]\n$mcs |",
|
||||
"$mcs |\nset mcs [MCS new]",
|
||||
"set mcs [MCS new]\n$mcs initValue |",
|
||||
"# my |",
|
||||
"puts {my |}",
|
||||
])
|
||||
def test_unknown_and_non_command_contexts(code):
|
||||
assert complete(CLASS + code) is None
|
||||
|
||||
|
||||
def test_prefix_and_replacement():
|
||||
items = complete(CLASS + "set mcs [MCS new]\n$mcs initV|alue")
|
||||
assert [item.label for item in items] == ["initValue"]
|
||||
assert items[0].text_edit.new_text == "initValue"
|
||||
assert items[0].text_edit.range.start.character == 5
|
||||
assert items[0].text_edit.range.end.character == 14
|
||||
|
||||
|
||||
def test_namespace():
|
||||
source = "namespace eval geometry {\n" + CLASS + "set mcs [MCS new]\n$mcs |\n}"
|
||||
assert "initOrg" in {item.label for item in complete(source)}
|
||||
|
||||
|
||||
def test_lsp_space_trigger(tmp_path, monkeypatch):
|
||||
import lsp_server
|
||||
from test_completion_context import _completion_server
|
||||
|
||||
server, document, _ = _completion_server(tmp_path, monkeypatch)
|
||||
source = CLASS + "set mcs [MCS new]\n$mcs "
|
||||
document = server.workspace.get_text_document(document.uri)
|
||||
document._source = source
|
||||
document.version = 2
|
||||
items = lsp_server.on_completion(lsp.CompletionParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
|
||||
position=lsp.Position(line=len(source.splitlines()) - 1, character=5),
|
||||
context=lsp.CompletionContext(trigger_kind=lsp.CompletionTriggerKind.TriggerCharacter, trigger_character=" "),
|
||||
)).items
|
||||
assert "initOrg" in {item.label for item in items}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tail", ["set mcs [MC", "set mcs [MC]", "MC"])
|
||||
def test_class_name_completion_while_editing(tmp_path, monkeypatch, tail):
|
||||
import lsp_server
|
||||
from test_completion_context import _completion_server
|
||||
|
||||
server, document, _ = _completion_server(tmp_path, monkeypatch)
|
||||
source = CLASS + tail
|
||||
document = server.workspace.get_text_document(document.uri)
|
||||
document._source = source
|
||||
document.version = 2
|
||||
items = lsp_server.on_completion(lsp.CompletionParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
|
||||
position=lsp.Position(line=len(source.splitlines()) - 1,
|
||||
character=len(tail.rstrip("]"))),
|
||||
)).items
|
||||
classes = [item for item in items if item.label == "MCS"]
|
||||
assert len(classes) == 1
|
||||
assert classes[0].kind == lsp.CompletionItemKind.Class
|
||||
|
||||
|
||||
def test_classes_are_indexed_with_their_own_kind(tmp_path, monkeypatch):
|
||||
from test_completion_context import _completion_server, _document
|
||||
|
||||
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||
assert server.update_poco_completion_for_file(_document(tmp_path / "class.tcl", CLASS))
|
||||
items = server.completion_items_snapshot()
|
||||
assert any(item.label == "MCS" and item.kind == lsp.CompletionItemKind.Class for item in items)
|
||||
assert "MCS" not in server.custom_function_names_snapshot()
|
||||
|
||||
|
||||
def test_class_command_completions():
|
||||
assert {item.label for item in complete(CLASS + "set mcs [MCS |]")} == {"new", "create"}
|
||||
|
||||
|
||||
def test_semantic_class_declarations_and_uses():
|
||||
from tools.parser import CustomParser
|
||||
from tools.semantic_tokens import _Highlighter, TokenModifier
|
||||
|
||||
source = CLASS + 'set mcs [MCS new]\nset second [::MCS new]\nputs "MCS"\n'
|
||||
tree = CustomParser().parse(source)
|
||||
highlighter = _Highlighter([], {"MCS"})
|
||||
highlighter.highlight_classes(tree)
|
||||
tree.accept(highlighter, recurse=True)
|
||||
line = col = 0
|
||||
classified = []
|
||||
for token in highlighter.tokens():
|
||||
col = col + token.offset if token.line == 0 else token.offset
|
||||
line += token.line
|
||||
text = source.splitlines()[line][col:col + token.length]
|
||||
classified.append((line, col, text, token.tok_type, token.tok_modifiers))
|
||||
classes = [entry for entry in classified if entry[3] == "class"]
|
||||
assert [entry[2] for entry in classes] == ["MCS", "MCS", "::MCS"]
|
||||
assert TokenModifier.declaration in classes[0][4]
|
||||
assert not any(entry[2] in {"MCS", "::MCS"} and entry[3] == "function" for entry in classified)
|
||||
assert len({entry[:2] for entry in classified}) == len(classified)
|
||||
|
||||
|
||||
def test_namespaced_class_symbols_and_method_body_references():
|
||||
from tools.parser import CustomParser
|
||||
from tools.tcloo_symbols import class_completion_items, class_symbols
|
||||
|
||||
tree = CustomParser().parse('''namespace eval geometry {
|
||||
oo::class create MCS {
|
||||
method duplicate {} {return [MCS new]}
|
||||
}
|
||||
set mcs [MCS new]
|
||||
}
|
||||
set mcs [geometry::MCS new]
|
||||
set mcs [::geometry::MCS new]
|
||||
''')
|
||||
declarations, references = class_symbols(tree)
|
||||
assert set(declarations) == {"::geometry::MCS"}
|
||||
assert [node.contents for node in references] == ["MCS", "MCS", "geometry::MCS", "::geometry::MCS"]
|
||||
assert class_completion_items(tree)[0].label == "geometry::MCS"
|
||||
@@ -0,0 +1,56 @@
|
||||
import lsprotocol.types as lsp
|
||||
|
||||
from tools.parser import CustomParser
|
||||
from tools.semantic_tokens import _Highlighter, TOKEN_TYPE_INDEX, TokenModifier
|
||||
|
||||
|
||||
def test_methods_use_proc_colors_without_coloring_plain_arguments():
|
||||
source = '''oo::class create MCS {
|
||||
method initOrg {x y z} {return [self]}
|
||||
method reset {} {my initOrg 0 0 0}
|
||||
}
|
||||
set obj [MCS new]
|
||||
$obj initOrg 1 2 3
|
||||
puts initOrg
|
||||
# initOrg
|
||||
'''
|
||||
tree = CustomParser().parse(source)
|
||||
highlighter = _Highlighter([], {})
|
||||
highlighter.highlight_classes(tree)
|
||||
highlighter.highlight_methods(tree, source, "file:///test.tcl")
|
||||
tree.accept(highlighter, recurse=True)
|
||||
line = column = 0
|
||||
tokens = []
|
||||
for token in highlighter.tokens():
|
||||
column = column + token.offset if token.line == 0 else token.offset
|
||||
line += token.line
|
||||
text = source.splitlines()[line][column:column + token.length]
|
||||
tokens.append((line, column, text, token.tok_type, token.tok_modifiers))
|
||||
methods = [token for token in tokens if token[2] in {"initOrg", "reset"}]
|
||||
assert [token[2] for token in methods] == ["initOrg", "reset", "initOrg", "initOrg"]
|
||||
assert all(token[3] == "function" for token in methods)
|
||||
assert TokenModifier.declaration in methods[0][4]
|
||||
assert TokenModifier.declaration in methods[1][4]
|
||||
assert all(token[3] == "class" for token in tokens if token[2] == "MCS")
|
||||
assert len({token[:2] for token in tokens}) == len(tokens)
|
||||
|
||||
|
||||
def test_psc_method_calls_are_function_tokens(tmp_path, monkeypatch):
|
||||
import lsp_server
|
||||
from test_psc_classes import setup_project, caller
|
||||
|
||||
server, root, _, _ = setup_project(tmp_path, monkeypatch)
|
||||
server.refresh_psc_scripts([root])
|
||||
source = "set obj [MCS new]\n$obj initOrg 1 2 3"
|
||||
uri, _ = caller(server, root, source)
|
||||
data = lsp_server.semantic_tokens(lsp.SemanticTokensParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=uri))).data
|
||||
line = column = 0
|
||||
tokens = {}
|
||||
for index in range(0, len(data), 5):
|
||||
delta, offset, length, kind, _ = data[index:index + 5]
|
||||
column = column + offset if delta == 0 else offset
|
||||
line += delta
|
||||
tokens[(line, source.splitlines()[line][column:column + length])] = kind
|
||||
assert tokens[(1, "initOrg")] == TOKEN_TYPE_INDEX["function"]
|
||||
assert tokens[(0, "MCS")] == TOKEN_TYPE_INDEX["class"]
|
||||
@@ -0,0 +1,94 @@
|
||||
import lsprotocol.types as lsp
|
||||
import pytest
|
||||
from pygls import uris
|
||||
|
||||
from tools.tcloo_navigation import tcloo_definition
|
||||
|
||||
|
||||
CLASS = '''oo::class create MCS {
|
||||
constructor {value} {}
|
||||
method initOrg {dx dy dz} {return [self]}
|
||||
method toStr {} {my initOrg 1 2 3}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def locate(source, classes=None):
|
||||
prefix, suffix = source.split("|")
|
||||
return tcloo_definition(prefix + suffix, "file:///caller.tcl", lsp.Position(
|
||||
line=prefix.count("\n"),
|
||||
character=len(prefix.rsplit("\n", 1)[-1].encode("utf-16-le")) // 2), classes)
|
||||
|
||||
|
||||
def target_text(location, source):
|
||||
assert location is not None
|
||||
span = location.range
|
||||
line = source.splitlines()[span.start.line].encode("utf-16-le")
|
||||
return line[span.start.character * 2:span.end.character * 2].decode("utf-16-le")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code, expected", [
|
||||
("set mcs [M|CS new 0]", "MCS"),
|
||||
("set mcs [::M|CS new 0]", "MCS"),
|
||||
("set mcs [MCS n|ew 0]", "constructor"),
|
||||
("MCS cr|eate instance 0", "constructor"),
|
||||
("set mcs [MCS new 0]\n$mcs init|Org 1 2 3", "initOrg"),
|
||||
("MCS create instance 0\ninstance to|Str", "toStr"),
|
||||
("set mcs [[MCS new 0] initOrg 1 2 3]\n$mcs to|Str", "toStr"),
|
||||
("set mcs [MCS new 0]\nputs [$mcs init|Org", "initOrg"),
|
||||
("puts 😀; set mcs [M|CS new 0]", "MCS"),
|
||||
])
|
||||
def test_local_class_method_and_constructor_targets(code, expected):
|
||||
source = CLASS + code
|
||||
target = locate(source)
|
||||
assert target.uri == "file:///caller.tcl"
|
||||
assert target_text(target, source.replace("|", "")) == expected
|
||||
|
||||
|
||||
def test_my_and_method_declaration():
|
||||
for source in [CLASS.replace("my initOrg", "my init|Org"), CLASS.replace("method initOrg", "method init|Org")]:
|
||||
assert target_text(locate(source), CLASS) == "initOrg"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [
|
||||
"set mcs [MCS new 0]\nset mcs text\n$mcs init|Org 1 2 3",
|
||||
"$unknown init|Org 1 2 3",
|
||||
"puts {M|CS}",
|
||||
"# M|CS",
|
||||
"set mcs [MCS new 0]\n$mcs initOrg to|Str 2 3",
|
||||
])
|
||||
def test_no_guessing_for_unknown_receivers_or_plain_text(code):
|
||||
assert locate(CLASS + code) is None
|
||||
|
||||
|
||||
def test_same_method_name_resolves_to_correct_class():
|
||||
source = CLASS + "oo::class create Other {method initOrg {} {}}\nset obj [Other new]\n$obj init|Org"
|
||||
target = locate(source)
|
||||
assert target.range.start.line == 5
|
||||
|
||||
|
||||
def test_namespaced_class_and_utf16_definition():
|
||||
source = 'namespace eval geo {\nputs 😀; ' + CLASS + '\nset obj [MCS new 0]\n$obj init|Org 1 2 3\n}'
|
||||
assert target_text(locate(source), source.replace("|", "")) == "initOrg"
|
||||
source = 'puts 😀; ' + CLASS + '\nset obj [M|CS new 0]'
|
||||
target = locate(source)
|
||||
assert target_text(target, source.replace("|", "")) == "MCS"
|
||||
|
||||
|
||||
def test_psc_definition_navigation_uses_library_uri(tmp_path, monkeypatch):
|
||||
import lsp_server
|
||||
from test_psc_classes import setup_project, caller, CLASS as LIBRARY_SOURCE
|
||||
|
||||
server, root, _, script = setup_project(tmp_path, monkeypatch)
|
||||
server.refresh_psc_scripts([root])
|
||||
for word, source in [
|
||||
("MCS", "set obj [MCS new]"),
|
||||
("initOrg", "set obj [MCS new]\n$obj initOrg 1 2 3"),
|
||||
]:
|
||||
uri, _ = caller(server, root, source)
|
||||
lines = source.splitlines()
|
||||
position = lsp.Position(line=len(lines) - 1, character=lines[-1].index(word) + 1)
|
||||
result = lsp_server.goto_definition(lsp.DefinitionParams(text_document=lsp.TextDocumentIdentifier(uri=uri), position=position))
|
||||
assert result is not None and len(result) == 1
|
||||
assert server.paths_equal(script, uris.to_fs_path(result[0].uri))
|
||||
assert target_text(result[0], LIBRARY_SOURCE) == word
|
||||
@@ -2,27 +2,13 @@
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "CDL Support",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comment"
|
||||
},
|
||||
{
|
||||
"include": "#function"
|
||||
},
|
||||
{
|
||||
"include": "#variables"
|
||||
},
|
||||
{
|
||||
"include": "#types"
|
||||
},
|
||||
{
|
||||
"include": "#keywords"
|
||||
},
|
||||
{
|
||||
"include": "#numbers"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
}
|
||||
{ "include": "#comment" },
|
||||
{ "include": "#function" },
|
||||
{ "include": "#variables" },
|
||||
{ "include": "#types" },
|
||||
{ "include": "#keywords" },
|
||||
{ "include": "#numbers" },
|
||||
{ "include": "#strings" }
|
||||
],
|
||||
"repository": {
|
||||
"keywords": {
|
||||
@@ -70,11 +56,7 @@
|
||||
{
|
||||
"name": "keyword.control.cdl",
|
||||
"match": "TYPE\\s+(o|g|i|d|b|s)\\b",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "storage.type.cs"
|
||||
}
|
||||
}
|
||||
"captures": { "1": { "name": "storage.type.cs" } }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -83,38 +65,22 @@
|
||||
{
|
||||
"name": "keyword.control.cdl",
|
||||
"match": "\\b(PARAM|EVENT)\\s+([a-zA-Z_]\\w*)\\b",
|
||||
"captures": {
|
||||
"2": {
|
||||
"name": "variable.other.cdl"
|
||||
}
|
||||
}
|
||||
"captures": { "2": { "name": "variable.other.cdl" } }
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.cdl",
|
||||
"match": "\\bMACHINE\\s+([a-zA-Z_]\\w*)\\b",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "variable.other.cdl"
|
||||
}
|
||||
}
|
||||
"captures": { "1": { "name": "variable.other.cdl" } }
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.cdl",
|
||||
"match": "\\bCATEGORY\\s+((MILL|LATHE|DRILL)(\\s+(MILL|LATHE|DRILL))*)\\b",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "variable.language.cdl"
|
||||
}
|
||||
}
|
||||
"match": "\\bCATEGORY\\s+((MILL|LATHE|DRILL|INVALID)(\\s+(MILL|LATHE|DRILL|INVALID))*)\\b",
|
||||
"captures": { "1": { "name": "variable.language.cdl" } }
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.cdl",
|
||||
"match": "\\bTOGGLE\\s+(OFF|ON|off|on)\\b",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "variable.other.constant"
|
||||
}
|
||||
}
|
||||
"captures": { "1": { "name": "variable.other.constant" } }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -123,11 +89,7 @@
|
||||
{
|
||||
"name": "storage.type.function.cdl",
|
||||
"match": "\\bEVENT\\s+([a-zA-Z_]\\w*)\\b",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "entity.name.function"
|
||||
}
|
||||
}
|
||||
"captures": { "1": { "name": "entity.name.function" } }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
{
|
||||
"include": "#comment"
|
||||
},
|
||||
{
|
||||
"include": "#escaped-quoted-strings"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
},
|
||||
@@ -28,6 +31,31 @@
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"escaped-quoted-strings": {
|
||||
"name": "string.quoted.double.def",
|
||||
"begin": "\\\\\"",
|
||||
"end": "\\\\\"",
|
||||
"beginCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.string.begin.def"
|
||||
}
|
||||
},
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.string.end.def"
|
||||
}
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.def",
|
||||
"match": "\\\\."
|
||||
},
|
||||
{
|
||||
"name": "variable.other.property",
|
||||
"match": "\\$[_a-zA-Z][_a-zA-Z0-9]*(\\(\\$?[_a-zA-Z][_a-zA-Z0-9]*(,\\$?[_a-zA-Z][_a-zA-Z0-9]*)*\\))?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keywords": {
|
||||
"patterns": [
|
||||
{
|
||||
@@ -151,4 +179,4 @@
|
||||
}
|
||||
},
|
||||
"scopeName": "source.def"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const assert = require("node:assert/strict")
|
||||
const fs = require("node:fs")
|
||||
const path = require("node:path")
|
||||
const vm = require("node:vm")
|
||||
const { test } = require("node:test")
|
||||
const { transformSync } = require("esbuild")
|
||||
|
||||
const source = fs.readFileSync(path.join(__dirname, "../client/src/common/cdlEventHandler.ts"), "utf8")
|
||||
const compiled = transformSync(source, { loader: "ts", format: "cjs" }).code
|
||||
const context = { module: { exports: {} } }
|
||||
vm.runInNewContext(compiled, context)
|
||||
const { cdlEventHandlerAtLine, createCdlEventHandlerSnippet } = context.module.exports
|
||||
|
||||
test("TOGGLE Off adds the defined globals to the event handler", () => {
|
||||
const cdl = `EVENT GDM_header
|
||||
{
|
||||
PARAM product_status {
|
||||
TYPE o
|
||||
OPTIONS "Serie", "Prototyp"
|
||||
}
|
||||
PARAM stm_param_mpf_name
|
||||
{
|
||||
TYPE s
|
||||
TOGGLE Off
|
||||
}
|
||||
PARAM stm_param_wks_path {
|
||||
TYPE s
|
||||
TOGGLE Off
|
||||
}
|
||||
}`
|
||||
const snippet = createCdlEventHandlerSnippet(cdlEventHandlerAtLine(cdl, 0))
|
||||
assert.match(snippet, /global mom_stm_param_mpf_name_defined/)
|
||||
assert.match(snippet, /global mom_stm_param_wks_path_defined/)
|
||||
assert.match(snippet, /global mom_product_status\n/)
|
||||
assert.doesNotMatch(snippet, /mom_product_status_defined/)
|
||||
})
|
||||
|
||||
test("toggle detection ignores comments, strings, other events, and TOGGLE On", () => {
|
||||
const cdl = `EVENT first {
|
||||
PARAM plain { TYPE s UI_LABEL "TOGGLE Off { ignored }" }
|
||||
PARAM enabled { TOGGLE On }
|
||||
PARAM commented { TYPE s # TOGGLE Off
|
||||
}
|
||||
PARAM mom_disabled { TOGGLE off }
|
||||
}
|
||||
EVENT second { PARAM other { TOGGLE Off } }`
|
||||
const snippet = createCdlEventHandlerSnippet(cdlEventHandlerAtLine(cdl, 0))
|
||||
assert.match(snippet, /global mom_disabled_defined/)
|
||||
assert.equal((snippet.match(/_defined/g) ?? []).length, 1)
|
||||
assert.doesNotMatch(snippet, /mom_mom_|mom_other/)
|
||||
})
|
||||
+322
-39
@@ -6,24 +6,24 @@ set te875st 11111
|
||||
#set ::custom_flag(from_move,$::mom_path_name) 1
|
||||
|
||||
if {$main == 1 && 1 == 1} {
|
||||
puts "main"
|
||||
puts "main"
|
||||
}
|
||||
|
||||
proc test {} {
|
||||
puts "main"
|
||||
proc llll {} {}
|
||||
set rrrrrrr
|
||||
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
|
||||
|
||||
MOM_abort
|
||||
|
||||
namespace eval myns {
|
||||
proc add {a b} {
|
||||
set sum [expr {$a + $b}]
|
||||
return $sum
|
||||
}
|
||||
set config "debug"
|
||||
proc add {a b} {
|
||||
set sum [expr {$a + $b}]
|
||||
return $sum
|
||||
}
|
||||
set config "debug"
|
||||
}
|
||||
|
||||
#_________________________________________________________________________________________________
|
||||
@@ -31,7 +31,7 @@ namespace eval myns {
|
||||
# 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
|
||||
LIB_GE_message [string repeat $type $length] "output_$output" $line_num
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ SERVICE_spacer_output "*" 2 0 0
|
||||
# Function to delete the file
|
||||
#_________________________________________________________________________________________________
|
||||
proc SERVICE_remove_file {file} {
|
||||
if {![SERVICE_check_file_exists $file]} {return}
|
||||
MOM_remove_file $file
|
||||
if {![SERVICE_check_file_exists $file]} {return}
|
||||
MOM_remove_file $file
|
||||
}
|
||||
|
||||
#_________________________________________________________________________________________________
|
||||
@@ -51,8 +51,8 @@ proc SERVICE_remove_file {file} {
|
||||
# Function to check if the file exists
|
||||
#_________________________________________________________________________________________________
|
||||
proc SERVICE_check_file_exists {file} {
|
||||
if {[file exists $file]} {return 1}
|
||||
return 0
|
||||
if {[file exists $file]} {return 1}
|
||||
return 0
|
||||
}
|
||||
|
||||
#_________________________________________________________________________________________________
|
||||
@@ -60,19 +60,19 @@ proc SERVICE_check_file_exists {file} {
|
||||
# Ask UDE Info for the Tool
|
||||
#_________________________________________________________________________________________________
|
||||
proc SERVICE_ask_ude_tool {pos ude_name tool_name} {
|
||||
MOM_ask_ude_info $tool_name "tool" $pos
|
||||
MOM_ask_ude_info $tool_name "tool" $pos
|
||||
|
||||
if {[lsearch $::mom_result $ude_name] != -1} {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
if {[lsearch $::mom_result $ude_name] != -1} {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
proc MOM_dummy_event_start {} {
|
||||
global mom_new_item
|
||||
global mom_new_item_end
|
||||
global mom_new_item
|
||||
global mom_new_item_end
|
||||
|
||||
#Put your UDE Handler Tcl here
|
||||
#Put your UDE Handler Tcl here
|
||||
}
|
||||
|
||||
#_________________________________________________________________________________________________
|
||||
@@ -80,12 +80,12 @@ proc MOM_dummy_event_start {} {
|
||||
# Ask UDE Info for the Operation
|
||||
#_________________________________________________________________________________________________
|
||||
proc SERVICE_ask_ude_operation {pos ude_name path_name} {
|
||||
MOM_ask_ude_info $path_name "operation" $pos
|
||||
MOM_ask_ude_info $path_name "operation" $pos
|
||||
|
||||
if {[lsearch $::mom_result $ude_name] != -1} {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
if {[lsearch $::mom_result $ude_name] != -1} {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
#_________________________________________________________________________________________________
|
||||
@@ -96,7 +96,7 @@ proc SERVICE_ask_ude_operation {pos ude_name path_name} {
|
||||
# restore
|
||||
#_________________________________________________________________________________________________
|
||||
proc SERVICE_output_handling {handler} {
|
||||
set ::lib_ge(hidden_output) $handler
|
||||
set ::lib_ge(hidden_output) $handler
|
||||
}
|
||||
|
||||
#_________________________________________________________________________________________________
|
||||
@@ -105,22 +105,305 @@ proc SERVICE_output_handling {handler} {
|
||||
# this function is called in start of program
|
||||
#_________________________________________________________________________________________________
|
||||
proc SERVICE_get_tool_data {} {
|
||||
global mom_tool_data
|
||||
global mom_operation_info
|
||||
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)
|
||||
}
|
||||
}
|
||||
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"
|
||||
MOM_do_template "end_of_program_rewind"
|
||||
} EndOfProgramRewind
|
||||
|
||||
|
||||
SERVICE_remove_file "test"
|
||||
|
||||
|
||||
oo::class create MCS {
|
||||
variable values
|
||||
|
||||
constructor {args} {
|
||||
for {set i 0} {$i <= 11} {incr i} {
|
||||
my initValue $i 0.
|
||||
}
|
||||
}
|
||||
destructor {
|
||||
#puts "[self] is now deleted"
|
||||
}
|
||||
|
||||
method fformat {value {precision 7}} {return [expr {round( 10.0 ** $precision * $value) / (10.0 ** $precision)}]}
|
||||
method radtodeg {rad} {return [expr {$rad*180.0/(4.0*atan(1.0))}]}
|
||||
method degtorad {deg} {return [expr {$deg*(4.0*atan(1.0))/180.0}]}
|
||||
|
||||
method duplicate {args} {return [[MCS new] initMCS [self]]}
|
||||
method type {args} {return "::MCS"}
|
||||
|
||||
method toArray {args} {
|
||||
for {set i 0} {$i < [array size values]} {incr i} {
|
||||
append arr [string cat $i " "] ; append arr [string cat $values($i) " "]
|
||||
}
|
||||
return [string trimright $arr]
|
||||
}
|
||||
method toLst {args} {
|
||||
for {set i 0} {$i < [array size values]} {incr i} {
|
||||
lappend lst $values($i)
|
||||
}
|
||||
return $lst
|
||||
}
|
||||
method toStr {args} {
|
||||
for {set i 0} {$i < [array size values]} {incr i} {
|
||||
if {$i>0} {set str [string cat $str ", "]}
|
||||
append str $values($i)
|
||||
}
|
||||
return $str
|
||||
}
|
||||
method toStrF {{precision 7}} {
|
||||
for {set i 0} {$i < [array size values]} {incr i} {
|
||||
if {$i>0} {set str [string cat $str ", "]}
|
||||
append str [my fformat $values($i) 10]
|
||||
}
|
||||
return $str
|
||||
}
|
||||
|
||||
method initValue {i value} {set values($i) [expr double($value)] ; return [self]}
|
||||
method initArray {a} {upvar $a arr ; for {set i 0} {$i < [array size arr]} {incr i} {my initValue $i $arr($i)} ; return [self]}
|
||||
method initArrayLst {arr_lst} {array set arr $arr_lst ; for {set i 0} {$i < [array size arr]} {incr i} {my initValue $i $arr($i)} ; return [self]}
|
||||
method initLst {lst} {for {set i 0} {$i < [llength $lst]} {incr i} {my initValue $i [lindex $lst $i]} ; return [self]}
|
||||
|
||||
method initMCS {mcs} {
|
||||
my identity
|
||||
set p [$mcs org] ; my initOrgP $p ; $p destroy
|
||||
set v [$mcs xVct] ; my initXvctV $v ; $v destroy
|
||||
set v [$mcs yVct] ; my initYvctV $v ; $v destroy
|
||||
set v [$mcs zVct] ; my initZvctV $v ; $v destroy
|
||||
return [self]
|
||||
}
|
||||
method initMTX {mtx} {
|
||||
set p [$mtx org] ; my initOrg $p ; $p destroy
|
||||
set v [$mtx xVct] ; my initXvct $v ; $v destroy
|
||||
set v [$mtx yVct] ; my initYvct $v ; $v destroy
|
||||
set v [$mtx zVct] ; my initZvct $v ; $v destroy
|
||||
return [self]
|
||||
}
|
||||
|
||||
method initOrg {dx dy dz} {my initValue 0 $dx ; my initValue 1 $dy ; my initValue 2 $dz ; return [self]}
|
||||
method initOrgP {p} {return [my initOrg [$p x] [$p y] [$p z]]}
|
||||
|
||||
method initXvct {dx dy dz} {my initValue 3 $dx ; my initValue 4 $dy ; my initValue 5 $dz ; return [self]}
|
||||
method initXvctV {v} {return [my initXvct [$v x] [$v y] [$v z]]}
|
||||
|
||||
method initYvct {dx dy dz} {my initValue 6 $dx ; my initValue 7 $dy ; my initValue 8 $dz ; return [self]}
|
||||
method initYvctV {v} {return [my initYvct [$v x] [$v y] [$v z]]}
|
||||
|
||||
method initZvct {dx dy dz} {my initValue 9 $dx ; my initValue 10 $dy ; my initValue 11 $dz ; return [self]}
|
||||
method initZvctV {v} {return [my initZvct [$v x] [$v y] [$v z]]}
|
||||
|
||||
method initOrgArray {a} {upvar $a arr ; return [my initOrg $arr(0) $arr(1) $arr(2)]}
|
||||
method initVctsArray {a} {
|
||||
upvar $a arr
|
||||
my initXvct $arr(0) $arr(1) $arr(2)
|
||||
my initYvct $arr(3) $arr(4) $arr(5)
|
||||
my initZvct $arr(6) $arr(7) $arr(8)
|
||||
return [self]
|
||||
}
|
||||
|
||||
method value {i} {return $values($i)}
|
||||
|
||||
method org {args} {return [Point3D new $values(0) $values(1) $values(2)]}
|
||||
method xVct {args} {return [Vector3D new $values(3) $values(4) $values(5)]}
|
||||
method yVct {args} {return [Vector3D new $values(6) $values(7) $values(8)]}
|
||||
method zVct {args} {return [Vector3D new $values(9) $values(10) $values(11)]}
|
||||
|
||||
method same {mcs {precision 7}} {
|
||||
set org [[self] org] ; set org_1 [$mcs org]
|
||||
set vx [[self] xVct] ; set vx_1 [$mcs xVct]
|
||||
set vy [[self] yVct] ; set vy_1 [$mcs yVct]
|
||||
set vz [[self] zVct] ; set vz_1 [$mcs zVct]
|
||||
|
||||
if {[$org same $org_1 $precision] && [$vx same $vx_1 $precision] && [$vy same $vy_1 $precision] && [$vz same $vz_1 $precision]} {
|
||||
set value 1
|
||||
} else {
|
||||
set value 0
|
||||
}
|
||||
|
||||
$org destroy ; $org_1 destroy
|
||||
$vx destroy ; $vx_1 destroy
|
||||
$vy destroy ; $vy_1 destroy
|
||||
$vz destroy ; $vz_1 destroy
|
||||
return $value
|
||||
}
|
||||
}
|
||||
|
||||
set point [Point3D new]
|
||||
|
||||
$point add 1 2 3
|
||||
|
||||
$custom_flag(from_move,$::mom_path_name)
|
||||
|
||||
oo::class create Point3D {
|
||||
variable x 0. ; variable y 0. ; variable z 0.
|
||||
|
||||
constructor {{dx 0.} {dy 0.} {dz 0.}} {my initX $dx ; my initY $dy ; my initZ $dz}
|
||||
destructor {
|
||||
#puts "[self] is now deleted"
|
||||
}
|
||||
|
||||
method fformat {value {precision 7}} {return [expr {round( 10.0 ** $precision * $value) / (10.0 ** $precision)}]}
|
||||
method radtodeg {rad} {return [expr {$rad*180.0/(4.0*atan(1.0))}]}
|
||||
method degtorad {deg} {return [expr {$deg*(4.0*atan(1.0))/180.0}]}
|
||||
|
||||
method duplicate {args} {return [[Point3D new] initP [self]]}
|
||||
method type {args} {return "::Point3D"}
|
||||
|
||||
method toArray {{i 0}} {return [list [incr i 0] $x [incr i] $y [incr i] $z]}
|
||||
method toLst {args} {return [list $x $y $z]}
|
||||
method toStr {args} {return "$x, $y, $z"}
|
||||
method toStrF {{precision 7}} {return "[my fformat $x $precision], [my fformat $y $precision], [my fformat $z $precision]"}
|
||||
|
||||
method x {args} {return $x}
|
||||
method y {args} {return $y}
|
||||
method z {args} {return $z}
|
||||
|
||||
method initX {dx} {set x [expr double($dx)]}
|
||||
method initY {dy} {set y [expr double($dy)]}
|
||||
method initZ {dz} {set z [expr double($dz)]}
|
||||
|
||||
method init {dx dy dz} {my initX $dx ; my initY $dy ; my initZ $dz ; return [self]}
|
||||
method initArray {a} {upvar $a arr ; return [my init $arr(0) $arr(1) $arr(2)]}
|
||||
method initArrayLst {arr_lst} {array set arr $arr_lst ; return [my init $arr(0) $arr(1) $arr(2)]}
|
||||
method initLst {lst} {return [my init [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
|
||||
method initP {p} {return [my init [$p x] [$p y] [$p z]]}
|
||||
|
||||
method reset {args} {my init 0. 0. 0. ; return [self]}
|
||||
|
||||
method add {dx dy dz} {my init [expr {$x + $dx}] [expr {$y + $dy}] [expr {$z + $dz}] ; return [self]}
|
||||
method addArray {a} {upvar $a arr ; return [my add $arr(0) $arr(1) $arr(2)]}
|
||||
method addArrayLst {arr_lst} {array set arr $arr_lst ; return [my add $arr(0) $arr(1) $arr(2)]}
|
||||
method addLst {lst} {return [my add [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
|
||||
method addP {p} {return [my add [$p x] [$p y] [$p z]]}
|
||||
|
||||
method sub {dx dy dz} {my init [expr {$x - $dx}] [expr {$y - $dy}] [expr {$z - $dz}] ; return [self]}
|
||||
method subArray {a} {upvar $a arr ; return [my sub $arr(0) $arr(1) $arr(2)]}
|
||||
method subArrayLst {arr_lst} {array set arr $arr_lst ; return [my sub $arr(0) $arr(1) $arr(2)]}
|
||||
method subLst {lst} {return [my sub [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
|
||||
method subP {p} {return [my sub [$p x] [$p y] [$p z]]}
|
||||
|
||||
method dist {dx dy dz} {return [expr {sqrt([expr {$x - $dx}]**2 + [expr {$y - $dy}]**2 + [expr {$z - $dz}]**2)}]}
|
||||
method distArray {a} {upvar $a arr ; return [my dist $arr(0) $arr(1) $arr(2)]}
|
||||
method distArrayLst {arr_lst} {array set arr $arr_lst ; return [my dist $arr(0) $arr(1) $arr(2)]}
|
||||
method distLst {lst} {return [my dist [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
|
||||
method distP {p} {return [my dist [$p x] [$p y] [$p z]]}
|
||||
|
||||
method dist_to_line {pl1 pl2} {
|
||||
set pl [[$pl2 duplicate] subP $pl1]
|
||||
set pp [[$pl1 duplicate] subP [self]]
|
||||
|
||||
set vl [[Vector3D new] initV $pl]
|
||||
set vp [[Vector3D new] initV $pp]
|
||||
|
||||
set l [$vl magnitude]
|
||||
|
||||
if {[my fformat $l 5]==0} {
|
||||
set value 0.
|
||||
} else {
|
||||
set vc [$vl cross $vp]
|
||||
set value [expr [$vc magnitude]/$l]
|
||||
$vc destroy
|
||||
}
|
||||
|
||||
$pl destroy ; $vl destroy
|
||||
$pp destroy ; $vp destroy
|
||||
return $value
|
||||
}
|
||||
method dist_to_lineV {v} {
|
||||
set p1 [[Point3D new] init 0. 0. 0.]
|
||||
set p2 [[Point3D new] initP $v]
|
||||
|
||||
set value [my dist_to_line $p1 $p2]
|
||||
$p1 destroy
|
||||
$p2 destroy
|
||||
return $value
|
||||
}
|
||||
|
||||
#midpoint method to calculate the midpoint between this point and another point. This can be done by averaging the x, y, and z coordinates of the two points.
|
||||
method midpoint {dx dy dz} {return [Point3D new [expr {double([[self] x] + $dx)/2}] [expr {double([[self] y] + $dy)/2}] [expr {double([[self] z] + $dz)/2}]]}
|
||||
method midpointArr {a} {upvar $a arr ; return [my midpoint $arr(0) $arr(1) $arr(2)]}
|
||||
method midpointArrLst {arr} {array set arr $arr_lst ; return [my midpoint $arr(0) $arr(1) $arr(2)]}
|
||||
method midpointLst {lst} {return [my midpoint [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
|
||||
method midpointP {p} {return [my midpoint [$p x] [$p y] [$p z]]}
|
||||
|
||||
method same {p {precision 7}} {
|
||||
if {[my fformat [expr {[[self] x] - [$p x]}] $precision] == 0. && [my fformat [expr {[[self] y] - [$p y]}] $precision] == 0. && [my fformat [expr {[[self] z] - [$p z]}] $precision] == 0.} {return 1} else {return 0}
|
||||
}
|
||||
|
||||
#This method takes three arguments: a vector object "axis" as the axis of rotation, an angle of rotation, and a point object "center" as the center of rotation.
|
||||
#First, it translates the point by subtracting the center of rotation,
|
||||
#then it applies the Rodrigues' rotation formula to calculate the new coordinates of the point, and finally,
|
||||
#it translates back the point by adding the center of rotation.
|
||||
#The center parameter is defined as an optional parameter with a default value of a new Point3D object, initialized with the values (0,0,0).
|
||||
method rotAround {u v w angle {x0 0.} {y0 0.} {z0 0.}} {
|
||||
set x1 [expr {double($x) - $x0}] ; set y1 [expr {double($y) - $y0}] ; set z1 [expr {double($z) - $z0}]
|
||||
set u [expr {double($u)}] ; set v [expr {double($v)}] ; set w [expr {double($w)}]
|
||||
set a [my degtorad $angle]
|
||||
|
||||
set coss [expr {cos($a)}]
|
||||
set sinn [expr {sin($a)}]
|
||||
|
||||
set x2 [expr {$u*($u*$x1 + $v*$y1 + $w*$z1)*(1 - $coss) + $x1*$coss + (-$w*$y1 + $v*$z1)*$sinn + $x0}]
|
||||
set y2 [expr {$v*($u*$x1 + $v*$y1 + $w*$z1)*(1 - $coss) + $y1*$coss + ($w*$x1 - $u*$z1)*$sinn + $y0}]
|
||||
set z2 [expr {$w*($u*$x1 + $v*$y1 + $w*$z1)*(1 - $coss) + $z1*$coss + (-$v*$x1 + $u*$y1)*$sinn + $z0}]
|
||||
|
||||
my init $x2 $y2 $z2
|
||||
return [self]
|
||||
}
|
||||
method rotAroundLst {aLst angle {cLst {0. 0. 0.}}} {
|
||||
return [my rotAround [lindex $aLst 0] [lindex $aLst 1] [lindex $aLst 2] $angle [lindex $cLst 0] [lindex $cLst 1] [lindex $cLst 2]]
|
||||
}
|
||||
method rotAroundVP {axis angle {center {}}} {
|
||||
if {[catch {set s [$center type]}] || (![string equal [$center type] "::Point3D"] && ![string equal [$center type] "::Vector3D"])} {
|
||||
set center [Point3D new]
|
||||
set b_center true
|
||||
}
|
||||
|
||||
my rotAround [$axis x] [$axis y] [$axis z] $angle [$center x] [$center y] [$center z]
|
||||
if {[info exists b_center]} {$center destroy}
|
||||
return [self]
|
||||
}
|
||||
|
||||
method transform {mtx_from mtx_to} {
|
||||
if {[catch {set s [$mtx_from type]}] || ![string equal [$mtx_from type] "::Matrix4x4"]} {
|
||||
set mtx_from [[Matrix4x4 new] identity]
|
||||
set b_mtx_from true
|
||||
}
|
||||
if {[catch {set s [$mtx_to type]}] || ![string equal [$mtx_to type] "::Matrix4x4"]} {
|
||||
set mtx_to [[Matrix4x4 new] identity]
|
||||
set b_mtx_to true
|
||||
}
|
||||
|
||||
set m [[Matrix4x4 new] identity]
|
||||
$m initValue 0 [my x]
|
||||
$m initValue 4 [my y]
|
||||
$m initValue 8 [my z]
|
||||
$m initValue 12 1.
|
||||
|
||||
set trs [[[[$mtx_from duplicate] inverse] multiply $mtx_to false] inverse]
|
||||
$m multiply $trs true
|
||||
|
||||
my init [$m value 0] [$m value 4] [$m value 8]
|
||||
$m destroy
|
||||
$trs destroy
|
||||
if {[info exists b_mtx_from]} {$mtx_from destroy}
|
||||
if {[info exists b_mtx_to]} {$mtx_to destroy}
|
||||
return [self]
|
||||
}
|
||||
}
|
||||
|
||||
set v1 [MCS new]
|
||||
|
||||
$v1 initArray
|
||||
|
||||
Reference in New Issue
Block a user