Complete BLOCK_LIST/ADDR_LIST completions and format comment spacing #45

Merged
Christoph merged 1 commits from dynamic_snippets into main 2026-09-24 12:27:56 +00:00
4 changed files with 159 additions and 1 deletions
+65
View File
@@ -85,6 +85,7 @@ from tools.tcl_command_completion import (
TCL_COMMAND_ITEMS,
TCL_COMMAND_NAMES,
DynamicCompletionKind,
line_prefix_at_position,
path_completion_items,
tcl_argument_completion,
)
@@ -279,6 +280,66 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
lsp.CompletionOptions(trigger_characters=["$", " ", "-", "(", ","]),
)
def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
result = _on_completion(params)
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
symbol_list = _symbol_list_completion(LSP_SERVER.get_lines(doc), params.position)
if symbol_list is not None and symbol_list.is_incomplete:
# Re-request while typing so the full list appears at BLOCK_LIST/ADDR_LIST.
result.items = [*symbol_list.items, *result.items]
result.is_incomplete = True
return result
# Keywords that expand to all loaded .def names: keyword -> (items, description).
SYMBOL_LIST_KEYWORDS = {
"BLOCK_LIST": (lambda: LSP_SERVER.block_template_items(), "block templates"),
"ADDR_LIST": (lambda: LSP_SERVER.address_items(), "addresses"),
}
_WORD_BEFORE_CURSOR_RE = re.compile(r"(?<![$\w])[A-Za-z_]+$")
def _symbol_list_completion(source_lines, position: lsp.Position) -> lsp.CompletionList | None:
"""Complete BLOCK_LIST/ADDR_LIST to all loaded block templates/addresses.
While the typed word is a prefix of a keyword, an incomplete list with the
keyword itself is returned. Once the keyword is typed, the complete list of
quoted names replaces it.
"""
line_prefix = line_prefix_at_position(source_lines, position)
match = _WORD_BEFORE_CURSOR_RE.search(line_prefix or "")
if match is None:
return None
word = match.group()
for keyword, (symbol_items, _) in SYMBOL_LIST_KEYWORDS.items():
if not word.startswith(keyword):
continue
items = []
ranked = ranked_completion_items(((0, item) for item in symbol_items()), CompletionContext.GENERAL)
for item in ranked:
item = _quoted_item(item, source_lines, position, word)
# Keep every name visible for the typed keyword; text after it narrows.
item.filter_text = f"{keyword}{item.label}"
items.append(item)
return lsp.CompletionList(is_incomplete=False, items=items)
keywords = [
lsp.CompletionItem(
label=keyword,
kind=lsp.CompletionItemKind.Snippet,
detail=f"Show all loaded {description}",
insert_text=keyword,
sort_text=f"000:{keyword.casefold()}",
command=lsp.Command(title=f"Show {description}", command="editor.action.triggerSuggest"),
)
for keyword, (_, description) in SYMBOL_LIST_KEYWORDS.items()
if keyword.startswith(word)
]
if keywords:
return lsp.CompletionList(is_incomplete=True, items=keywords)
return None
def _on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
position = params.position
source_lines = LSP_SERVER.get_lines(doc)
@@ -302,6 +363,10 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
return lsp.CompletionList(is_incomplete=False, items=array_items)
context = completion_context(source_lines, position)
symbol_list = _symbol_list_completion(source_lines, position)
if symbol_list is not None and symbol_list.is_incomplete is False:
return symbol_list
# Variable completion wins inside command arguments. Otherwise prefer the
# narrow command grammar when the cursor is at a known subcommand/option.
argument_completion = None
+8
View File
@@ -12,6 +12,14 @@ class NxFormatter(BaseFormatter):
while leaving all other formatting behavior unchanged.
"""
def format_comment(self, comment) -> List[str]: # type: ignore[override]
# "#Comment" -> "# Comment". Leave "#", "##..." separators, "#!" and
# comments that already start with whitespace untouched.
value = comment.value
if value and not value[0].isspace() and value[0] not in "#!":
value = " " + value
return [f"#{value}"]
def format_braced_expression(self, expr) -> List[str]: # type: ignore[override]
# This method mirrors BaseFormatter.format_braced_expression but inserts
# a line continuation (" \") between continuation lines similar to
@@ -198,3 +198,63 @@ def test_typed_quotes_are_replaced_not_doubled(tmp_path, monkeypatch):
)
).items
assert _edit(items, "SPOS") == ('"SPOS"', (22, 26))
def test_block_list_prefix_offers_keyword_and_stays_incomplete(tmp_path, monkeypatch):
server, _ = _project(tmp_path, monkeypatch)
uri = (tmp_path / "caller.tcl").as_uri()
source = "MOM_do_template BLOCK_L"
server.workspace.put_text_document(
lsp.TextDocumentItem(uri=uri, language_id="tcl", version=next(_versions), text=source)
)
result = lsp_server.on_completion(
lsp.CompletionParams(
text_document=lsp.TextDocumentIdentifier(uri=uri),
position=lsp.Position(line=0, character=len(source)),
)
)
assert result.is_incomplete
keyword = result.items[0]
assert keyword.label == "BLOCK_LIST"
assert keyword.command.command == "editor.action.triggerSuggest"
def test_block_list_shows_all_templates_quoted(tmp_path, monkeypatch):
server, _ = _project(tmp_path, monkeypatch)
items = _complete(server, tmp_path, "set a 1\n BLOCK_LIST")
assert [item.label for item in items] == ["external_subprogram", "steady_rest"]
assert _edit(items, "steady_rest") == ('"steady_rest"', (4, 14))
assert all(item.filter_text.startswith("BLOCK_LIST") for item in items)
def test_block_list_ignores_variables_and_other_words(tmp_path, monkeypatch):
server, _ = _project(tmp_path, monkeypatch)
for source in ("set x $BLOCK_LIST", "set x MY_BLOCK_LIST", "set x steady"):
labels = [item.label for item in _complete(server, tmp_path, source)]
assert "BLOCK_LIST" not in labels
def test_addr_list_shows_all_addresses_quoted(tmp_path, monkeypatch):
server, _ = _project(tmp_path, monkeypatch)
items = _complete(server, tmp_path, "MOM_force Once ADDR_LIST")
assert [item.label for item in items] == ["SPOS", "X"]
assert _edit(items, "SPOS") == ('"SPOS"', (15, 24))
assert all(item.filter_text.startswith("ADDR_LIST") for item in items)
def test_addr_list_prefix_offers_keyword(tmp_path, monkeypatch):
server, _ = _project(tmp_path, monkeypatch)
uri = (tmp_path / "caller.tcl").as_uri()
source = "ADDR"
server.workspace.put_text_document(
lsp.TextDocumentItem(uri=uri, language_id="tcl", version=next(_versions), text=source)
)
result = lsp_server.on_completion(
lsp.CompletionParams(
text_document=lsp.TextDocumentIdentifier(uri=uri),
position=lsp.Position(line=0, character=len(source)),
)
)
assert result.is_incomplete
assert result.items[0].label == "ADDR_LIST"
assert "BLOCK_LIST" not in {item.label for item in result.items}
@@ -1,4 +1,4 @@
"""Formatting of uplevel bodies."""
"""Formatting with NxFormatter."""
from tclint.format import FormatterOpts
from tools.formatter import NxFormatter
@@ -31,3 +31,28 @@ def test_uplevel_without_level_and_with_variable_level():
assert _format(source) == (
"uplevel {\n\tset x 1\n}\nuplevel $lvl {\n\tset y 2\n}\nuplevel set z 3\n"
)
def test_comment_gets_space_after_hash():
source = (
"#Comment\n"
"# already spaced\n"
"#\tTabbed\n"
"#\n"
"##########\n"
"set x 1 ;#inline\n"
"proc a {} {\n"
"\t#nested\n"
"}\n"
)
assert _format(source) == (
"# Comment\n"
"# already spaced\n"
"#\tTabbed\n"
"#\n"
"##########\n"
"set x 1 ;# inline\n"
"proc a {} {\n"
"\t# nested\n"
"}\n"
)