Merge pull request 'Cross-file navigation and rename for PSC .def templates and addresses' (#46) from DEF_file into main
build_and_puplish.yml / build_and_publish (release) Successful in 34s
build_and_puplish.yml / build_and_publish (release) Successful in 34s
This commit was merged in pull request #46.
This commit is contained in:
@@ -5,6 +5,13 @@ Versions correspond to the Git tags of this repository.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Go to Definition from block template and address arguments in Tcl (`MOM_do_template`, `MOM_force`, `MOM_suppress`, `MOM_ask_address_value`, ...) to the `BLOCK_TEMPLATE`/`ADDRESS` declaration in the PSC `.def` files
|
||||||
|
- Hover over block templates shows the template body; hover over addresses shows format, leader, trailer, min/max, and modality
|
||||||
|
- Find References and Rename for block templates and addresses across Tcl and `.def` files, including addresses used inside block templates
|
||||||
|
- Go to Definition, hover, references, and rename also work inside `.def` files
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
- Document DEF block templates, addresses, `BLOCK_LIST`/`ADDR_LIST`, and the formatting changes in the README
|
- Document DEF block templates, addresses, `BLOCK_LIST`/`ADDR_LIST`, and the formatting changes in the README
|
||||||
|
|||||||
@@ -75,6 +75,26 @@ Select the keyword from the completion list (or type it completely) to open the
|
|||||||
directly after the keyword, such as `ADDR_LISTSP`, narrows it. The keyword is replaced by the
|
directly after the keyword, such as `ADDR_LISTSP`, narrows it. The keyword is replaced by the
|
||||||
selected quoted name.
|
selected quoted name.
|
||||||
|
|
||||||
|
### Navigation between Tcl and DEF files
|
||||||
|
|
||||||
|
Block template and address names are linked to their declarations in the PSC `.def` files:
|
||||||
|
|
||||||
|
- **Go to Definition** (F12) on `MOM_do_template "steady_rest"` or `MOM_force Once X` jumps to
|
||||||
|
`BLOCK_TEMPLATE steady_rest` or `ADDRESS X`. Inside a `.def` file, an address used in a block
|
||||||
|
template (`X[$mom_pos(0)]`) jumps to its `ADDRESS` declaration.
|
||||||
|
- **Hover** shows the body of a block template, or the format (resolved to its `FORMAT`
|
||||||
|
definition), leader, trailer, min/max, and modality (`FORCE`) of an address.
|
||||||
|
- **Find All References** (Shift+F12) lists the declaration, all Tcl calls, and all block templates
|
||||||
|
that use an address.
|
||||||
|
- **Rename** (F2) changes the declaration and all usages in Tcl and `.def` files together. Only
|
||||||
|
names declared in a loaded `.def` file can be renamed.
|
||||||
|
|
||||||
|
Recognized Tcl commands: `MOM_do_template`, `MOM_add_to_block_buffer`, `MOM_polar_motion`,
|
||||||
|
`MOM_force_block`, `MOM_ask_address_value`, `MOM_add_to_address_buffer`, `MOM_enable_address`,
|
||||||
|
`MOM_disable_address`, `MOM_force`, `MOM_suppress`, `MOM_incremental`, and
|
||||||
|
`MOM_ask_definition_element`/`MOM_has_definition_element` with `ADDRESS` or `BLOCK`. Names built
|
||||||
|
from variables (`MOM_do_template $name`) cannot be resolved statically.
|
||||||
|
|
||||||
## NX Tcl Remote Debugger
|
## NX Tcl Remote Debugger
|
||||||
|
|
||||||
### Add a VS Code attach configuration
|
### Add a VS Code attach configuration
|
||||||
@@ -133,6 +153,7 @@ Simply open any supported file type and enjoy:
|
|||||||
- Context-aware completion with local symbols ranked before workspace and built-in symbols, plus semantic arguments, local paths, Tcl subcommands, and options such as `string compare -nocase`
|
- Context-aware completion with local symbols ranked before workspace and built-in symbols, plus semantic arguments, local paths, Tcl subcommands, and options such as `string compare -nocase`
|
||||||
- Placeholder-based snippets for common Tcl control structures and procedures
|
- Placeholder-based snippets for common Tcl control structures and procedures
|
||||||
- Block template and address suggestions from PSC DEF files, including `BLOCK_LIST` and `ADDR_LIST`
|
- Block template and address suggestions from PSC DEF files, including `BLOCK_LIST` and `ADDR_LIST`
|
||||||
|
- Go to Definition, hover, references, and rename for block templates and addresses across Tcl and DEF files
|
||||||
- Remote NX Tcl debugging with breakpoints and full stepping
|
- Remote NX Tcl debugging with breakpoints and full stepping
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import * as vscode from "vscode"
|
||||||
|
import { LanguageClient, Range, State } from "vscode-languageclient/node"
|
||||||
|
|
||||||
|
// .def files are not synchronized with the language server, so each request
|
||||||
|
// carries the current text of the document.
|
||||||
|
const DEF_SELECTOR: vscode.DocumentSelector = { scheme: "file", language: "def" }
|
||||||
|
|
||||||
|
function params(document: vscode.TextDocument, position: vscode.Position, extra: object = {}) {
|
||||||
|
return {
|
||||||
|
textDocument: { uri: document.uri.toString() },
|
||||||
|
position: { line: position.line, character: position.character },
|
||||||
|
text: document.getText(),
|
||||||
|
...extra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerDefProviders(getClient: () => LanguageClient | undefined): vscode.Disposable[] {
|
||||||
|
const request = async <T>(
|
||||||
|
method: string,
|
||||||
|
document: vscode.TextDocument,
|
||||||
|
position: vscode.Position,
|
||||||
|
token: vscode.CancellationToken,
|
||||||
|
extra: object = {}
|
||||||
|
): Promise<{ client: LanguageClient; result: T } | undefined> => {
|
||||||
|
const client = getClient()
|
||||||
|
if (!client || client.state !== State.Running) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const result = await client.sendRequest<T>(method, params(document, position, extra), token)
|
||||||
|
return result ? { client, result } : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
vscode.languages.registerDefinitionProvider(DEF_SELECTOR, {
|
||||||
|
async provideDefinition(document, position, token) {
|
||||||
|
const response = await request<any>("nxPostSupport/def/definition", document, position, token)
|
||||||
|
return response && response.client.protocol2CodeConverter.asDefinitionResult(response.result, token)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
vscode.languages.registerHoverProvider(DEF_SELECTOR, {
|
||||||
|
async provideHover(document, position, token) {
|
||||||
|
const response = await request<any>("nxPostSupport/def/hover", document, position, token)
|
||||||
|
return response && response.client.protocol2CodeConverter.asHover(response.result)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
vscode.languages.registerReferenceProvider(DEF_SELECTOR, {
|
||||||
|
async provideReferences(document, position, context, token) {
|
||||||
|
const response = await request<any>("nxPostSupport/def/references", document, position, token, {
|
||||||
|
includeDeclaration: context.includeDeclaration
|
||||||
|
})
|
||||||
|
return response && response.client.protocol2CodeConverter.asReferences(response.result, token)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
vscode.languages.registerRenameProvider(DEF_SELECTOR, {
|
||||||
|
async prepareRename(document, position, token) {
|
||||||
|
const response = await request<{ range: Range; placeholder: string }>(
|
||||||
|
"nxPostSupport/def/prepareRename",
|
||||||
|
document,
|
||||||
|
position,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("Only declared block templates and addresses can be renamed.")
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
range: response.client.protocol2CodeConverter.asRange(response.result.range),
|
||||||
|
placeholder: response.result.placeholder
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async provideRenameEdits(document, position, newName, token) {
|
||||||
|
const response = await request<any>("nxPostSupport/def/rename", document, position, token, {
|
||||||
|
newName
|
||||||
|
})
|
||||||
|
if (!response) {
|
||||||
|
throw new Error(`"${newName}" is not a valid block template or address name.`)
|
||||||
|
}
|
||||||
|
return response.client.protocol2CodeConverter.asWorkspaceEdit(response.result, token)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
defDocumentSymbolProvider,
|
defDocumentSymbolProvider,
|
||||||
definitionCdlEventHandler
|
definitionCdlEventHandler
|
||||||
} from "./common/handlers"
|
} from "./common/handlers"
|
||||||
|
import { registerDefProviders } from "./common/defProviders"
|
||||||
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
|
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
|
||||||
import {
|
import {
|
||||||
checkVersion,
|
checkVersion,
|
||||||
@@ -234,6 +235,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
context.subscriptions.push(formatDefProvider)
|
context.subscriptions.push(formatDefProvider)
|
||||||
|
context.subscriptions.push(...registerDefProviders(() => client))
|
||||||
|
|
||||||
const cdlSymbolProvider = vscode.languages.registerDocumentSymbolProvider(
|
const cdlSymbolProvider = vscode.languages.registerDocumentSymbolProvider(
|
||||||
{ scheme: "file", language: "cdl" },
|
{ scheme: "file", language: "cdl" },
|
||||||
|
|||||||
+125
-2
@@ -52,6 +52,15 @@ from tools.completion_items import (
|
|||||||
completion_context,
|
completion_context,
|
||||||
ranked_completion_items,
|
ranked_completion_items,
|
||||||
)
|
)
|
||||||
|
from tools.def_navigation import (
|
||||||
|
all_def_target_locations,
|
||||||
|
def_declarations,
|
||||||
|
def_definition_locations,
|
||||||
|
def_hover_markdown,
|
||||||
|
def_rename_edits,
|
||||||
|
def_symbol_at,
|
||||||
|
)
|
||||||
|
from tools.def_symbols import parse_def_document
|
||||||
from tools.folding_ranges import build_folding_ranges
|
from tools.folding_ranges import build_folding_ranges
|
||||||
from tools.index_cache import IndexCache
|
from tools.index_cache import IndexCache
|
||||||
from tools.inlay_hint import (
|
from tools.inlay_hint import (
|
||||||
@@ -59,6 +68,7 @@ from tools.inlay_hint import (
|
|||||||
build_builtin_inlay_signatures,
|
build_builtin_inlay_signatures,
|
||||||
)
|
)
|
||||||
from tools.navigation import (
|
from tools.navigation import (
|
||||||
|
DEF_SYMBOL_KINDS,
|
||||||
SymbolIdentity,
|
SymbolIdentity,
|
||||||
call_hierarchy_identity,
|
call_hierarchy_identity,
|
||||||
call_hierarchy_items,
|
call_hierarchy_items,
|
||||||
@@ -295,6 +305,7 @@ SYMBOL_LIST_KEYWORDS = {
|
|||||||
"BLOCK_LIST": (lambda: LSP_SERVER.block_template_items(), "block templates"),
|
"BLOCK_LIST": (lambda: LSP_SERVER.block_template_items(), "block templates"),
|
||||||
"ADDR_LIST": (lambda: LSP_SERVER.address_items(), "addresses"),
|
"ADDR_LIST": (lambda: LSP_SERVER.address_items(), "addresses"),
|
||||||
}
|
}
|
||||||
|
DEF_NAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
||||||
_WORD_BEFORE_CURSOR_RE = re.compile(r"(?<![$\w])[A-Za-z_]+$")
|
_WORD_BEFORE_CURSOR_RE = re.compile(r"(?<![$\w])[A-Za-z_]+$")
|
||||||
|
|
||||||
|
|
||||||
@@ -709,6 +720,10 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def_hover = _tcl_def_hover(document_uri, pos, token)
|
||||||
|
if def_hover is not None:
|
||||||
|
return def_hover
|
||||||
|
|
||||||
# 1) If token is a known MOM proc/variable, return built-in hover
|
# 1) If token is a known MOM proc/variable, return built-in hover
|
||||||
match = BUILTIN_HOVER_ITEMS.get(token)
|
match = BUILTIN_HOVER_ITEMS.get(token)
|
||||||
if match and match.get("kind") == "function":
|
if match and match.get("kind") == "function":
|
||||||
@@ -748,6 +763,22 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _def_target(identity: SymbolIdentity) -> tuple[str, str] | None:
|
||||||
|
return (identity.kind, identity.name) if identity.kind in DEF_SYMBOL_KINDS else None
|
||||||
|
|
||||||
|
|
||||||
|
def _tcl_def_hover(uri: str, position: lsp.Position, token: str) -> lsp.Hover | None:
|
||||||
|
documents = LSP_SERVER.def_documents_snapshot()
|
||||||
|
if not any(token in document.names(kind) for document in documents.values() for kind in DEF_SYMBOL_KINDS):
|
||||||
|
return None
|
||||||
|
context = _navigation_context(uri, position)
|
||||||
|
target = context and _def_target(context[3])
|
||||||
|
markdown = target and def_hover_markdown(documents, target)
|
||||||
|
if not markdown:
|
||||||
|
return None
|
||||||
|
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=markdown), range=context[2].range)
|
||||||
|
|
||||||
|
|
||||||
@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 TclOO declarations, then indexed proc and variable definitions."""
|
"""Resolve TclOO declarations, then indexed proc and variable definitions."""
|
||||||
@@ -770,6 +801,9 @@ def goto_definition(params: lsp.DefinitionParams):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
indexes, definitions, _, identity = context
|
indexes, definitions, _, identity = context
|
||||||
|
target = _def_target(identity)
|
||||||
|
if target is not None:
|
||||||
|
return _sorted_locations(def_definition_locations(LSP_SERVER.def_documents_snapshot(), target)) or None
|
||||||
locations = [lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in matching_occurrences(identity, indexes, definitions) if occurrence.is_definition]
|
locations = [lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in matching_occurrences(identity, indexes, definitions) if occurrence.is_definition]
|
||||||
return _sorted_locations(locations) or None
|
return _sorted_locations(locations) or None
|
||||||
|
|
||||||
@@ -811,6 +845,11 @@ def references(params: lsp.ReferenceParams) -> list[lsp.Location]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
indexes, definitions, _, identity = context
|
indexes, definitions, _, identity = context
|
||||||
|
target = _def_target(identity)
|
||||||
|
if target is not None:
|
||||||
|
return all_def_target_locations(
|
||||||
|
LSP_SERVER.def_documents_snapshot(), indexes, target, params.context.include_declaration
|
||||||
|
)
|
||||||
locations = [
|
locations = [
|
||||||
lsp.Location(uri=index.uri, range=occurrence.range)
|
lsp.Location(uri=index.uri, range=occurrence.range)
|
||||||
for index, occurrence in matching_occurrences(identity, indexes, definitions)
|
for index, occurrence in matching_occurrences(identity, indexes, definitions)
|
||||||
@@ -858,7 +897,11 @@ def prepare_rename(params: lsp.PrepareRenameParams):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
indexes, definitions, occurrence, identity = context
|
indexes, definitions, occurrence, identity = context
|
||||||
if not _is_renamable(identity, indexes, definitions):
|
target = _def_target(identity)
|
||||||
|
if target is not None:
|
||||||
|
if not def_declarations(LSP_SERVER.def_documents_snapshot(), target):
|
||||||
|
return None
|
||||||
|
elif not _is_renamable(identity, indexes, definitions):
|
||||||
return None
|
return None
|
||||||
return lsp.PrepareRenamePlaceholder(range=occurrence.range, placeholder=occurrence.placeholder)
|
return lsp.PrepareRenamePlaceholder(range=occurrence.range, placeholder=occurrence.placeholder)
|
||||||
|
|
||||||
@@ -868,7 +911,7 @@ def prepare_rename(params: lsp.PrepareRenameParams):
|
|||||||
lsp.RenameOptions(prepare_provider=True),
|
lsp.RenameOptions(prepare_provider=True),
|
||||||
)
|
)
|
||||||
def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None:
|
def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None:
|
||||||
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", params.new_name):
|
if not DEF_NAME_RE.fullmatch(params.new_name):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
context = _navigation_context(params.text_document.uri, params.position)
|
context = _navigation_context(params.text_document.uri, params.position)
|
||||||
@@ -876,6 +919,9 @@ def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
indexes, definitions, _, identity = context
|
indexes, definitions, _, identity = context
|
||||||
|
target = _def_target(identity)
|
||||||
|
if target is not None:
|
||||||
|
return def_rename_edits(LSP_SERVER.def_documents_snapshot(), indexes, target, params.new_name)
|
||||||
if not _is_renamable(identity, indexes, definitions):
|
if not _is_renamable(identity, indexes, definitions):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -905,6 +951,83 @@ def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None:
|
|||||||
return lsp.WorkspaceEdit(changes=changes)
|
return lsp.WorkspaceEdit(changes=changes)
|
||||||
|
|
||||||
|
|
||||||
|
# .def files are not synchronized with the server; the client sends their
|
||||||
|
# current text with each request.
|
||||||
|
DEF_REQUEST_DEFINITION = "nxPostSupport/def/definition"
|
||||||
|
DEF_REQUEST_HOVER = "nxPostSupport/def/hover"
|
||||||
|
DEF_REQUEST_REFERENCES = "nxPostSupport/def/references"
|
||||||
|
DEF_REQUEST_PREPARE_RENAME = "nxPostSupport/def/prepareRename"
|
||||||
|
DEF_REQUEST_RENAME = "nxPostSupport/def/rename"
|
||||||
|
|
||||||
|
|
||||||
|
def _def_request_context(params):
|
||||||
|
uri = params.textDocument.uri
|
||||||
|
position = lsp.Position(line=params.position.line, character=params.position.character)
|
||||||
|
path = uris.to_fs_path(uri)
|
||||||
|
documents = LSP_SERVER.def_documents_snapshot(path, params.text)
|
||||||
|
symbol = def_symbol_at(parse_def_document(params.text), position)
|
||||||
|
if symbol is None:
|
||||||
|
return None
|
||||||
|
target, range_, _ = symbol
|
||||||
|
current = next(key for key in documents if LSP_SERVER.paths_equal(key, path))
|
||||||
|
return documents, {current: uri}, target, range_
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(DEF_REQUEST_DEFINITION)
|
||||||
|
def def_definition(params) -> list[lsp.Location] | None:
|
||||||
|
context = _def_request_context(params)
|
||||||
|
if context is None:
|
||||||
|
return None
|
||||||
|
documents, def_uris, target, _ = context
|
||||||
|
return _sorted_locations(def_definition_locations(documents, target, def_uris)) or None
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(DEF_REQUEST_HOVER)
|
||||||
|
def def_hover(params) -> lsp.Hover | None:
|
||||||
|
context = _def_request_context(params)
|
||||||
|
if context is None:
|
||||||
|
return None
|
||||||
|
documents, _, target, range_ = context
|
||||||
|
markdown = def_hover_markdown(documents, target)
|
||||||
|
if markdown is None:
|
||||||
|
return None
|
||||||
|
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=markdown), range=range_)
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(DEF_REQUEST_REFERENCES)
|
||||||
|
def def_references(params) -> list[lsp.Location]:
|
||||||
|
context = _def_request_context(params)
|
||||||
|
if context is None:
|
||||||
|
return []
|
||||||
|
documents, def_uris, target, _ = context
|
||||||
|
include_declaration = bool(getattr(params, "includeDeclaration", True))
|
||||||
|
indexes, _ = LSP_SERVER.navigation_state()
|
||||||
|
return all_def_target_locations(documents, indexes, target, include_declaration, def_uris)
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(DEF_REQUEST_PREPARE_RENAME)
|
||||||
|
def def_prepare_rename(params):
|
||||||
|
context = _def_request_context(params)
|
||||||
|
if context is None:
|
||||||
|
return None
|
||||||
|
documents, _, target, range_ = context
|
||||||
|
if not def_declarations(documents, target):
|
||||||
|
return None
|
||||||
|
return lsp.PrepareRenamePlaceholder(range=range_, placeholder=target[1])
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(DEF_REQUEST_RENAME)
|
||||||
|
def def_rename(params) -> lsp.WorkspaceEdit | None:
|
||||||
|
if not DEF_NAME_RE.fullmatch(params.newName):
|
||||||
|
return None
|
||||||
|
context = _def_request_context(params)
|
||||||
|
if context is None:
|
||||||
|
return None
|
||||||
|
documents, def_uris, target, _ = context
|
||||||
|
indexes, _ = LSP_SERVER.navigation_state()
|
||||||
|
return def_rename_edits(documents, indexes, target, params.newName, def_uris)
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.WORKSPACE_SYMBOL)
|
@LSP_SERVER.feature(lsp.WORKSPACE_SYMBOL)
|
||||||
def workspace_symbol(params: lsp.WorkspaceSymbolParams):
|
def workspace_symbol(params: lsp.WorkspaceSymbolParams):
|
||||||
return workspace_symbols(LSP_SERVER.navigation_snapshot(), params.query)
|
return workspace_symbols(LSP_SERVER.navigation_snapshot(), params.query)
|
||||||
|
|||||||
+21
-14
@@ -18,7 +18,7 @@ from tools import checks, incremental_parse, 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.tcloo_completion import indexed_classes
|
||||||
from tools.def_symbols import DefSymbols, read_def_symbols
|
from tools.def_symbols import ADDRESS, BLOCK_TEMPLATE, DefDocument, parse_def_document, read_def_source
|
||||||
from tools.file_sourcing import get_all_psc_files, psc_defined_event_files, psc_script_files
|
from tools.file_sourcing import get_all_psc_files, psc_defined_event_files, psc_script_files
|
||||||
from tools.formatter import NxFormatter as Formatter
|
from tools.formatter import NxFormatter as Formatter
|
||||||
from tools.index_cache import FileStat, IndexCache, file_stat
|
from tools.index_cache import FileStat, IndexCache, file_stat
|
||||||
@@ -62,8 +62,8 @@ class TclLanguageServer(LanguageServer):
|
|||||||
self.psc_script_paths: list[str] = []
|
self.psc_script_paths: list[str] = []
|
||||||
self._psc_files: dict[str, list[pathlib.Path]] = {}
|
self._psc_files: dict[str, list[pathlib.Path]] = {}
|
||||||
self._psc_lock = threading.RLock()
|
self._psc_lock = threading.RLock()
|
||||||
# .def path -> BLOCK_TEMPLATE/ADDRESS names, in PSC DefinedEvents order.
|
# .def path -> parsed declarations, in PSC DefinedEvents order.
|
||||||
self.def_symbols: dict[str, DefSymbols] = {}
|
self.def_documents: dict[str, DefDocument] = {}
|
||||||
self.navigation_indexes: dict[str, FileSymbolIndex] = {}
|
self.navigation_indexes: dict[str, FileSymbolIndex] = {}
|
||||||
self.variable_indexes: dict[
|
self.variable_indexes: dict[
|
||||||
str,
|
str,
|
||||||
@@ -257,7 +257,7 @@ class TclLanguageServer(LanguageServer):
|
|||||||
|
|
||||||
def refresh_def_symbols(self, roots, report=LOGGER.warning):
|
def refresh_def_symbols(self, roots, report=LOGGER.warning):
|
||||||
"""Read the block templates and addresses of all .def files listed as PSC DefinedEvents."""
|
"""Read the block templates and addresses of all .def files listed as PSC DefinedEvents."""
|
||||||
symbols: dict[str, DefSymbols] = {}
|
documents: dict[str, DefDocument] = {}
|
||||||
for root in roots:
|
for root in roots:
|
||||||
for psc in get_all_psc_files(root):
|
for psc in get_all_psc_files(root):
|
||||||
try:
|
try:
|
||||||
@@ -266,33 +266,40 @@ class TclLanguageServer(LanguageServer):
|
|||||||
report(f"Could not read PSC {psc}: {error}")
|
report(f"Could not read PSC {psc}: {error}")
|
||||||
continue
|
continue
|
||||||
for def_file in def_files:
|
for def_file in def_files:
|
||||||
if str(def_file) in symbols:
|
if str(def_file) in documents:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
symbols[str(def_file)] = read_def_symbols(def_file)
|
documents[str(def_file)] = parse_def_document(read_def_source(def_file))
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
report(f"Could not read DEF file {def_file}: {error}")
|
report(f"Could not read DEF file {def_file}: {error}")
|
||||||
with self._index_lock:
|
with self._index_lock:
|
||||||
self.def_symbols = symbols
|
self.def_documents = documents
|
||||||
|
|
||||||
def _def_symbol_items(self, attribute: str, kind, description: str) -> list[lsp.CompletionItem]:
|
def def_documents_snapshot(self, current_path=None, current_source: str | None = None) -> dict[str, DefDocument]:
|
||||||
|
"""Return the PSC .def documents; ``current_source`` replaces the file being edited."""
|
||||||
with self._index_lock:
|
with self._index_lock:
|
||||||
symbols = dict(self.def_symbols)
|
documents = dict(self.def_documents)
|
||||||
|
if current_path is not None and current_source is not None:
|
||||||
|
key = next((path for path in documents if self.paths_equal(path, current_path)), os.fspath(current_path))
|
||||||
|
documents[key] = parse_def_document(current_source)
|
||||||
|
return documents
|
||||||
|
|
||||||
|
def _def_symbol_items(self, kind: str, item_kind, description: str) -> list[lsp.CompletionItem]:
|
||||||
return [
|
return [
|
||||||
lsp.CompletionItem(
|
lsp.CompletionItem(
|
||||||
label=name,
|
label=name,
|
||||||
kind=kind,
|
kind=item_kind,
|
||||||
detail=f"{description} ({pathlib.Path(path).name})",
|
detail=f"{description} ({pathlib.Path(path).name})",
|
||||||
)
|
)
|
||||||
for path, def_symbols in symbols.items()
|
for path, document in self.def_documents_snapshot().items()
|
||||||
for name in getattr(def_symbols, attribute)
|
for name in document.names(kind)
|
||||||
]
|
]
|
||||||
|
|
||||||
def block_template_items(self) -> list[lsp.CompletionItem]:
|
def block_template_items(self) -> list[lsp.CompletionItem]:
|
||||||
return self._def_symbol_items("block_templates", lsp.CompletionItemKind.Struct, "Block template")
|
return self._def_symbol_items(BLOCK_TEMPLATE, lsp.CompletionItemKind.Struct, "Block template")
|
||||||
|
|
||||||
def address_items(self) -> list[lsp.CompletionItem]:
|
def address_items(self) -> list[lsp.CompletionItem]:
|
||||||
return self._def_symbol_items("addresses", lsp.CompletionItemKind.Field, "Address")
|
return self._def_symbol_items(ADDRESS, lsp.CompletionItemKind.Field, "Address")
|
||||||
|
|
||||||
def refresh_psc_scripts(self, roots, report=LOGGER.warning):
|
def refresh_psc_scripts(self, roots, report=LOGGER.warning):
|
||||||
"""Index PSC dependencies through the same pipeline as workspace procs."""
|
"""Index PSC dependencies through the same pipeline as workspace procs."""
|
||||||
|
|||||||
+156
-12
@@ -1,11 +1,53 @@
|
|||||||
"""Block templates and addresses declared in NX post definition (.def) files."""
|
"""Block templates, addresses and formats declared in NX post definition (.def) files."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
_BLOCK_TEMPLATE_RE = re.compile(r"^\s*BLOCK_TEMPLATE\s+([^\s{]+)", re.MULTILINE)
|
BLOCK_TEMPLATE = "block_template"
|
||||||
_ADDRESS_RE = re.compile(r"^\s*ADDRESS\s+([^\s{]+)", re.MULTILINE)
|
ADDRESS = "address"
|
||||||
|
FORMAT = "format"
|
||||||
|
|
||||||
|
_KINDS = {"BLOCK_TEMPLATE": BLOCK_TEMPLATE, "ADDRESS": ADDRESS, "FORMAT": FORMAT}
|
||||||
|
_HEADER_RE = re.compile(r"^\s*(BLOCK_TEMPLATE|ADDRESS|FORMAT)\s+([^\s{]+)")
|
||||||
|
# A block template element is an address followed by its expression: X[$mom_pos(0)].
|
||||||
|
_ELEMENT_RE = re.compile(r"^\s*([A-Za-z_]\w*)\[")
|
||||||
|
_PROPERTY_RE = re.compile(r"^\s*([A-Za-z_]+)\s*(.*?)\s*$")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DefDeclaration:
|
||||||
|
kind: str
|
||||||
|
name: str
|
||||||
|
# 0-based line and UTF-16 columns of the name.
|
||||||
|
line: int
|
||||||
|
start: int
|
||||||
|
end: int
|
||||||
|
end_line: int
|
||||||
|
text: str
|
||||||
|
# ADDRESS: body properties such as ("LEADER", '"X"'); FORMAT: (("FORMAT", '"%d"'),).
|
||||||
|
properties: tuple[tuple[str, str], ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DefReference:
|
||||||
|
"""An address used as element of a block template."""
|
||||||
|
|
||||||
|
kind: str
|
||||||
|
name: str
|
||||||
|
line: int
|
||||||
|
start: int
|
||||||
|
end: int
|
||||||
|
container: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DefDocument:
|
||||||
|
declarations: tuple[DefDeclaration, ...] = ()
|
||||||
|
references: tuple[DefReference, ...] = ()
|
||||||
|
|
||||||
|
def names(self, kind: str) -> tuple[str, ...]:
|
||||||
|
return tuple(dict.fromkeys(item.name for item in self.declarations if item.kind == kind))
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -14,23 +56,125 @@ class DefSymbols:
|
|||||||
addresses: tuple[str, ...] = ()
|
addresses: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
def _names(pattern: re.Pattern, source: str) -> tuple[str, ...]:
|
def _utf16_length(text: str) -> int:
|
||||||
return tuple(dict.fromkeys(pattern.findall(source)))
|
return len(text.encode("utf-16-le")) // 2
|
||||||
|
|
||||||
|
|
||||||
|
def _is_comment(line: str) -> bool:
|
||||||
|
return line.lstrip().startswith("#")
|
||||||
|
|
||||||
|
|
||||||
|
def _body(lines: list[str], header: int, rest: str) -> tuple[int, int, int] | None:
|
||||||
|
"""Return (first body line, first body column, closing line) of a braced body.
|
||||||
|
|
||||||
|
The body starts after the first "{" on the header line or on a following
|
||||||
|
line and ends at the next line starting with "}".
|
||||||
|
"""
|
||||||
|
if "{" in rest:
|
||||||
|
after = rest.split("{", 1)[1]
|
||||||
|
if "}" in after:
|
||||||
|
return None
|
||||||
|
start = header + 1
|
||||||
|
else:
|
||||||
|
start = header + 1
|
||||||
|
while start < len(lines) and (not lines[start].strip() or _is_comment(lines[start])):
|
||||||
|
start += 1
|
||||||
|
if start >= len(lines) or not lines[start].lstrip().startswith("{"):
|
||||||
|
return None
|
||||||
|
start += 1
|
||||||
|
end = start
|
||||||
|
while end < len(lines) and not lines[end].lstrip().startswith("}"):
|
||||||
|
end += 1
|
||||||
|
return start, 0, min(end, len(lines) - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_def_document(source: str) -> DefDocument:
|
||||||
|
"""Parse the block templates, addresses and formats of a .def source."""
|
||||||
|
lines = source.splitlines()
|
||||||
|
declarations: list[DefDeclaration] = []
|
||||||
|
references: list[DefReference] = []
|
||||||
|
index = 0
|
||||||
|
while index < len(lines):
|
||||||
|
line = lines[index]
|
||||||
|
match = None if _is_comment(line) else _HEADER_RE.match(line)
|
||||||
|
if match is None:
|
||||||
|
index += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
kind = _KINDS[match.group(1)]
|
||||||
|
name = match.group(2)
|
||||||
|
start = _utf16_length(line[: match.start(2)])
|
||||||
|
end = start + _utf16_length(name)
|
||||||
|
rest = line[match.end(2) :]
|
||||||
|
|
||||||
|
if kind == FORMAT:
|
||||||
|
declarations.append(
|
||||||
|
DefDeclaration(kind, name, index, start, end, index, line.strip(), (("FORMAT", rest.strip()),))
|
||||||
|
)
|
||||||
|
index += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
body = _body(lines, index, rest)
|
||||||
|
end_line = index if body is None else body[2]
|
||||||
|
properties: list[tuple[str, str]] = []
|
||||||
|
if body is not None:
|
||||||
|
for number in range(body[0], body[2]):
|
||||||
|
body_line = lines[number]
|
||||||
|
if _is_comment(body_line) or not body_line.strip():
|
||||||
|
continue
|
||||||
|
if kind == BLOCK_TEMPLATE:
|
||||||
|
element = _ELEMENT_RE.match(body_line)
|
||||||
|
if element is not None:
|
||||||
|
element_start = _utf16_length(body_line[: element.start(1)])
|
||||||
|
references.append(
|
||||||
|
DefReference(
|
||||||
|
ADDRESS,
|
||||||
|
element.group(1),
|
||||||
|
number,
|
||||||
|
element_start,
|
||||||
|
element_start + _utf16_length(element.group(1)),
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prop = _PROPERTY_RE.match(body_line)
|
||||||
|
if prop is not None:
|
||||||
|
properties.append((prop.group(1).upper(), prop.group(2)))
|
||||||
|
|
||||||
|
declarations.append(
|
||||||
|
DefDeclaration(
|
||||||
|
kind,
|
||||||
|
name,
|
||||||
|
index,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
end_line,
|
||||||
|
"\n".join(lines[index : end_line + 1]),
|
||||||
|
tuple(properties),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
index = end_line + 1
|
||||||
|
|
||||||
|
return DefDocument(tuple(declarations), tuple(references))
|
||||||
|
|
||||||
|
|
||||||
def parse_def_symbols(source: str) -> DefSymbols:
|
def parse_def_symbols(source: str) -> DefSymbols:
|
||||||
"""Return the BLOCK_TEMPLATE and ADDRESS names of a .def source in declaration order."""
|
"""Return the BLOCK_TEMPLATE and ADDRESS names of a .def source in declaration order."""
|
||||||
|
document = parse_def_document(source)
|
||||||
return DefSymbols(
|
return DefSymbols(
|
||||||
block_templates=_names(_BLOCK_TEMPLATE_RE, source),
|
block_templates=document.names(BLOCK_TEMPLATE),
|
||||||
addresses=_names(_ADDRESS_RE, source),
|
addresses=document.names(ADDRESS),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def read_def_symbols(path: Path) -> DefSymbols:
|
def read_def_source(path: Path) -> str:
|
||||||
data = path.read_bytes()
|
data = path.read_bytes()
|
||||||
try:
|
try:
|
||||||
source = data.decode("utf-8-sig")
|
return data.decode("utf-8-sig")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
# Older Windows NX layers use the ANSI code page.
|
# Older Windows NX layers use the ANSI code page.
|
||||||
source = data.decode("cp1252")
|
return data.decode("cp1252")
|
||||||
return parse_def_symbols(source)
|
|
||||||
|
|
||||||
|
def read_def_symbols(path: Path) -> DefSymbols:
|
||||||
|
return parse_def_symbols(read_def_source(path))
|
||||||
|
|||||||
@@ -114,6 +114,49 @@ def _name_range(node: Node, raw_name: str, *, variable_sub: bool = False) -> lsp
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DEF_BLOCK_TEMPLATE = "block_template"
|
||||||
|
DEF_ADDRESS = "address"
|
||||||
|
DEF_SYMBOL_KINDS = frozenset({DEF_BLOCK_TEMPLATE, DEF_ADDRESS})
|
||||||
|
|
||||||
|
# NX commands taking .def names: command -> (first argument index, last index or
|
||||||
|
# None for all following arguments, kind).
|
||||||
|
_DEF_ARGUMENTS: dict[str, tuple[tuple[int, int | None, str], ...]] = {
|
||||||
|
"MOM_do_template": ((0, 0, DEF_BLOCK_TEMPLATE),),
|
||||||
|
"MOM_add_to_block_buffer": ((0, 0, DEF_BLOCK_TEMPLATE),),
|
||||||
|
"MOM_polar_motion": ((0, 0, DEF_BLOCK_TEMPLATE),),
|
||||||
|
"MOM_force_block": ((1, None, DEF_BLOCK_TEMPLATE),),
|
||||||
|
"MOM_ask_address_value": ((0, 0, DEF_ADDRESS),),
|
||||||
|
"MOM_add_to_address_buffer": ((0, 0, DEF_ADDRESS),),
|
||||||
|
"MOM_enable_address": ((0, None, DEF_ADDRESS),),
|
||||||
|
"MOM_disable_address": ((0, None, DEF_ADDRESS),),
|
||||||
|
"MOM_force": ((1, None, DEF_ADDRESS),),
|
||||||
|
"MOM_suppress": ((1, None, DEF_ADDRESS),),
|
||||||
|
"MOM_incremental": ((1, None, DEF_ADDRESS),),
|
||||||
|
}
|
||||||
|
_DEFINITION_ELEMENT_KINDS = {"ADDRESS": DEF_ADDRESS, "BLOCK": DEF_BLOCK_TEMPLATE}
|
||||||
|
|
||||||
|
|
||||||
|
def def_argument_kinds(command: Command) -> list[tuple[Node, str]]:
|
||||||
|
"""Return the arguments of ``command`` that name a .def block template or address."""
|
||||||
|
routine = _static_contents(command.routine)
|
||||||
|
if routine in {"MOM_ask_definition_element", "MOM_has_definition_element"}:
|
||||||
|
kind = _DEFINITION_ELEMENT_KINDS.get((_static_contents(command.args[0]) or "").upper()) if command.args else None
|
||||||
|
return [(command.args[1], kind)] if kind and len(command.args) >= 2 else []
|
||||||
|
result = []
|
||||||
|
for first, last, kind in _DEF_ARGUMENTS.get(routine or "", ()):
|
||||||
|
for position, argument in enumerate(command.args):
|
||||||
|
if position >= first and (last is None or position <= last):
|
||||||
|
result.append((argument, kind))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _def_name(node: Node) -> str | None:
|
||||||
|
name = _static_contents(node)
|
||||||
|
if not name or node.contents_pos is None or any(char.isspace() or char in "$[]{}\\\"" for char in name):
|
||||||
|
return None
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
def _node_range(node: Node) -> lsp.Range | None:
|
def _node_range(node: Node) -> lsp.Range | None:
|
||||||
if node.pos is None or node.end_pos is None:
|
if node.pos is None or node.end_pos is None:
|
||||||
return None
|
return None
|
||||||
@@ -436,6 +479,23 @@ def build_file_symbol_index(
|
|||||||
if routine:
|
if routine:
|
||||||
add_proc(command.routine, routine, scope, is_definition=False)
|
add_proc(command.routine, routine, scope, is_definition=False)
|
||||||
|
|
||||||
|
for node, kind in def_argument_kinds(command):
|
||||||
|
name = _def_name(node)
|
||||||
|
if name is None:
|
||||||
|
continue
|
||||||
|
line, column = node.contents_pos
|
||||||
|
occurrences.append(
|
||||||
|
SymbolOccurrence(
|
||||||
|
identity=shared(SymbolIdentity(kind=kind, name=name)),
|
||||||
|
range=lsp.Range(
|
||||||
|
start=lsp.Position(line=line - 1, character=column - 1),
|
||||||
|
end=lsp.Position(line=line - 1, character=column - 1 + len(name)),
|
||||||
|
),
|
||||||
|
placeholder=name,
|
||||||
|
symbol_kind=lsp.SymbolKind.Struct if kind == DEF_BLOCK_TEMPLATE else lsp.SymbolKind.Field,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
declaration_nodes = _variable_declaration_nodes(command)
|
declaration_nodes = _variable_declaration_nodes(command)
|
||||||
declaration_ids = {id(node) for node in declaration_nodes}
|
declaration_ids = {id(node) for node in declaration_nodes}
|
||||||
for node in declaration_nodes:
|
for node in declaration_nodes:
|
||||||
|
|||||||
@@ -430,6 +430,7 @@ VALUES_BY_POSITION: dict[tuple[tuple[str, ...], int], tuple[str, ...]] = {
|
|||||||
(("array", "names"), 3): ("-exact", "-glob", "-regexp"),
|
(("array", "names"), 3): ("-exact", "-glob", "-regexp"),
|
||||||
(("MOM_force",), 1): ("Always", "Once", "Off"),
|
(("MOM_force",), 1): ("Always", "Once", "Off"),
|
||||||
(("MOM_suppress",), 1): ("Always", "Once", "Off"),
|
(("MOM_suppress",), 1): ("Always", "Once", "Off"),
|
||||||
|
(("MOM_do_template",), 2): ("CREATE", "BUFFER"),
|
||||||
(("close",), 2): ("read", "write"),
|
(("close",), 2): ("read", "write"),
|
||||||
(("open",), 2): ("r", "r+", "w", "w+", "a", "a+"),
|
(("open",), 2): ("r", "r+", "w", "w+", "a", "a+"),
|
||||||
(("package", "prefer"), 2): ("latest", "stable"),
|
(("package", "prefer"), 2): ("latest", "stable"),
|
||||||
@@ -475,11 +476,14 @@ DYNAMIC_COMPLETION_RULES = (
|
|||||||
DynamicCompletionRule(("rename",), frozenset({1}), DynamicCompletionKind.PROCEDURE),
|
DynamicCompletionRule(("rename",), frozenset({1}), DynamicCompletionKind.PROCEDURE),
|
||||||
# Namespace-taking commands.
|
# Namespace-taking commands.
|
||||||
DynamicCompletionRule(("MOM_do_template",), frozenset({1}), DynamicCompletionKind.BLOCK_TEMPLATE),
|
DynamicCompletionRule(("MOM_do_template",), frozenset({1}), DynamicCompletionKind.BLOCK_TEMPLATE),
|
||||||
|
DynamicCompletionRule(("MOM_do_template",), frozenset({2}), DynamicCompletionKind.VALUE),
|
||||||
DynamicCompletionRule(("MOM_ask_address_value",), frozenset({1}), DynamicCompletionKind.ADDRESS),
|
DynamicCompletionRule(("MOM_ask_address_value",), frozenset({1}), DynamicCompletionKind.ADDRESS),
|
||||||
DynamicCompletionRule(("MOM_force",), frozenset({1}), DynamicCompletionKind.VALUE),
|
DynamicCompletionRule(("MOM_force",), frozenset({1}), DynamicCompletionKind.VALUE),
|
||||||
DynamicCompletionRule(("MOM_force",), _REPEATED_SUBCOMMAND_ARGUMENTS, DynamicCompletionKind.ADDRESS),
|
DynamicCompletionRule(("MOM_force",), _REPEATED_SUBCOMMAND_ARGUMENTS, DynamicCompletionKind.ADDRESS),
|
||||||
DynamicCompletionRule(("MOM_suppress",), frozenset({1}), DynamicCompletionKind.VALUE),
|
DynamicCompletionRule(("MOM_suppress",), frozenset({1}), DynamicCompletionKind.VALUE),
|
||||||
DynamicCompletionRule(("MOM_suppress",), _REPEATED_SUBCOMMAND_ARGUMENTS, DynamicCompletionKind.ADDRESS),
|
DynamicCompletionRule(("MOM_suppress",), _REPEATED_SUBCOMMAND_ARGUMENTS, DynamicCompletionKind.ADDRESS),
|
||||||
|
DynamicCompletionRule(("MOM_disable_address",), frozenset(range(1, 33)), DynamicCompletionKind.ADDRESS),
|
||||||
|
DynamicCompletionRule(("MOM_enable_address",), _REPEATED_SUBCOMMAND_ARGUMENTS, DynamicCompletionKind.ADDRESS),
|
||||||
DynamicCompletionRule(("namespace", "children"), frozenset({2}), DynamicCompletionKind.NAMESPACE),
|
DynamicCompletionRule(("namespace", "children"), frozenset({2}), DynamicCompletionKind.NAMESPACE),
|
||||||
DynamicCompletionRule(
|
DynamicCompletionRule(
|
||||||
("namespace", "delete"),
|
("namespace", "delete"),
|
||||||
|
|||||||
Reference in New Issue
Block a user