feat: complete BLOCK_LIST/ADDR_LIST keywords and space comments
Add keyword-driven completions for BLOCK_LIST and ADDR_LIST in the LSP server. - While typing a prefix of these keywords the server returns the keyword(s) as incomplete snippet suggestions that trigger a re-request. - Once the keyword is typed the server replaces it with a full list of the corresponding loaded block templates or addresses (quoted), and sets each item's filter_text to include the keyword so further typing narrows results. - Integrate this flow into on_completion and factor the symbol-list logic into a helper that returns either incomplete keyword suggestions or the completed symbol list. Also implement NxFormatter.format_comment to ensure a single space after a leading '#' for comments that don't already start with whitespace, while leaving sequences of '#' (separators), shebangs, and already-spaced comments unchanged. This affects both standalone and inline comments. Add/rename tests to cover the new completions and comment-spacing behavior.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
+26
-1
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user