@@ -444,6 +444,77 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DEFINITION)
|
||||||
|
def goto_definition(params: lsp.DefinitionParams):
|
||||||
|
"""Provide go-to-definition locations for Tcl procs.
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
- Find the token under the cursor.
|
||||||
|
- If it matches a custom proc collected in proc_signatures, locate its declaration
|
||||||
|
by searching the current document first, then other indexed files.
|
||||||
|
- Return a Location pointing to the proc name in its declaration line.
|
||||||
|
"""
|
||||||
|
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
pos = params.position
|
||||||
|
try:
|
||||||
|
line = doc.lines[pos.line]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Identify token under cursor
|
||||||
|
token = None
|
||||||
|
for m in re.finditer(r"\b\w+\b", line):
|
||||||
|
if m.start() <= pos.character <= m.end():
|
||||||
|
token = m.group(0)
|
||||||
|
break
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Helper to search a single source text for a proc declaration
|
||||||
|
def find_decl_in_source(source_text: str, uri: str) -> Optional[lsp.Location]:
|
||||||
|
lines = source_text.split("\n")
|
||||||
|
pattern = re.compile(r"^\s*proc\s+" + re.escape(token) + r"\b")
|
||||||
|
for i, ln in enumerate(lines):
|
||||||
|
m = pattern.match(ln)
|
||||||
|
if m:
|
||||||
|
start_char = ln.find(token)
|
||||||
|
if start_char < 0:
|
||||||
|
start_char = max(m.end() - len(token), 0)
|
||||||
|
start = lsp.Position(i, start_char)
|
||||||
|
end = lsp.Position(i, start_char + len(token))
|
||||||
|
return lsp.Location(uri=uri, range=lsp.Range(start=start, end=end))
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 1) Search in current document
|
||||||
|
loc = find_decl_in_source(doc.source, doc.uri)
|
||||||
|
if loc:
|
||||||
|
return loc
|
||||||
|
|
||||||
|
# 2) Search in indexed files from proc_signatures
|
||||||
|
# Build list of candidate files that declare this token as a proc
|
||||||
|
candidate_files: List[str] = []
|
||||||
|
for file_path, procs in LSP_SERVER.proc_signatures.items():
|
||||||
|
if token in procs:
|
||||||
|
candidate_files.append(file_path)
|
||||||
|
|
||||||
|
for fp in candidate_files:
|
||||||
|
uri = pathlib.Path(fp).as_uri()
|
||||||
|
# Try to get from workspace if available; else read from disk
|
||||||
|
try:
|
||||||
|
other_doc = LSP_SERVER.workspace.get_text_document(uri)
|
||||||
|
source = other_doc.source
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
source = pathlib.Path(fp).read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
loc = find_decl_in_source(source, uri)
|
||||||
|
if loc:
|
||||||
|
return loc
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
# Linting features end here
|
# Linting features end here
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
@@ -508,6 +579,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),
|
||||||
semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
|
semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
|
||||||
|
definition_provider=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Ensure server/src is on the path for imports
|
||||||
|
THIS_DIR = Path(__file__).parent
|
||||||
|
SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||||
|
if str(SRC_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC_DIR))
|
||||||
|
|
||||||
|
import lsprotocol.types as lsp # type: ignore
|
||||||
|
from lsp_server import LSP_SERVER, goto_definition # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def _loc_to_tuple(loc: lsp.Location) -> tuple[str, int, int, int, int]:
|
||||||
|
"""Helper to normalize Location into a tuple for easy asserts."""
|
||||||
|
return (
|
||||||
|
loc.uri,
|
||||||
|
loc.range.start.line,
|
||||||
|
loc.range.start.character,
|
||||||
|
loc.range.end.line,
|
||||||
|
loc.range.end.character,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_first_location(result) -> lsp.Location | None:
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
if isinstance(result, list):
|
||||||
|
return result[0] if result else None
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def test_goto_definition_same_file(tmp_path: Path):
|
||||||
|
source_lines = [
|
||||||
|
"proc add {a b} {",
|
||||||
|
" return [expr {$a + $b}]",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
"set x [add 1 2]",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
uri = Path(tmp_path / "same.tcl").as_uri()
|
||||||
|
# Put a text document into the workspace
|
||||||
|
LSP_SERVER.workspace.put_text_document(
|
||||||
|
lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Position on the word 'add' in the last line
|
||||||
|
line_idx = 4
|
||||||
|
char_idx = source_lines[line_idx].index("add") + 1 # somewhere inside token
|
||||||
|
|
||||||
|
params = lsp.DefinitionParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=uri),
|
||||||
|
position=lsp.Position(line=line_idx, character=char_idx),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = goto_definition(params)
|
||||||
|
loc = _extract_first_location(result)
|
||||||
|
|
||||||
|
assert loc is not None
|
||||||
|
assert loc.uri == uri
|
||||||
|
# Definition should be on line 0 at the token 'add'
|
||||||
|
start = loc.range.start
|
||||||
|
end = loc.range.end
|
||||||
|
assert start.line == 0
|
||||||
|
assert end.line == 0
|
||||||
|
assert source_lines[0][start.character : end.character] == "add"
|
||||||
|
|
||||||
|
|
||||||
|
def test_goto_definition_cross_file(tmp_path: Path):
|
||||||
|
# File A declares the proc
|
||||||
|
a_lines = [
|
||||||
|
"proc myproc {arg} {",
|
||||||
|
" return $arg",
|
||||||
|
"}",
|
||||||
|
]
|
||||||
|
a_src = "\n".join(a_lines)
|
||||||
|
a_path = tmp_path / "a.tcl"
|
||||||
|
a_uri = a_path.as_uri()
|
||||||
|
LSP_SERVER.workspace.put_text_document(
|
||||||
|
lsp.TextDocumentItem(uri=a_uri, language_id="tcl", version=1, text=a_src)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update indices for file A so proc_signatures gets populated
|
||||||
|
doc_a = LSP_SERVER.workspace.get_text_document(a_uri)
|
||||||
|
LSP_SERVER.update_poco_completion_for_file(doc_a)
|
||||||
|
|
||||||
|
# File B calls the proc
|
||||||
|
b_lines = [
|
||||||
|
"set y [myproc 42]",
|
||||||
|
]
|
||||||
|
b_src = "\n".join(b_lines)
|
||||||
|
b_uri = (tmp_path / "b.tcl").as_uri()
|
||||||
|
LSP_SERVER.workspace.put_text_document(
|
||||||
|
lsp.TextDocumentItem(uri=b_uri, language_id="tcl", version=1, text=b_src)
|
||||||
|
)
|
||||||
|
|
||||||
|
call_line = 0
|
||||||
|
call_char = b_lines[0].index("myproc") + 2
|
||||||
|
|
||||||
|
params = lsp.DefinitionParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=b_uri),
|
||||||
|
position=lsp.Position(line=call_line, character=call_char),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = goto_definition(params)
|
||||||
|
loc = _extract_first_location(result)
|
||||||
|
|
||||||
|
assert loc is not None
|
||||||
|
assert loc.uri == a_uri
|
||||||
|
start = loc.range.start
|
||||||
|
end = loc.range.end
|
||||||
|
assert start.line == 0
|
||||||
|
assert a_lines[start.line][start.character : end.character] == "myproc"
|
||||||
|
|
||||||
Reference in New Issue
Block a user