feat(server): index PSC scripts and provide cross-file TclOO navigation/completions
Add PSC (.psc) indexing and share TclOO class metadata across files so class definitions discovered via PSC layers can be used for completions, signature help, inlay hints, and "go to definition". Key behavior changes: - Client file watcher now includes *.psc and .vscode launch paths/tests updated to use the postprocessor test folder; .gitignore updated to ignore that folder. - Server watches .psc changes and refreshes a PSC script index; new tools/tcloo_navigation.py exposes tcloo_definition used by the language server to resolve cross-file class/constructor/method definitions. - Language server uses class_snapshot(document.path) when producing TclOO completions, signature help, and inlay hints so resolved class metadata is available across files. Also includes related docs/changelog updates, minor code formatting cleanups, and added tests for PSC/TclOO behavior.
This commit is contained in:
@@ -10,3 +10,4 @@ __pycache__
|
|||||||
*.g4
|
*.g4
|
||||||
.antlr
|
.antlr
|
||||||
.claude
|
.claude
|
||||||
|
/test/postprocessor/
|
||||||
|
|||||||
Vendored
+4
-4
@@ -13,7 +13,7 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
|
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
"${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",
|
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||||
"${workspaceFolder}",
|
"${workspaceFolder}",
|
||||||
"${workspaceFolder}/test/test.tcl"
|
"${workspaceFolder}/test/postprocessor"
|
||||||
],
|
],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
|
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
"${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",
|
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||||
"${workspaceFolder}",
|
"${workspaceFolder}",
|
||||||
"${workspaceFolder}/test/test.tcl"
|
"${workspaceFolder}/test/postprocessor"
|
||||||
],
|
],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
## Unreleased
|
## 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
|
- 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
|
- 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
|
- Show signature help and parameter inlay hints for resolved TclOO methods and constructors, including optional and variadic arguments
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ A comprehensive VS Code extension providing language support and remote debuggin
|
|||||||
- **Call Hierarchy** - Traces incoming and outgoing calls between custom TCL procedures and MOM event handlers
|
- **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
|
- **Document Highlights** - Highlights all reads, writes, and calls of the symbol under the cursor
|
||||||
- **Context-aware Completion** - Prioritizes local symbols and suggests variables, procedures, namespaces, paths, Tcl subcommands, valid argument values, and options 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`
|
- **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
|
- **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
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import {
|
import { cdlEventHandlerAtLine, createCdlEventHandlerSnippet } from "./cdlEventHandler"
|
||||||
cdlEventHandlerAtLine,
|
|
||||||
createCdlEventHandlerSnippet
|
|
||||||
} from "./cdlEventHandler"
|
|
||||||
|
|
||||||
const MACHINE_HEADER_REGEX = /^MACHINE\s+\S+/
|
const MACHINE_HEADER_REGEX = /^MACHINE\s+\S+/
|
||||||
|
|
||||||
@@ -65,10 +62,7 @@ export function diagnosticHandler(document: vscode.TextDocument) {
|
|||||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||||
const text = document.getText()
|
const text = document.getText()
|
||||||
if (!isFirstLineMachine(text)) {
|
if (!isFirstLineMachine(text)) {
|
||||||
const range = new vscode.Range(
|
const range = new vscode.Range(document.positionAt(0), document.positionAt(text.length))
|
||||||
document.positionAt(0),
|
|
||||||
document.positionAt(text.length)
|
|
||||||
)
|
|
||||||
const diagnostic = new vscode.Diagnostic(
|
const diagnostic = new vscode.Diagnostic(
|
||||||
range,
|
range,
|
||||||
"The first line should contain 'MACHINE'.",
|
"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) {
|
export function completionHandlerCdl(document: vscode.TextDocument, position: vscode.Position) {
|
||||||
const linePrefix = document.lineAt(position).text.substring(0, position.character)
|
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 ")) {
|
if (linePrefix.endsWith("TYPE ")) {
|
||||||
return [
|
return [
|
||||||
@@ -193,8 +187,7 @@ export async function definitionCdlEventHandler(
|
|||||||
.filter(
|
.filter(
|
||||||
(symbol) =>
|
(symbol) =>
|
||||||
symbol.kind === vscode.SymbolKind.Function &&
|
symbol.kind === vscode.SymbolKind.Function &&
|
||||||
(symbol.name === handlerName ||
|
(symbol.name === handlerName || symbol.name.endsWith(`::${handlerName}`))
|
||||||
symbol.name.endsWith(`::${handlerName}`))
|
|
||||||
)
|
)
|
||||||
.map((symbol) => symbol.location)
|
.map((symbol) => symbol.location)
|
||||||
if (indexedLocations.length > 0) {
|
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.
|
// The Tcl language server may still be starting; use the file fallback below.
|
||||||
}
|
}
|
||||||
|
|
||||||
const declaration = new RegExp(
|
const declaration = new RegExp(`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`)
|
||||||
`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`
|
|
||||||
)
|
|
||||||
const tclFiles = await vscode.workspace.findFiles(
|
const tclFiles = await vscode.workspace.findFiles(
|
||||||
"**/*.tcl",
|
"**/*.tcl",
|
||||||
"**/{.git,.nox,.venv,dist,node_modules,out}/**"
|
"**/{.git,.nox,.venv,dist,node_modules,out}/**"
|
||||||
@@ -233,12 +224,7 @@ export async function definitionCdlEventHandler(
|
|||||||
locations.push(
|
locations.push(
|
||||||
new vscode.Location(
|
new vscode.Location(
|
||||||
uri,
|
uri,
|
||||||
new vscode.Range(
|
new vscode.Range(lineNumber, start, lineNumber, start + handlerName.length)
|
||||||
lineNumber,
|
|
||||||
start,
|
|
||||||
lineNumber,
|
|
||||||
start + handlerName.length
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ async function createServer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Options to control the language client
|
// Options to control the language client
|
||||||
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.tcl")
|
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.{tcl,psc}")
|
||||||
const clientOptions: LanguageClientOptions = {
|
const clientOptions: LanguageClientOptions = {
|
||||||
// Register the server for python documents
|
// Register the server for python documents
|
||||||
documentSelector: isVirtualWorkspace()
|
documentSelector: isVirtualWorkspace()
|
||||||
|
|||||||
+102
-177
@@ -79,6 +79,7 @@ from tools.tcloo_arguments import method_signature_help
|
|||||||
from tclint.lexer import TclSyntaxError
|
from tclint.lexer import TclSyntaxError
|
||||||
from tools.tcloo_completion import parse_completion_source, tcloo_completions
|
from tools.tcloo_completion import parse_completion_source, tcloo_completions
|
||||||
from tools.tcloo_symbols import class_completion_items
|
from tools.tcloo_symbols import class_completion_items
|
||||||
|
from tools.tcloo_navigation import tcloo_definition
|
||||||
from tools.tcl_command_completion import (
|
from tools.tcl_command_completion import (
|
||||||
TCL_COMMAND_ITEMS,
|
TCL_COMMAND_ITEMS,
|
||||||
TCL_COMMAND_NAMES,
|
TCL_COMMAND_NAMES,
|
||||||
@@ -92,40 +93,18 @@ GLOBAL_SETTINGS = {}
|
|||||||
|
|
||||||
|
|
||||||
MAX_WORKERS = 5
|
MAX_WORKERS = 5
|
||||||
LSP_SERVER = TclLanguageServer(
|
LSP_SERVER = TclLanguageServer(name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS)
|
||||||
name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS
|
|
||||||
)
|
|
||||||
|
|
||||||
BUILTIN_PROC_NAMES = {
|
BUILTIN_PROC_NAMES = {item.label for item in standard_items.tcl_keyword_list + standard_items.nx_procs} | set(TCL_COMMAND_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}
|
BUILTIN_VARIABLE_NAMES = {item.label for item in standard_items.nx_variables}
|
||||||
_TCL_COMMAND_ITEMS_BY_LABEL = {
|
_TCL_COMMAND_ITEMS_BY_LABEL = {item.label: item for item in TCL_COMMAND_ITEMS}
|
||||||
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]
|
||||||
}
|
|
||||||
_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_TCL_LABELS = {item.label for item in standard_items.tcl_keyword_list}
|
||||||
STATIC_COMPLETION_ITEMS = tuple(
|
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)
|
||||||
_TCL_KEYWORD_ITEMS
|
STATIC_VARIABLE_ITEMS = {item.label: item for item in standard_items.nx_variables}
|
||||||
+ [item for item in TCL_COMMAND_ITEMS if item.label not in _STATIC_TCL_LABELS]
|
BUILTIN_INLAY_SIGNATURES = build_builtin_inlay_signatures(standard_items.json_data.get("MOM_procs", []))
|
||||||
+ 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 = {}
|
BUILTIN_HOVER_ITEMS = {}
|
||||||
for _hover_item in (
|
for _hover_item in standard_items.json_data.get("MOM_procs", []) + standard_items.json_data.get("mom_variables", []):
|
||||||
standard_items.json_data.get("MOM_procs", [])
|
|
||||||
+ standard_items.json_data.get("mom_variables", [])
|
|
||||||
):
|
|
||||||
BUILTIN_HOVER_ITEMS.setdefault(_hover_item.get("label"), _hover_item)
|
BUILTIN_HOVER_ITEMS.setdefault(_hover_item.get("label"), _hover_item)
|
||||||
|
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
@@ -236,12 +215,23 @@ def did_rename_files(params: lsp.RenameFilesParams) -> None:
|
|||||||
def did_change_watched_files(params: lsp.DidChangeWatchedFilesParams) -> None:
|
def did_change_watched_files(params: lsp.DidChangeWatchedFilesParams) -> None:
|
||||||
"""Keep indexes for closed Tcl files synchronized with disk changes."""
|
"""Keep indexes for closed Tcl files synchronized with disk changes."""
|
||||||
for change in params.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:
|
if change.type == lsp.FileChangeType.Deleted:
|
||||||
LSP_SERVER.remove_file_state(change.uri)
|
LSP_SERVER.remove_file_state(change.uri)
|
||||||
else:
|
else:
|
||||||
_index_tcl_file_from_disk(change.uri)
|
_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_SERVER.feature(
|
||||||
lsp.TEXT_DOCUMENT_DIAGNOSTIC,
|
lsp.TEXT_DOCUMENT_DIAGNOSTIC,
|
||||||
lsp.DiagnosticOptions(
|
lsp.DiagnosticOptions(
|
||||||
@@ -255,9 +245,7 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
|||||||
uri = params.text_document.uri
|
uri = params.text_document.uri
|
||||||
doc = LSP_SERVER.workspace.get_text_document(uri)
|
doc = LSP_SERVER.workspace.get_text_document(uri)
|
||||||
diagnostic_state = LSP_SERVER.diagnostic_snapshot(uri)
|
diagnostic_state = LSP_SERVER.diagnostic_snapshot(uri)
|
||||||
was_cached = (
|
was_cached = diagnostic_state is not None and diagnostic_state[0] == doc.version
|
||||||
diagnostic_state is not None and diagnostic_state[0] == doc.version
|
|
||||||
)
|
|
||||||
if not was_cached:
|
if not was_cached:
|
||||||
LSP_SERVER.compute_diagnostics(doc)
|
LSP_SERVER.compute_diagnostics(doc)
|
||||||
diagnostic_state = LSP_SERVER.diagnostic_snapshot(uri)
|
diagnostic_state = LSP_SERVER.diagnostic_snapshot(uri)
|
||||||
@@ -282,11 +270,13 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
position = params.position
|
position = params.position
|
||||||
source_lines = LSP_SERVER.get_lines(doc)
|
source_lines = LSP_SERVER.get_lines(doc)
|
||||||
oo_items = tcloo_completions(source_lines, position)
|
oo_items = tcloo_completions(source_lines, position, LSP_SERVER.class_snapshot(doc.path))
|
||||||
if oo_items is not None:
|
if oo_items is not None:
|
||||||
return lsp.CompletionList(is_incomplete=False, items=oo_items)
|
return lsp.CompletionList(is_incomplete=False, items=oo_items)
|
||||||
array_items = array_element_completions(
|
array_items = array_element_completions(
|
||||||
source_lines, position, LSP_SERVER.navigation_snapshot().values(),
|
source_lines,
|
||||||
|
position,
|
||||||
|
LSP_SERVER.navigation_snapshot().values(),
|
||||||
str(pathlib.Path(uris.to_fs_path(doc.uri))),
|
str(pathlib.Path(uris.to_fs_path(doc.uri))),
|
||||||
)
|
)
|
||||||
if array_items is not None:
|
if array_items is not None:
|
||||||
@@ -298,20 +288,14 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
argument_completion = None
|
argument_completion = None
|
||||||
if context != CompletionContext.VARIABLE:
|
if context != CompletionContext.VARIABLE:
|
||||||
argument_completion = tcl_argument_completion(source_lines, position)
|
argument_completion = tcl_argument_completion(source_lines, position)
|
||||||
if (
|
if argument_completion is not None and argument_completion.dynamic_kind is None:
|
||||||
argument_completion is not None
|
|
||||||
and argument_completion.dynamic_kind is None
|
|
||||||
):
|
|
||||||
items = ranked_completion_items(
|
items = ranked_completion_items(
|
||||||
((0, item) for item in argument_completion.items),
|
((0, item) for item in argument_completion.items),
|
||||||
CompletionContext.GENERAL,
|
CompletionContext.GENERAL,
|
||||||
)
|
)
|
||||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||||
|
|
||||||
if (
|
if argument_completion is not None and argument_completion.dynamic_kind == DynamicCompletionKind.PATH:
|
||||||
argument_completion is not None
|
|
||||||
and argument_completion.dynamic_kind == DynamicCompletionKind.PATH
|
|
||||||
):
|
|
||||||
dynamic_items: tuple[lsp.CompletionItem, ...] = ()
|
dynamic_items: tuple[lsp.CompletionItem, ...] = ()
|
||||||
if doc.uri.startswith("file:"):
|
if doc.uri.startswith("file:"):
|
||||||
document_path = pathlib.Path(uris.to_fs_path(doc.uri))
|
document_path = pathlib.Path(uris.to_fs_path(doc.uri))
|
||||||
@@ -320,9 +304,7 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
argument_completion,
|
argument_completion,
|
||||||
position,
|
position,
|
||||||
)
|
)
|
||||||
path_candidates = [
|
path_candidates = [(0, item) for item in argument_completion.items]
|
||||||
(0, item) for item in argument_completion.items
|
|
||||||
]
|
|
||||||
path_candidates.extend((10, item) for item in dynamic_items)
|
path_candidates.extend((10, item) for item in dynamic_items)
|
||||||
items = ranked_completion_items(
|
items = ranked_completion_items(
|
||||||
path_candidates,
|
path_candidates,
|
||||||
@@ -335,8 +317,7 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
if (
|
if (
|
||||||
argument_completion is None
|
argument_completion is None
|
||||||
and params.context is not None
|
and params.context is not None
|
||||||
and params.context.trigger_kind
|
and params.context.trigger_kind == lsp.CompletionTriggerKind.TriggerCharacter
|
||||||
== lsp.CompletionTriggerKind.TriggerCharacter
|
|
||||||
and params.context.trigger_character in {" ", "-", "(", ","}
|
and params.context.trigger_character in {" ", "-", "(", ","}
|
||||||
):
|
):
|
||||||
return lsp.CompletionList(is_incomplete=False, items=[])
|
return lsp.CompletionList(is_incomplete=False, items=[])
|
||||||
@@ -347,9 +328,7 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
tree = parse_completion_source(doc.source)
|
tree = parse_completion_source(doc.source)
|
||||||
if tree is None:
|
if tree is None:
|
||||||
return lsp.CompletionList(is_incomplete=False, items=[])
|
return lsp.CompletionList(is_incomplete=False, items=[])
|
||||||
globals_set, procs_locals, proc_ranges = LSP_SERVER.variable_index_for_document(
|
globals_set, procs_locals, proc_ranges = LSP_SERVER.variable_index_for_document(doc, tree)
|
||||||
doc, tree
|
|
||||||
)
|
|
||||||
|
|
||||||
local_names: set[str] = set()
|
local_names: set[str] = set()
|
||||||
for proc_range in proc_ranges:
|
for proc_range in proc_ranges:
|
||||||
@@ -361,16 +340,14 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
candidates: list[tuple[int, lsp.CompletionItem]] = []
|
candidates: list[tuple[int, lsp.CompletionItem]] = []
|
||||||
candidates.extend((0, item) for item in class_completion_items(tree))
|
candidates.extend((0, item) for item in class_completion_items(tree))
|
||||||
for name in sorted(local_names - globals_set):
|
for name in sorted(local_names - globals_set):
|
||||||
candidates.append(
|
candidates.append((
|
||||||
(
|
0,
|
||||||
0,
|
lsp.CompletionItem(
|
||||||
lsp.CompletionItem(
|
label=name,
|
||||||
label=name,
|
kind=lsp.CompletionItemKind.Variable,
|
||||||
kind=lsp.CompletionItemKind.Variable,
|
detail="Local variable",
|
||||||
detail="Local variable",
|
),
|
||||||
),
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for name in sorted(globals_set):
|
for name in sorted(globals_set):
|
||||||
item = STATIC_VARIABLE_ITEMS.get(name)
|
item = STATIC_VARIABLE_ITEMS.get(name)
|
||||||
@@ -380,12 +357,10 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
kind=lsp.CompletionItemKind.Variable,
|
kind=lsp.CompletionItemKind.Variable,
|
||||||
detail="Workspace variable",
|
detail="Workspace variable",
|
||||||
)
|
)
|
||||||
candidates.append(
|
candidates.append((
|
||||||
(
|
100,
|
||||||
100,
|
item,
|
||||||
item,
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
filepath = str(pathlib.Path(uris.to_fs_path(doc.uri)))
|
filepath = str(pathlib.Path(uris.to_fs_path(doc.uri)))
|
||||||
items_by_file = LSP_SERVER.completion_items_by_file_snapshot()
|
items_by_file = LSP_SERVER.completion_items_by_file_snapshot()
|
||||||
@@ -396,14 +371,15 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
if argument_completion is not None:
|
if argument_completion is not None:
|
||||||
static_candidates = [(0, item) for item in argument_completion.items]
|
static_candidates = [(0, item) for item in argument_completion.items]
|
||||||
if argument_completion.dynamic_kind == DynamicCompletionKind.VARIABLE:
|
if argument_completion.dynamic_kind == DynamicCompletionKind.VARIABLE:
|
||||||
variable_candidates = [*static_candidates, *candidates]
|
variable_candidates = list(candidates)
|
||||||
variable_candidates.extend(
|
variable_candidates.extend((300, item) for item in standard_items.nx_variables)
|
||||||
(300, item) for item in standard_items.nx_variables
|
|
||||||
)
|
|
||||||
items = ranked_completion_items(
|
items = ranked_completion_items(
|
||||||
variable_candidates,
|
variable_candidates,
|
||||||
CompletionContext.VARIABLE,
|
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)
|
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||||
|
|
||||||
if argument_completion.dynamic_kind == DynamicCompletionKind.PROCEDURE:
|
if argument_completion.dynamic_kind == DynamicCompletionKind.PROCEDURE:
|
||||||
@@ -412,14 +388,8 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
lsp.CompletionItemKind.Function,
|
lsp.CompletionItemKind.Function,
|
||||||
lsp.CompletionItemKind.Method,
|
lsp.CompletionItemKind.Method,
|
||||||
}
|
}
|
||||||
procedure_candidates = [
|
procedure_candidates = [(priority, item) for priority, item in candidates if item.kind in procedure_kinds]
|
||||||
(priority, item)
|
procedure_candidates.extend((300, item) for item in standard_items.nx_procs)
|
||||||
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(
|
items = ranked_completion_items(
|
||||||
[*static_candidates, *procedure_candidates],
|
[*static_candidates, *procedure_candidates],
|
||||||
CompletionContext.GENERAL,
|
CompletionContext.GENERAL,
|
||||||
@@ -430,26 +400,20 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
namespace_candidates = list(static_candidates)
|
namespace_candidates = list(static_candidates)
|
||||||
prefix_is_absolute = argument_completion.active_prefix.startswith("::")
|
prefix_is_absolute = argument_completion.active_prefix.startswith("::")
|
||||||
for index in LSP_SERVER.navigation_snapshot().values():
|
for index in LSP_SERVER.navigation_snapshot().values():
|
||||||
priority = (
|
priority = 100 if LSP_SERVER.paths_equal(index.path, filepath) else 200
|
||||||
100
|
|
||||||
if LSP_SERVER.paths_equal(index.path, filepath)
|
|
||||||
else 200
|
|
||||||
)
|
|
||||||
for occurrence in index.occurrences:
|
for occurrence in index.occurrences:
|
||||||
if occurrence.identity.kind != "namespace":
|
if occurrence.identity.kind != "namespace":
|
||||||
continue
|
continue
|
||||||
name = occurrence.identity.name
|
name = occurrence.identity.name
|
||||||
label = name if prefix_is_absolute else name.removeprefix("::")
|
label = name if prefix_is_absolute else name.removeprefix("::")
|
||||||
namespace_candidates.append(
|
namespace_candidates.append((
|
||||||
(
|
priority,
|
||||||
priority,
|
lsp.CompletionItem(
|
||||||
lsp.CompletionItem(
|
label=label,
|
||||||
label=label,
|
kind=lsp.CompletionItemKind.Module,
|
||||||
kind=lsp.CompletionItemKind.Module,
|
detail="Tcl namespace",
|
||||||
detail="Tcl namespace",
|
),
|
||||||
),
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
items = ranked_completion_items(
|
items = ranked_completion_items(
|
||||||
namespace_candidates,
|
namespace_candidates,
|
||||||
CompletionContext.GENERAL,
|
CompletionContext.GENERAL,
|
||||||
@@ -470,7 +434,7 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
)
|
)
|
||||||
def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None:
|
def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None:
|
||||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
method_help = method_signature_help(document.source, params.position)
|
method_help = method_signature_help(document.source, params.position, LSP_SERVER.class_snapshot(document.path))
|
||||||
if method_help is not None:
|
if method_help is not None:
|
||||||
return method_help
|
return method_help
|
||||||
try:
|
try:
|
||||||
@@ -480,9 +444,7 @@ def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None:
|
|||||||
if tree is None:
|
if tree is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
custom_signatures, custom_docs = LSP_SERVER.proc_metadata_snapshot(
|
custom_signatures, custom_docs = LSP_SERVER.proc_metadata_snapshot(document.path)
|
||||||
document.path
|
|
||||||
)
|
|
||||||
|
|
||||||
return build_signature_help(
|
return build_signature_help(
|
||||||
document.source,
|
document.source,
|
||||||
@@ -528,20 +490,17 @@ def inlay_hints(params: lsp.InlayHintParams):
|
|||||||
|
|
||||||
# Built-in NX procedures are the fallback. Workspace procedures replace them,
|
# Built-in NX procedures are the fallback. Workspace procedures replace them,
|
||||||
# and a declaration in the current file wins over duplicate workspace names.
|
# and a declaration in the current file wins over duplicate workspace names.
|
||||||
custom_signatures = LSP_SERVER.custom_inlay_signatures_snapshot(
|
custom_signatures = LSP_SERVER.custom_inlay_signatures_snapshot(document.path)
|
||||||
document.path
|
|
||||||
)
|
|
||||||
signatures = ChainMap(custom_signatures, BUILTIN_INLAY_SIGNATURES)
|
signatures = ChainMap(custom_signatures, BUILTIN_INLAY_SIGNATURES)
|
||||||
|
|
||||||
generator = InlayHintGenerator(
|
generator = InlayHintGenerator(
|
||||||
document.source,
|
document.source,
|
||||||
signatures,
|
signatures,
|
||||||
|
external_classes=LSP_SERVER.class_snapshot(document.path),
|
||||||
source_lines=LSP_SERVER.get_lines(document),
|
source_lines=LSP_SERVER.get_lines(document),
|
||||||
requested_range=params.range,
|
requested_range=params.range,
|
||||||
parameter_names=parameter_names,
|
parameter_names=parameter_names,
|
||||||
suppress_when_argument_matches_name=inlay_settings.get(
|
suppress_when_argument_matches_name=inlay_settings.get("suppressWhenArgumentMatchesName", True),
|
||||||
"suppressWhenArgumentMatchesName", True
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return generator.generate(tree)
|
return generator.generate(tree)
|
||||||
|
|
||||||
@@ -562,20 +521,20 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
|
|||||||
|
|
||||||
# Reuse cached AST
|
# Reuse cached AST
|
||||||
tree = LSP_SERVER.get_tree(document)
|
tree = LSP_SERVER.get_tree(document)
|
||||||
hl.highlight_classes(tree)
|
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)
|
tree.accept(hl, recurse=True)
|
||||||
|
|
||||||
tokens = hl.tokens()
|
tokens = hl.tokens()
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
data.extend(
|
data.extend([
|
||||||
[
|
token.line,
|
||||||
token.line,
|
token.offset,
|
||||||
token.offset,
|
token.length,
|
||||||
token.length,
|
TOKEN_TYPE_INDEX[token.tok_type],
|
||||||
TOKEN_TYPE_INDEX[token.tok_type],
|
reduce(operator.or_, token.tok_modifiers, 0),
|
||||||
reduce(operator.or_, token.tok_modifiers, 0),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
return lsp.SemanticTokens(data=data)
|
return lsp.SemanticTokens(data=data)
|
||||||
|
|
||||||
|
|
||||||
@@ -617,9 +576,7 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
|||||||
if match and match.get("kind") == "function":
|
if match and match.get("kind") == "function":
|
||||||
label = match.get("label", "")
|
label = match.get("label", "")
|
||||||
parameters = match.get("parameters", [])
|
parameters = match.get("parameters", [])
|
||||||
param_lines = (
|
param_lines = "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
||||||
"\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
|
||||||
)
|
|
||||||
example_data = match.get("example", [])
|
example_data = match.get("example", [])
|
||||||
example_md = "\n".join(f"{line}" for line in example_data)
|
example_md = "\n".join(f"{line}" for line in example_data)
|
||||||
returns_data = match.get("returns", ["None"])
|
returns_data = match.get("returns", ["None"])
|
||||||
@@ -648,28 +605,30 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
|||||||
# Build a merged map of proc -> docs gathered during initialization and updates
|
# Build a merged map of proc -> docs gathered during initialization and updates
|
||||||
proc_doc = LSP_SERVER.proc_documentation(token, document.path)
|
proc_doc = LSP_SERVER.proc_documentation(token, document.path)
|
||||||
if proc_doc is not None:
|
if proc_doc is not None:
|
||||||
return lsp.Hover(
|
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=proc_doc))
|
||||||
lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=proc_doc)
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DEFINITION)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DEFINITION)
|
||||||
def goto_definition(params: lsp.DefinitionParams):
|
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)
|
context = _navigation_context(params.text_document.uri, params.position)
|
||||||
if context is None:
|
if context is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
indexes, definitions, _, identity = context
|
indexes, definitions, _, identity = context
|
||||||
locations = [
|
locations = [lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in matching_occurrences(identity, indexes, definitions) if occurrence.is_definition]
|
||||||
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
|
return _sorted_locations(locations) or None
|
||||||
|
|
||||||
|
|
||||||
@@ -713,9 +672,7 @@ def references(params: lsp.ReferenceParams) -> list[lsp.Location]:
|
|||||||
indexes, definitions, _, identity = context
|
indexes, definitions, _, identity = context
|
||||||
locations = [
|
locations = [
|
||||||
lsp.Location(uri=index.uri, range=occurrence.range)
|
lsp.Location(uri=index.uri, range=occurrence.range)
|
||||||
for index, occurrence in matching_occurrences(
|
for index, occurrence in matching_occurrences(identity, indexes, definitions)
|
||||||
identity, indexes, definitions
|
|
||||||
)
|
|
||||||
if params.context.include_declaration or not occurrence.is_definition
|
if params.context.include_declaration or not occurrence.is_definition
|
||||||
]
|
]
|
||||||
return _sorted_locations(locations)
|
return _sorted_locations(locations)
|
||||||
@@ -747,11 +704,7 @@ def _is_renamable(
|
|||||||
if identity.kind == "proc":
|
if identity.kind == "proc":
|
||||||
if basename in BUILTIN_PROC_NAMES:
|
if basename in BUILTIN_PROC_NAMES:
|
||||||
return False
|
return False
|
||||||
definition_count = sum(
|
definition_count = sum(occurrence.is_definition and occurrence.identity == identity for index in indexes.values() for occurrence in index.occurrences)
|
||||||
occurrence.is_definition and occurrence.identity == identity
|
|
||||||
for index in indexes.values()
|
|
||||||
for occurrence in index.occurrences
|
|
||||||
)
|
|
||||||
return definition_count == 1
|
return definition_count == 1
|
||||||
|
|
||||||
return basename not in BUILTIN_VARIABLE_NAMES
|
return basename not in BUILTIN_VARIABLE_NAMES
|
||||||
@@ -766,9 +719,7 @@ def prepare_rename(params: lsp.PrepareRenameParams):
|
|||||||
indexes, definitions, occurrence, identity = context
|
indexes, definitions, occurrence, identity = context
|
||||||
if not _is_renamable(identity, indexes, definitions):
|
if not _is_renamable(identity, indexes, definitions):
|
||||||
return None
|
return None
|
||||||
return lsp.PrepareRenamePlaceholder(
|
return lsp.PrepareRenamePlaceholder(range=occurrence.range, placeholder=occurrence.placeholder)
|
||||||
range=occurrence.range, placeholder=occurrence.placeholder
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(
|
@LSP_SERVER.feature(
|
||||||
@@ -800,9 +751,7 @@ def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None:
|
|||||||
if key in seen:
|
if key in seen:
|
||||||
continue
|
continue
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
changes.setdefault(index.uri, []).append(
|
changes.setdefault(index.uri, []).append(lsp.TextEdit(range=occurrence.range, new_text=params.new_name))
|
||||||
lsp.TextEdit(range=occurrence.range, new_text=params.new_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
for edits in changes.values():
|
for edits in changes.values():
|
||||||
edits.sort(
|
edits.sort(
|
||||||
@@ -913,12 +862,8 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
|||||||
|
|
||||||
settings = params.initialization_options["settings"]
|
settings = params.initialization_options["settings"]
|
||||||
_update_workspace_settings(settings)
|
_update_workspace_settings(settings)
|
||||||
log_to_output(
|
log_to_output(f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n")
|
||||||
f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n"
|
log_to_output(f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n")
|
||||||
)
|
|
||||||
log_to_output(
|
|
||||||
f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n"
|
|
||||||
)
|
|
||||||
semantic_tokens_legend = lsp.SemanticTokensLegend(
|
semantic_tokens_legend = lsp.SemanticTokensLegend(
|
||||||
token_types=TOKEN_TYPES,
|
token_types=TOKEN_TYPES,
|
||||||
token_modifiers=[m.name for m in TokenModifier],
|
token_modifiers=[m.name for m in TokenModifier],
|
||||||
@@ -927,9 +872,7 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
|||||||
capabilities=lsp.ServerCapabilities(
|
capabilities=lsp.ServerCapabilities(
|
||||||
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
|
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
|
||||||
folding_range_provider=True,
|
folding_range_provider=True,
|
||||||
semantic_tokens_provider=lsp.SemanticTokensOptions(
|
semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
|
||||||
legend=semantic_tokens_legend, full=True, range=False
|
|
||||||
),
|
|
||||||
definition_provider=True,
|
definition_provider=True,
|
||||||
document_highlight_provider=True,
|
document_highlight_provider=True,
|
||||||
references_provider=True,
|
references_provider=True,
|
||||||
@@ -963,19 +906,10 @@ def initialized(_params: lsp.InitializedParams):
|
|||||||
"node_modules",
|
"node_modules",
|
||||||
"out",
|
"out",
|
||||||
}
|
}
|
||||||
tcl_files = (
|
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]))
|
||||||
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()):
|
for filepath in sorted(tcl_files, key=lambda path: str(path).casefold()):
|
||||||
try:
|
try:
|
||||||
document = TextDocument(
|
document = TextDocument(uri=filepath.as_uri(), language_id="tcl")
|
||||||
uri=filepath.as_uri(), language_id="tcl"
|
|
||||||
)
|
|
||||||
LSP_SERVER.update_poco_completion_for_file(
|
LSP_SERVER.update_poco_completion_for_file(
|
||||||
document,
|
document,
|
||||||
cache_tree=False,
|
cache_tree=False,
|
||||||
@@ -983,6 +917,7 @@ def initialized(_params: lsp.InitializedParams):
|
|||||||
)
|
)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
log_to_output(f"Fehler beim Parsen von {filepath}: {error}")
|
log_to_output(f"Fehler beim Parsen von {filepath}: {error}")
|
||||||
|
_refresh_psc_index()
|
||||||
log_to_output("Background indexing completed.")
|
log_to_output("Background indexing completed.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_to_output(f"Background indexing failed: {e}")
|
log_to_output(f"Background indexing failed: {e}")
|
||||||
@@ -1089,36 +1024,26 @@ def _get_settings_by_document(document: TextDocument | None):
|
|||||||
# *****************************************************
|
# *****************************************************
|
||||||
# Logging and notification.
|
# Logging and notification.
|
||||||
# *****************************************************
|
# *****************************************************
|
||||||
def log_to_output(
|
def log_to_output(message: str, msg_type: lsp.MessageType = lsp.MessageType.Log) -> None:
|
||||||
message: str, msg_type: lsp.MessageType = lsp.MessageType.Log
|
LSP_SERVER.window_log_message(lsp.LogMessageParams(message=message, type=msg_type))
|
||||||
) -> None:
|
|
||||||
LSP_SERVER.window_log_message(
|
|
||||||
lsp.LogMessageParams(message=message, type=msg_type)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def log_error(message: str) -> None:
|
def log_error(message: str) -> None:
|
||||||
log_to_output(message, lsp.MessageType.Error)
|
log_to_output(message, lsp.MessageType.Error)
|
||||||
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onError", "onWarning", "always"]:
|
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onError", "onWarning", "always"]:
|
||||||
LSP_SERVER.window_show_message(
|
LSP_SERVER.window_show_message(lsp.ShowMessageParams(message=message, type=lsp.MessageType.Error))
|
||||||
lsp.ShowMessageParams(message=message, type=lsp.MessageType.Error)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def log_warning(message: str) -> None:
|
def log_warning(message: str) -> None:
|
||||||
log_to_output(message, lsp.MessageType.Warning)
|
log_to_output(message, lsp.MessageType.Warning)
|
||||||
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onWarning", "always"]:
|
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onWarning", "always"]:
|
||||||
LSP_SERVER.window_show_message(
|
LSP_SERVER.window_show_message(lsp.ShowMessageParams(message=message, type=lsp.MessageType.Warning))
|
||||||
lsp.ShowMessageParams(message=message, type=lsp.MessageType.Warning)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def log_always(message: str) -> None:
|
def log_always(message: str) -> None:
|
||||||
log_to_output(message, lsp.MessageType.Info)
|
log_to_output(message, lsp.MessageType.Info)
|
||||||
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["always"]:
|
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["always"]:
|
||||||
LSP_SERVER.window_show_message(
|
LSP_SERVER.window_show_message(lsp.ShowMessageParams(message=message, type=lsp.MessageType.Info))
|
||||||
lsp.ShowMessageParams(message=message, type=lsp.MessageType.Info)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# *****************************************************
|
# *****************************************************
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import threading
|
import threading
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
@@ -15,6 +16,8 @@ from tclint.violations import Violation
|
|||||||
from tools import checks, parser
|
from tools import checks, parser
|
||||||
from tools.completion_items import CompletionCollector
|
from tools.completion_items import CompletionCollector
|
||||||
from tools.tcloo_symbols import class_completion_items
|
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.formatter import NxFormatter as Formatter
|
||||||
from tools.inlay_hint import InlayHintSignature, build_custom_inlay_signatures
|
from tools.inlay_hint import InlayHintSignature, build_custom_inlay_signatures
|
||||||
from tools.navigation import FileSymbolIndex, build_file_symbol_index
|
from tools.navigation import FileSymbolIndex, build_file_symbol_index
|
||||||
@@ -35,6 +38,10 @@ class TclLanguageServer(LanguageServer):
|
|||||||
self.poco_completion: dict = {}
|
self.poco_completion: dict = {}
|
||||||
self.proc_signatures: dict = {}
|
self.proc_signatures: dict = {}
|
||||||
self.proc_docs: 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.navigation_indexes: dict[str, FileSymbolIndex] = {}
|
||||||
self.variable_indexes: dict[
|
self.variable_indexes: dict[
|
||||||
str,
|
str,
|
||||||
@@ -171,6 +178,74 @@ class TclLanguageServer(LanguageServer):
|
|||||||
self._workspace_completion_cache = (self._index_generation, items)
|
self._workspace_completion_cache = (self._index_generation, items)
|
||||||
return 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(
|
def completion_items_by_file_snapshot(
|
||||||
self,
|
self,
|
||||||
) -> dict[str, tuple[lsp.CompletionItem, ...]]:
|
) -> dict[str, tuple[lsp.CompletionItem, ...]]:
|
||||||
@@ -455,6 +530,7 @@ class TclLanguageServer(LanguageServer):
|
|||||||
self.poco_completion.pop(filepath, None)
|
self.poco_completion.pop(filepath, None)
|
||||||
self.proc_signatures.pop(filepath, None)
|
self.proc_signatures.pop(filepath, None)
|
||||||
self.proc_docs.pop(filepath, None)
|
self.proc_docs.pop(filepath, None)
|
||||||
|
self.class_indexes.pop(filepath, None)
|
||||||
self.navigation_indexes.pop(filepath, None)
|
self.navigation_indexes.pop(filepath, None)
|
||||||
self.variable_indexes.pop(filepath, None)
|
self.variable_indexes.pop(filepath, None)
|
||||||
self._committed_index_versions.pop(filepath, None)
|
self._committed_index_versions.pop(filepath, None)
|
||||||
@@ -486,6 +562,7 @@ class TclLanguageServer(LanguageServer):
|
|||||||
self.poco_completion,
|
self.poco_completion,
|
||||||
self.proc_signatures,
|
self.proc_signatures,
|
||||||
self.proc_docs,
|
self.proc_docs,
|
||||||
|
self.class_indexes,
|
||||||
self.navigation_indexes,
|
self.navigation_indexes,
|
||||||
self.variable_indexes,
|
self.variable_indexes,
|
||||||
self._index_tokens,
|
self._index_tokens,
|
||||||
@@ -545,6 +622,7 @@ class TclLanguageServer(LanguageServer):
|
|||||||
)
|
)
|
||||||
tree.accept(collector, recurse=True)
|
tree.accept(collector, recurse=True)
|
||||||
collector.custom_functions.extend(class_completion_items(tree))
|
collector.custom_functions.extend(class_completion_items(tree))
|
||||||
|
classes = indexed_classes(tree, document.uri, document.source)
|
||||||
docs = build_proc_docs(tree, document.source)
|
docs = build_proc_docs(tree, document.source)
|
||||||
navigation_index = build_file_symbol_index(
|
navigation_index = build_file_symbol_index(
|
||||||
filepath, document.uri, tree
|
filepath, document.uri, tree
|
||||||
@@ -565,6 +643,7 @@ class TclLanguageServer(LanguageServer):
|
|||||||
self.poco_completion[filepath] = list(collector.custom_functions)
|
self.poco_completion[filepath] = list(collector.custom_functions)
|
||||||
self.proc_signatures[filepath] = dict(collector.proc_signatures)
|
self.proc_signatures[filepath] = dict(collector.proc_signatures)
|
||||||
self.proc_docs[filepath] = docs
|
self.proc_docs[filepath] = docs
|
||||||
|
self.class_indexes[filepath] = classes
|
||||||
self.navigation_indexes[filepath] = navigation_index
|
self.navigation_indexes[filepath] = navigation_index
|
||||||
self.variable_indexes[filepath] = (document.version, variable_index)
|
self.variable_indexes[filepath] = (document.version, variable_index)
|
||||||
self._committed_index_versions[filepath] = document.version
|
self._committed_index_versions[filepath] = document.version
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -14,6 +15,9 @@ class SourcedFile:
|
|||||||
def read_psc_file(psc_file: Path) -> List[SourcedFile]:
|
def read_psc_file(psc_file: Path) -> List[SourcedFile]:
|
||||||
tree = ET.parse(psc_file)
|
tree = ET.parse(psc_file)
|
||||||
root = tree.getroot()
|
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")
|
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]:
|
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__":
|
def psc_script_files(psc_file: Path) -> list[Path]:
|
||||||
test = get_all_psc_files(
|
"""Resolve layer script paths relative to the PSC, preserving load order."""
|
||||||
Path(
|
def expanded(value):
|
||||||
r"H:\janus-engineering-customers\KSB_Frankenthal\custom\library\machine\installed_machines\ksb_pe_grob_g550_sone\postprocessor"
|
return Path(os.path.expandvars(value).replace("\\", "/"))
|
||||||
)
|
|
||||||
)
|
paths = []
|
||||||
print(test)
|
for layer in read_psc_file(psc_file):
|
||||||
for pp in test:
|
folder = layer.subfolder or "."
|
||||||
read_psc_file(pp)
|
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
|
||||||
|
|||||||
@@ -158,11 +158,13 @@ class InlayHintGenerator(Visitor):
|
|||||||
requested_range: lsp.Range | None = None,
|
requested_range: lsp.Range | None = None,
|
||||||
parameter_names: str = "all",
|
parameter_names: str = "all",
|
||||||
suppress_when_argument_matches_name: bool = True,
|
suppress_when_argument_matches_name: bool = True,
|
||||||
|
external_classes=None,
|
||||||
):
|
):
|
||||||
self.source_lines = (
|
self.source_lines = (
|
||||||
source_lines if source_lines is not None else source.splitlines()
|
source_lines if source_lines is not None else source.splitlines()
|
||||||
)
|
)
|
||||||
self.source = source
|
self.source = source
|
||||||
|
self.external_classes = external_classes
|
||||||
self.proc_signatures = proc_signatures
|
self.proc_signatures = proc_signatures
|
||||||
self.requested_range = requested_range
|
self.requested_range = requested_range
|
||||||
self.parameter_names = parameter_names
|
self.parameter_names = parameter_names
|
||||||
@@ -198,7 +200,7 @@ class InlayHintGenerator(Visitor):
|
|||||||
|
|
||||||
walk(tree)
|
walk(tree)
|
||||||
if self.parameter_names != "none":
|
if self.parameter_names != "none":
|
||||||
for call in resolved_method_calls(self.source):
|
for call in resolved_method_calls(self.source, self.external_classes):
|
||||||
if not self._node_intersects_requested_range(call.command):
|
if not self._node_intersects_requested_range(call.command):
|
||||||
continue
|
continue
|
||||||
parameters = method_parameters(call.parameters)
|
parameters = method_parameters(call.parameters)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from tclint.commands.plugins import PluginManager
|
|||||||
from tclint.syntax_tree import BareWord, Command, QuotedWord, Visitor
|
from tclint.syntax_tree import BareWord, Command, QuotedWord, Visitor
|
||||||
from tools.variable_names import variable_name
|
from tools.variable_names import variable_name
|
||||||
from tools.tcloo_symbols import class_symbols
|
from tools.tcloo_symbols import class_symbols
|
||||||
|
from tools.tcloo_completion import _analyze
|
||||||
|
|
||||||
# Constructing a PluginManager scans entry points, and get_commands() rebuilds
|
# Constructing a PluginManager scans entry points, and get_commands() rebuilds
|
||||||
# the builtin command set on every call. Semantic tokens are requested often, so
|
# the builtin command set on every call. Semantic tokens are requested often, so
|
||||||
@@ -72,12 +73,9 @@ class _Highlighter(Visitor):
|
|||||||
self._commands = _load_commands(plugins)
|
self._commands = _load_commands(plugins)
|
||||||
self._tokens = []
|
self._tokens = []
|
||||||
self._class_tokens = {}
|
self._class_tokens = {}
|
||||||
|
self._method_tokens = {}
|
||||||
if isinstance(custom_functions, dict):
|
if isinstance(custom_functions, dict):
|
||||||
self._custom_function_names = frozenset(
|
self._custom_function_names = frozenset(item.label for items in custom_functions.values() for item in items)
|
||||||
item.label
|
|
||||||
for items in custom_functions.values()
|
|
||||||
for item in items
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self._custom_function_names = frozenset(custom_functions)
|
self._custom_function_names = frozenset(custom_functions)
|
||||||
|
|
||||||
@@ -86,15 +84,18 @@ class _Highlighter(Visitor):
|
|||||||
return
|
return
|
||||||
self._tokens.append((position, length, tok_type, modifiers or []))
|
self._tokens.append((position, length, tok_type, modifiers or []))
|
||||||
|
|
||||||
def highlight_classes(self, tree):
|
def highlight_classes(self, tree, external_classes=None):
|
||||||
declarations, references = class_symbols(tree)
|
declarations, references = class_symbols(tree, external_classes)
|
||||||
for node, modifiers in [
|
for node, modifiers in [
|
||||||
*((node, [TokenModifier.declaration]) for node in declarations.values()),
|
*((node, [TokenModifier.declaration]) for node in declarations.values()),
|
||||||
*((node, []) for node in references),
|
*((node, []) for node in references),
|
||||||
]:
|
]:
|
||||||
line, col = node.contents_pos
|
line, col = node.contents_pos
|
||||||
self._class_tokens[(line - 1, col - 1)] = (
|
self._class_tokens[(line - 1, col - 1)] = (
|
||||||
(line - 1, col - 1), len(node.contents), "class", modifiers,
|
(line - 1, col - 1),
|
||||||
|
len(node.contents),
|
||||||
|
"class",
|
||||||
|
modifiers,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_token_info(self, node):
|
def _get_token_info(self, node):
|
||||||
@@ -124,6 +125,28 @@ class _Highlighter(Visitor):
|
|||||||
|
|
||||||
return None
|
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):
|
def visit_quoted_word(self, word: QuotedWord):
|
||||||
if not word.contents:
|
if not word.contents:
|
||||||
return
|
return
|
||||||
@@ -170,7 +193,7 @@ class _Highlighter(Visitor):
|
|||||||
if routine.contents == "puts":
|
if routine.contents == "puts":
|
||||||
line, col = routine.contents_pos
|
line, col = routine.contents_pos
|
||||||
self._append_token((line - 1, col - 1), len(routine.contents), "function", [TokenModifier.builtin])
|
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]
|
first_arg = command.args[0]
|
||||||
token_info = self._get_token_info(first_arg)
|
token_info = self._get_token_info(first_arg)
|
||||||
if first_arg.contents is None:
|
if first_arg.contents is None:
|
||||||
@@ -217,8 +240,9 @@ class _Highlighter(Visitor):
|
|||||||
tokens = []
|
tokens = []
|
||||||
last_line = 0
|
last_line = 0
|
||||||
last_col = 0
|
last_col = 0
|
||||||
raw_tokens = [token for token in self._tokens if token[0] not in self._class_tokens]
|
overrides = {**self._method_tokens, **self._class_tokens}
|
||||||
raw_tokens.extend(self._class_tokens.values())
|
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]):
|
for (line, col), length, tok_type, tok_modifier in sorted(raw_tokens, key=lambda x: x[0]):
|
||||||
line_delta = line - last_line
|
line_delta = line - last_line
|
||||||
col_delta = col
|
col_delta = col
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ def _options(*labels: str) -> tuple[OptionSpec, ...]:
|
|||||||
|
|
||||||
|
|
||||||
OPTIONS_BY_PATH: dict[tuple[str, ...], tuple[OptionSpec, ...]] = {
|
OPTIONS_BY_PATH: dict[tuple[str, ...], tuple[OptionSpec, ...]] = {
|
||||||
|
("unset",): _options("nocomplain"),
|
||||||
("binary", "decode", "base64"): (OptionSpec("-strict"),),
|
("binary", "decode", "base64"): (OptionSpec("-strict"),),
|
||||||
("binary", "encode", "base64"): (
|
("binary", "encode", "base64"): (
|
||||||
OptionSpec("-maxlen", takes_value=True),
|
OptionSpec("-maxlen", takes_value=True),
|
||||||
@@ -437,109 +438,51 @@ _REPEATED_SUBCOMMAND_ARGUMENTS = frozenset(range(2, 33))
|
|||||||
DYNAMIC_COMPLETION_RULES = (
|
DYNAMIC_COMPLETION_RULES = (
|
||||||
# Variable-taking commands.
|
# Variable-taking commands.
|
||||||
DynamicCompletionRule(("append",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
DynamicCompletionRule(("append",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("array", "exists"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
("array", "exists"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
DynamicCompletionRule(("array", "get"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
),
|
DynamicCompletionRule(("array", "names"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("array", "set"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
("array", "get"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
DynamicCompletionRule(("array", "size"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
),
|
DynamicCompletionRule(("array", "statistics"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("array", "unset"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
("array", "names"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
DynamicCompletionRule(("catch",), frozenset({2, 3}), DynamicCompletionKind.VARIABLE),
|
||||||
),
|
DynamicCompletionRule(("dict", "append"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("dict", "incr"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
("array", "set"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
DynamicCompletionRule(("dict", "lappend"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
),
|
DynamicCompletionRule(("dict", "set"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("dict", "unset"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
("array", "size"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
DynamicCompletionRule(("dict", "update"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
),
|
DynamicCompletionRule(("dict", "with"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("global",), _REPEATED_ARGUMENTS, DynamicCompletionKind.VARIABLE),
|
||||||
("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(("incr",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("info", "exists"), frozenset({2}), DynamicCompletionKind.VARIABLE),
|
||||||
("info", "exists"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
|
||||||
),
|
|
||||||
DynamicCompletionRule(("lappend",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
DynamicCompletionRule(("lappend",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
DynamicCompletionRule(("set",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
DynamicCompletionRule(("set",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("unset",), _REPEATED_ARGUMENTS, DynamicCompletionKind.VARIABLE),
|
||||||
("unset",), _REPEATED_ARGUMENTS, DynamicCompletionKind.VARIABLE
|
DynamicCompletionRule(("variable",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
),
|
|
||||||
DynamicCompletionRule(
|
|
||||||
("variable",), frozenset({1}), DynamicCompletionKind.VARIABLE
|
|
||||||
),
|
|
||||||
DynamicCompletionRule(("vwait",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
DynamicCompletionRule(("vwait",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
# Procedure-taking commands.
|
# Procedure-taking commands.
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("info", "args"), frozenset({2}), DynamicCompletionKind.PROCEDURE),
|
||||||
("info", "args"), frozenset({2}), DynamicCompletionKind.PROCEDURE
|
DynamicCompletionRule(("info", "body"), frozenset({2}), DynamicCompletionKind.PROCEDURE),
|
||||||
),
|
DynamicCompletionRule(("info", "default"), frozenset({2}), DynamicCompletionKind.PROCEDURE),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("namespace", "origin"), frozenset({2}), DynamicCompletionKind.PROCEDURE),
|
||||||
("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),
|
DynamicCompletionRule(("rename",), frozenset({1}), DynamicCompletionKind.PROCEDURE),
|
||||||
# Namespace-taking commands.
|
# Namespace-taking commands.
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("namespace", "children"), frozenset({2}), DynamicCompletionKind.NAMESPACE),
|
||||||
("namespace", "children"), frozenset({2}), DynamicCompletionKind.NAMESPACE
|
|
||||||
),
|
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(
|
||||||
("namespace", "delete"),
|
("namespace", "delete"),
|
||||||
_REPEATED_SUBCOMMAND_ARGUMENTS,
|
_REPEATED_SUBCOMMAND_ARGUMENTS,
|
||||||
DynamicCompletionKind.NAMESPACE,
|
DynamicCompletionKind.NAMESPACE,
|
||||||
),
|
),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("namespace", "eval"), frozenset({2}), DynamicCompletionKind.NAMESPACE),
|
||||||
("namespace", "eval"), frozenset({2}), DynamicCompletionKind.NAMESPACE
|
DynamicCompletionRule(("namespace", "exists"), frozenset({2}), DynamicCompletionKind.NAMESPACE),
|
||||||
),
|
DynamicCompletionRule(("namespace", "parent"), 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
|
# Path-taking commands. Source files are narrowed to Tcl while directories
|
||||||
# remain visible so users can continue navigating.
|
# remain visible so users can continue navigating.
|
||||||
DynamicCompletionRule(("cd",), frozenset({1}), DynamicCompletionKind.PATH),
|
DynamicCompletionRule(("cd",), frozenset({1}), DynamicCompletionKind.PATH),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("load",), frozenset({1}), DynamicCompletionKind.PATH, (".dll", ".so", ".dylib")),
|
||||||
("load",), frozenset({1}), DynamicCompletionKind.PATH, (".dll", ".so", ".dylib")
|
|
||||||
),
|
|
||||||
DynamicCompletionRule(("open",), frozenset({1}), DynamicCompletionKind.PATH),
|
DynamicCompletionRule(("open",), frozenset({1}), DynamicCompletionKind.PATH),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(("source",), frozenset({1, 3}), DynamicCompletionKind.PATH, (".tcl",)),
|
||||||
("source",), frozenset({1, 3}), DynamicCompletionKind.PATH, (".tcl",)
|
|
||||||
),
|
|
||||||
*(
|
*(
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(
|
||||||
("file", subcommand),
|
("file", subcommand),
|
||||||
@@ -666,12 +609,7 @@ SUBCOMMAND_SNIPPET_ITEMS = {
|
|||||||
|
|
||||||
|
|
||||||
TCL_COMMAND_NAMES = tuple(
|
TCL_COMMAND_NAMES = tuple(
|
||||||
sorted(
|
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})
|
||||||
{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_ITEMS = tuple(
|
||||||
@@ -688,9 +626,7 @@ TCL_COMMAND_ITEMS = tuple(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def line_prefix_at_position(
|
def line_prefix_at_position(source_lines: Sequence[str], position: Position) -> str | None:
|
||||||
source_lines: Sequence[str], position: Position
|
|
||||||
) -> str | None:
|
|
||||||
"""Return the current line before an LSP UTF-16 position."""
|
"""Return the current line before an LSP UTF-16 position."""
|
||||||
|
|
||||||
if position.line < 0 or position.line >= len(source_lines):
|
if position.line < 0 or position.line >= len(source_lines):
|
||||||
@@ -703,9 +639,7 @@ def line_prefix_at_position(
|
|||||||
return line[:codepoint_offset]
|
return line[:codepoint_offset]
|
||||||
|
|
||||||
|
|
||||||
def tcl_argument_completion(
|
def tcl_argument_completion(source_lines: Sequence[str], position: Position) -> TclArgumentCompletion | None:
|
||||||
source_lines: Sequence[str], position: Position
|
|
||||||
) -> TclArgumentCompletion | None:
|
|
||||||
"""Describe static and dynamic argument completion at ``position``.
|
"""Describe static and dynamic argument completion at ``position``.
|
||||||
|
|
||||||
``None`` means that the cursor is not at a command-specific completion
|
``None`` means that the cursor is not at a command-specific completion
|
||||||
@@ -771,7 +705,7 @@ def tcl_argument_completion(
|
|||||||
active_prefix,
|
active_prefix,
|
||||||
)
|
)
|
||||||
if option_completion is not None:
|
if option_completion is not None:
|
||||||
if active_prefix.startswith("-"):
|
if active_prefix.startswith("-") or (path == ("unset",) and active_index == 1 and not active_prefix):
|
||||||
return option_completion
|
return option_completion
|
||||||
return _merge_dynamic_completion(
|
return _merge_dynamic_completion(
|
||||||
(*argument_items, *option_completion.items),
|
(*argument_items, *option_completion.items),
|
||||||
@@ -780,9 +714,7 @@ def tcl_argument_completion(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if argument_items:
|
if argument_items:
|
||||||
return _merge_dynamic_completion(
|
return _merge_dynamic_completion(argument_items, dynamic_completion, active_prefix)
|
||||||
argument_items, dynamic_completion, active_prefix
|
|
||||||
)
|
|
||||||
return dynamic_completion
|
return dynamic_completion
|
||||||
|
|
||||||
|
|
||||||
@@ -827,9 +759,7 @@ def _option_completion(
|
|||||||
if active_prefix and not active_prefix.startswith("-"):
|
if active_prefix and not active_prefix.startswith("-"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
remaining_options = tuple(
|
remaining_options = tuple(option.label for option in options if option.label not in used_options)
|
||||||
option.label for option in options if option.label not in used_options
|
|
||||||
)
|
|
||||||
return TclArgumentCompletion(
|
return TclArgumentCompletion(
|
||||||
items=_completion_items(
|
items=_completion_items(
|
||||||
remaining_options,
|
remaining_options,
|
||||||
@@ -840,16 +770,9 @@ def _option_completion(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _dynamic_completion(
|
def _dynamic_completion(words: Sequence[str], active_index: int, active_prefix: str) -> TclArgumentCompletion | None:
|
||||||
words: Sequence[str], active_index: int, active_prefix: str
|
for rule in sorted(DYNAMIC_COMPLETION_RULES, key=lambda item: len(item.path), reverse=True):
|
||||||
) -> TclArgumentCompletion | None:
|
if active_index in rule.argument_indices and tuple(words[: len(rule.path)]) == rule.path:
|
||||||
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(
|
return TclArgumentCompletion(
|
||||||
dynamic_kind=rule.kind,
|
dynamic_kind=rule.kind,
|
||||||
active_prefix=active_prefix,
|
active_prefix=active_prefix,
|
||||||
@@ -873,9 +796,7 @@ def _merge_dynamic_completion(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _completion_items(
|
def _completion_items(labels: Sequence[str], kind: CompletionItemKind, detail: str) -> tuple[CompletionItem, ...]:
|
||||||
labels: Sequence[str], kind: CompletionItemKind, detail: str
|
|
||||||
) -> tuple[CompletionItem, ...]:
|
|
||||||
return tuple(
|
return tuple(
|
||||||
CompletionItem(
|
CompletionItem(
|
||||||
label=label,
|
label=label,
|
||||||
@@ -913,9 +834,7 @@ def path_completion_items(
|
|||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
allowed_extensions = {
|
allowed_extensions = {extension.casefold() for extension in completion.path_extensions}
|
||||||
extension.casefold() for extension in completion.path_extensions
|
|
||||||
}
|
|
||||||
replace_start = max(
|
replace_start = max(
|
||||||
0,
|
0,
|
||||||
position.character - len(raw_prefix.encode("utf-16-le")) // 2,
|
position.character - len(raw_prefix.encode("utf-16-le")) // 2,
|
||||||
@@ -933,28 +852,17 @@ def path_completion_items(
|
|||||||
is_directory = entry.is_dir()
|
is_directory = entry.is_dir()
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
if (
|
if not is_directory and allowed_extensions and entry.suffix.casefold() not in allowed_extensions:
|
||||||
not is_directory
|
|
||||||
and allowed_extensions
|
|
||||||
and entry.suffix.casefold() not in allowed_extensions
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
escaped_name = "".join(
|
escaped_name = "".join(f"\\{character}" if character.isspace() else character for character in entry.name)
|
||||||
f"\\{character}" if character.isspace() else character
|
|
||||||
for character in entry.name
|
|
||||||
)
|
|
||||||
new_text = f"{normalized_directory}{escaped_name}"
|
new_text = f"{normalized_directory}{escaped_name}"
|
||||||
if is_directory:
|
if is_directory:
|
||||||
new_text += "/"
|
new_text += "/"
|
||||||
items.append(
|
items.append(
|
||||||
CompletionItem(
|
CompletionItem(
|
||||||
label=new_text,
|
label=new_text,
|
||||||
kind=(
|
kind=(CompletionItemKind.Folder if is_directory else CompletionItemKind.File),
|
||||||
CompletionItemKind.Folder
|
|
||||||
if is_directory
|
|
||||||
else CompletionItemKind.File
|
|
||||||
),
|
|
||||||
detail="Directory" if is_directory else "File",
|
detail="Directory" if is_directory else "File",
|
||||||
text_edit=TextEdit(range=replace_range, new_text=new_text),
|
text_edit=TextEdit(range=replace_range, new_text=new_text),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ def method_parameters(parameters: str) -> list[MethodParameter]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def method_signature_help(source: str, position: lsp.Position) -> lsp.SignatureHelp | None:
|
def method_signature_help(source: str, position: lsp.Position, external_classes=None) -> lsp.SignatureHelp | None:
|
||||||
lines = source.split("\n")
|
lines = source.split("\n")
|
||||||
if position.line >= len(lines):
|
if position.line >= len(lines):
|
||||||
return None
|
return None
|
||||||
# AST columns are codepoints; LSP columns are UTF-16 code units.
|
# 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")
|
prefix = lines[position.line].encode("utf-16-le")[:position.character * 2].decode("utf-16-le", errors="ignore")
|
||||||
cursor = (position.line, len(prefix))
|
cursor = (position.line, len(prefix))
|
||||||
candidates = [call for call in resolved_method_calls(source)
|
candidates = [call for call in resolved_method_calls(source, external_classes)
|
||||||
if _contains_cursor(call.command, lines, cursor)]
|
if _contains_cursor(call.command, lines, cursor)]
|
||||||
if not candidates:
|
if not candidates:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Conservative, document-local TclOO type inference without executing Tcl."""
|
"""Static TclOO inference using local and indexed classes, without executing Tcl."""
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
@@ -17,6 +17,9 @@ class ClassInfo:
|
|||||||
methods: dict[str, tuple[str, Script | None]] = field(default_factory=dict)
|
methods: dict[str, tuple[str, Script | None]] = field(default_factory=dict)
|
||||||
namespace: str = ""
|
namespace: str = ""
|
||||||
constructor: 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
|
@dataclass
|
||||||
@@ -25,6 +28,21 @@ class MethodCall:
|
|||||||
label: str
|
label: str
|
||||||
parameters: str
|
parameters: str
|
||||||
argument_offset: int = 1
|
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):
|
def parse_completion_source(source, pos=None):
|
||||||
@@ -57,7 +75,7 @@ def _qualified(name, namespace):
|
|||||||
|
|
||||||
|
|
||||||
def tcloo_completions(
|
def tcloo_completions(
|
||||||
source_lines: Sequence[str], position: lsp.Position,
|
source_lines: Sequence[str], position: lsp.Position, external_classes=None,
|
||||||
) -> list[lsp.CompletionItem] | None:
|
) -> list[lsp.CompletionItem] | None:
|
||||||
"""Return receiver-specific methods, or None outside a known OO context."""
|
"""Return receiver-specific methods, or None outside a known OO context."""
|
||||||
prefix = line_prefix_at_position(source_lines, position)
|
prefix = line_prefix_at_position(source_lines, position)
|
||||||
@@ -77,7 +95,7 @@ def tcloo_completions(
|
|||||||
tree = parse_completion_source("\n".join(lines))
|
tree = parse_completion_source("\n".join(lines))
|
||||||
if tree is None:
|
if tree is None:
|
||||||
return None
|
return None
|
||||||
classes, result, _ = _analyze(tree, typed, marker)
|
classes, result, _ = _analyze(tree, typed, marker, external_classes)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
cls, internal = result
|
cls, internal = result
|
||||||
@@ -99,11 +117,8 @@ def tcloo_completions(
|
|||||||
if name.startswith(typed) and (internal or not name.startswith("_") and not name[:1].isupper())]
|
if name.startswith(typed) and (internal or not name.startswith("_") and not name[:1].isupper())]
|
||||||
|
|
||||||
|
|
||||||
def _analyze(tree, typed="", marker=""):
|
def _collect_classes(tree, classes, uri=None, source=""):
|
||||||
classes = {}
|
|
||||||
contexts = []
|
contexts = []
|
||||||
calls = []
|
|
||||||
|
|
||||||
def collect(script, namespace=""):
|
def collect(script, namespace=""):
|
||||||
if script is None:
|
if script is None:
|
||||||
return
|
return
|
||||||
@@ -116,7 +131,9 @@ def _analyze(tree, typed="", marker=""):
|
|||||||
collect(_body(args[2]), _qualified(args[1].contents, namespace))
|
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:
|
elif routine == "oo::class" and len(args) == 3 and args[0].contents == "create" and args[1].contents:
|
||||||
name = _qualified(args[1].contents, namespace)
|
name = _qualified(args[1].contents, namespace)
|
||||||
info = classes.setdefault(name, ClassInfo(namespace=namespace))
|
info = ClassInfo(namespace=namespace)
|
||||||
|
info.definition = name_location(args[1], uri, source)
|
||||||
|
classes[name] = info
|
||||||
body = _body(args[2])
|
body = _body(args[2])
|
||||||
if body is None:
|
if body is None:
|
||||||
continue
|
continue
|
||||||
@@ -127,13 +144,31 @@ def _analyze(tree, typed="", marker=""):
|
|||||||
if method.routine.contents == "method" and len(ma) == 3 and ma[0].contents:
|
if method.routine.contents == "method" and len(ma) == 3 and ma[0].contents:
|
||||||
method_body = _body(ma[2])
|
method_body = _body(ma[2])
|
||||||
info.methods[ma[0].contents] = (ma[1].contents or "", method_body)
|
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))
|
contexts.append((method_body, namespace, name))
|
||||||
elif method.routine.contents in {"constructor", "destructor"} and ma:
|
elif method.routine.contents in {"constructor", "destructor"} and ma:
|
||||||
if method.routine.contents == "constructor" and len(ma) == 2:
|
if method.routine.contents == "constructor" and len(ma) == 2:
|
||||||
info.constructor = ma[0].contents or ""
|
info.constructor = ma[0].contents or ""
|
||||||
|
info.constructor_definition = name_location(method.routine, uri, source)
|
||||||
contexts.append((_body(ma[-1]), namespace, name))
|
contexts.append((_body(ma[-1]), namespace, name))
|
||||||
|
|
||||||
collect(tree)
|
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
|
result = None
|
||||||
|
|
||||||
def receiver(node, env, objects, namespace, owner, depth=0):
|
def receiver(node, env, objects, namespace, owner, depth=0):
|
||||||
@@ -180,13 +215,15 @@ def _analyze(tree, typed="", marker=""):
|
|||||||
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner)
|
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner)
|
||||||
method_name = args[0].contents if args else None
|
method_name = args[0].contents if args else None
|
||||||
if cls in classes and method_name in classes[cls].methods:
|
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]))
|
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"}:
|
elif name and _qualified(name, namespace) in classes and method_name in {"new", "create"}:
|
||||||
cls = _qualified(name, namespace)
|
cls = _qualified(name, namespace)
|
||||||
parameters = classes[cls].constructor
|
parameters = classes[cls].constructor
|
||||||
if method_name == "create":
|
if method_name == "create":
|
||||||
parameters = "objectName " + parameters
|
parameters = "objectName " + parameters
|
||||||
calls.append(MethodCall(cmd, f"{cls} {method_name}", 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:
|
if marker and args and args[0].contents == typed + marker:
|
||||||
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner)
|
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner)
|
||||||
if cls in classes:
|
if cls in classes:
|
||||||
@@ -221,6 +258,6 @@ def _analyze(tree, typed="", marker=""):
|
|||||||
return classes, result, calls
|
return classes, result, calls
|
||||||
|
|
||||||
|
|
||||||
def resolved_method_calls(source):
|
def resolved_method_calls(source, external_classes=None):
|
||||||
tree = parse_completion_source(source)
|
tree = parse_completion_source(source)
|
||||||
return _analyze(tree)[2] if tree is not None else []
|
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
|
||||||
@@ -6,7 +6,7 @@ from tclint.syntax_tree import Command
|
|||||||
from tools.tcloo_completion import _body, _qualified
|
from tools.tcloo_completion import _body, _qualified
|
||||||
|
|
||||||
|
|
||||||
def class_symbols(tree):
|
def class_symbols(tree, external_classes=None, reference_targets=None):
|
||||||
"""Return qualified class declarations and statically resolved name nodes."""
|
"""Return qualified class declarations and statically resolved name nodes."""
|
||||||
declarations = {}
|
declarations = {}
|
||||||
commands = []
|
commands = []
|
||||||
@@ -34,13 +34,17 @@ def class_symbols(tree):
|
|||||||
walk(child, namespace, in_class)
|
walk(child, namespace, in_class)
|
||||||
|
|
||||||
walk(tree)
|
walk(tree)
|
||||||
|
known_classes = set(external_classes or ()) | declarations.keys()
|
||||||
references = []
|
references = []
|
||||||
for command, namespace in commands:
|
for command, namespace in commands:
|
||||||
name = command.routine.contents
|
name = command.routine.contents
|
||||||
if name and any(candidate in declarations for candidate in (
|
target = next((candidate for candidate in (
|
||||||
_qualified(name, namespace), _qualified(name, ""),
|
_qualified(name, namespace), _qualified(name, ""),
|
||||||
)):
|
) if candidate in known_classes), None) if name else None
|
||||||
|
if target:
|
||||||
references.append(command.routine)
|
references.append(command.routine)
|
||||||
|
if reference_targets is not None:
|
||||||
|
reference_targets[command.routine.pos] = target
|
||||||
return declarations, references
|
return declarations, references
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,26 @@ def _argument_completion_labels(source: str) -> set[str] | None:
|
|||||||
return {item.label for item in completion.items}
|
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):
|
def test_array_keys_complete_in_set_and_substitution(tmp_path: Path, monkeypatch):
|
||||||
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
workspace = _document(
|
workspace = _document(
|
||||||
|
|||||||
@@ -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,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
|
||||||
@@ -403,3 +403,7 @@ oo::class create Point3D {
|
|||||||
return [self]
|
return [self]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set v1 [MCS new]
|
||||||
|
|
||||||
|
$v1 initArray
|
||||||
|
|||||||
Reference in New Issue
Block a user