From 62f74178f7c9168d578c5295190a0976eed43e85 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 25 Jul 2023 18:40:05 -0500 Subject: [PATCH] lsp: Implement "go to definition" --- blueprintcompiler/ast_utils.py | 14 ++++++++++---- blueprintcompiler/language/common.py | 1 + blueprintcompiler/language/values.py | 10 ++++++++++ blueprintcompiler/lsp.py | 16 ++++++++++++++++ blueprintcompiler/lsp_utils.py | 15 +++++++++++++++ blueprintcompiler/parse_tree.py | 2 ++ blueprintcompiler/tokenizer.py | 10 ++++++++++ 7 files changed, 64 insertions(+), 4 deletions(-) diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 59331f9..81958aa 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -22,7 +22,7 @@ from collections import ChainMap, defaultdict from functools import cached_property from .errors import * -from .lsp_utils import DocumentSymbol, SemanticToken +from .lsp_utils import DocumentSymbol, SemanticToken, LocationLink from .tokenizer import Range TType = T.TypeVar("TType") @@ -185,9 +185,8 @@ class AstNode: return getattr(self, name) for child in self.children: - if child.group.start <= idx < child.group.end: - docs = child.get_docs(idx) - if docs is not None: + if idx in child.range: + if docs := child.get_docs(idx): return docs return None @@ -196,6 +195,13 @@ class AstNode: for child in self.children: yield from child.get_semantic_tokens() + def get_reference(self, idx: int) -> T.Optional[LocationLink]: + for child in self.children: + if idx in child.range: + if ref := child.get_reference(idx): + return ref + return None + @property def document_symbol(self) -> T.Optional[DocumentSymbol]: return None diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index e1bfa36..8a6ce9b 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -50,6 +50,7 @@ from ..lsp_utils import ( Completion, CompletionItemKind, DocumentSymbol, + LocationLink, SemanticToken, SemanticTokenType, SymbolKind, diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 888d6a1..b9d719f 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -339,6 +339,16 @@ class IdentLiteral(AstNode): token = self.group.tokens["value"] yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) + def get_reference(self, _idx: int) -> T.Optional[LocationLink]: + ref = self.context[ScopeCtx].objects.get(self.ident) + if ref is None and self.root.is_legacy_template(self.ident): + ref = self.root.template + + if ref: + return LocationLink(self.range, ref.range, ref.ranges["id"]) + else: + return None + class Literal(AstNode): grammar = AnyOf( diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index ec161a8..a7e5f9b 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -204,6 +204,7 @@ class LanguageServer: "codeActionProvider": {}, "hoverProvider": True, "documentSymbolProvider": True, + "definitionProvider": True, }, "serverInfo": { "name": "Blueprint", @@ -389,6 +390,21 @@ class LanguageServer: self._send_response(id, [to_json(symbol) for symbol in symbols]) + @command("textDocument/definition") + def definition(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + idx = utils.pos_to_idx( + params["position"]["line"], params["position"]["character"], open_file.text + ) + definition = open_file.ast.get_reference(idx) + if definition is None: + self._send_response(id, None) + else: + self._send_response( + id, + definition.to_json(open_file.uri), + ) + def _send_file_updates(self, open_file: OpenFile): self._send_notification( "textDocument/publishDiagnostics", diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index 2225ba2..9da8461 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -169,3 +169,18 @@ class DocumentSymbol: selection_range: Range detail: T.Optional[str] = None children: T.List["DocumentSymbol"] = field(default_factory=list) + + +@dataclass +class LocationLink: + origin_selection_range: Range + target_range: Range + target_selection_range: Range + + def to_json(self, target_uri: str): + return { + "originSelectionRange": self.origin_selection_range.to_json(), + "targetUri": target_uri, + "targetRange": self.target_range.to_json(), + "targetSelectionRange": self.target_selection_range.to_json(), + } diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index b29bd96..a8efddb 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -81,6 +81,8 @@ class ParseGroup: self.keys[key] = val self.tokens[key] = token + if token: + self.set_range(key, token.range) def set_range(self, key: str, range: Range): assert_true(key not in self.ranges) diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index 64abf0f..e1066ac 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -24,6 +24,7 @@ from dataclasses import dataclass from enum import Enum from .errors import CompileError, CompilerBugError +from . import utils class TokenType(Enum): @@ -127,3 +128,12 @@ class Range: if b is None: return a return Range(min(a.start, b.start), max(a.end, b.end), a.original_text) + + def __contains__(self, other: T.Union[int, "Range"]) -> bool: + if isinstance(other, int): + return self.start <= other <= self.end + else: + return self.start <= other.start and self.end >= other.end + + def to_json(self): + return utils.idxs_to_range(self.start, self.end, self.original_text)