From dfb09b93579b47119ceb28d8b8244f989c293919 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 1 Nov 2021 23:52:36 -0500 Subject: [PATCH] lsp: Implement semantic tokens --- gtkblueprinttool/ast.py | 8 ++++++- gtkblueprinttool/ast_utils.py | 5 +++++ gtkblueprinttool/errors.py | 5 ++++- gtkblueprinttool/lsp.py | 39 +++++++++++++++++++++++++++++++++-- gtkblueprinttool/lsp_utils.py | 11 ++++++++++ gtkblueprinttool/utils.py | 2 +- 6 files changed, 65 insertions(+), 5 deletions(-) diff --git a/gtkblueprinttool/ast.py b/gtkblueprinttool/ast.py index 67ec208..7c1f51f 100644 --- a/gtkblueprinttool/ast.py +++ b/gtkblueprinttool/ast.py @@ -22,7 +22,7 @@ import typing as T from .ast_utils import * from .errors import assert_true, AlreadyCaughtError, CompileError, CompilerBugError, MultipleErrors from . import gir -from .lsp_utils import Completion, CompletionItemKind +from .lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from .tokenizer import Token from .utils import lazy_prop from .xml_emitter import XmlEmitter @@ -398,6 +398,12 @@ class IdentValue(Value): return type.doc + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: + if isinstance(self.parent.value_type, gir.Enumeration): + token = self.group.tokens["value"] + yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) + + class BaseAttribute(AstNode): """ A helper class for attribute syntax of the form `name: literal_value;`""" diff --git a/gtkblueprinttool/ast_utils.py b/gtkblueprinttool/ast_utils.py index 3aaaffa..aa4803f 100644 --- a/gtkblueprinttool/ast_utils.py +++ b/gtkblueprinttool/ast_utils.py @@ -22,6 +22,7 @@ from collections import ChainMap, defaultdict from . import ast from .errors import * +from .lsp_utils import SemanticToken from .utils import lazy_prop from .xml_emitter import XmlEmitter @@ -112,6 +113,10 @@ class AstNode: return None + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: + for child in self.children: + yield from child.get_semantic_tokens() + def validate(token_name=None, end_token_name=None, skip_incomplete=False): """ Decorator for functions that validate an AST node. Exceptions raised diff --git a/gtkblueprinttool/errors.py b/gtkblueprinttool/errors.py index 16d01a6..599138e 100644 --- a/gtkblueprinttool/errors.py +++ b/gtkblueprinttool/errors.py @@ -78,7 +78,10 @@ class CompileError(PrintableError): def pretty_print(self, filename, code): line_num, col_num = utils.idx_to_pos(self.start + 1, code) - line = code.splitlines(True)[line_num-1] + line = code.splitlines(True)[line_num] + + # Display 1-based line numbers + line_num += 1 print(f"""{_colors.RED}{_colors.BOLD}{self.category}: {self.message}{_colors.CLEAR} at {filename} line {line_num} column {col_num}: diff --git a/gtkblueprinttool/lsp.py b/gtkblueprinttool/lsp.py index dca0c68..0c58f45 100644 --- a/gtkblueprinttool/lsp.py +++ b/gtkblueprinttool/lsp.py @@ -65,6 +65,26 @@ class OpenFile: self.diagnostics.append(e) + def calc_semantic_tokens(self) -> T.List[int]: + tokens = list(self.ast.get_semantic_tokens()) + token_lists = [ + [ + *utils.idx_to_pos(token.start, self.text), # line and column + token.end - token.start, # length + token.type, + 0, # token modifiers + ] for token in tokens] + + # convert line, column numbers to deltas + for i, token_list in enumerate(token_lists[1:]): + token_list[0] -= token_lists[i][0] + if token_list[0] == 0: + token_list[1] -= token_lists[i][1] + + # flatten the list + return [x for y in token_lists for x in y] + + class LanguageServer: commands: T.Dict[str, T.Callable] = {} @@ -137,6 +157,12 @@ class LanguageServer: "openClose": True, "change": TextDocumentSyncKind.Incremental, }, + "semanticTokensProvider": { + "legend": { + "tokenTypes": ["enumMember"], + }, + "full": True, + }, "completionProvider": {}, "hoverProvider": True, } @@ -191,6 +217,15 @@ class LanguageServer: self._send_response(id, [completion.to_json(True) for completion in completions]) + @command("textDocument/semanticTokens/full") + def semantic_tokens(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + + self._send_response(id, { + "data": open_file.calc_semantic_tokens(), + }) + + def _send_file_updates(self, open_file: OpenFile): self._send_notification("textDocument/publishDiagnostics", { "uri": open_file.uri, @@ -202,8 +237,8 @@ class LanguageServer: end_l, end_c = utils.idx_to_pos(err.end or err.start, text) return { "range": { - "start": { "line": start_l - 1, "character": start_c }, - "end": { "line": end_l - 1, "character": end_c }, + "start": { "line": start_l, "character": start_c }, + "end": { "line": end_l, "character": end_c }, }, "message": err.message, "severity": 1, diff --git a/gtkblueprinttool/lsp_utils.py b/gtkblueprinttool/lsp_utils.py index d2b83b9..bd686c1 100644 --- a/gtkblueprinttool/lsp_utils.py +++ b/gtkblueprinttool/lsp_utils.py @@ -94,3 +94,14 @@ class Completion: "insertTextFormat": insert_text_format, } return { k: v for k, v in result.items() if v is not None } + + +class SemanticTokenType(enum.IntEnum): + EnumMember = 0 + + +@dataclass +class SemanticToken: + start: int + end: int + type: SemanticTokenType diff --git a/gtkblueprinttool/utils.py b/gtkblueprinttool/utils.py index a4a9bbd..cf733c7 100644 --- a/gtkblueprinttool/utils.py +++ b/gtkblueprinttool/utils.py @@ -73,7 +73,7 @@ def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]: sp = text[:idx].splitlines(keepends=True) line_num = len(sp) col_num = len(sp[-1]) - return (line_num, col_num) + return (line_num - 1, col_num) def pos_to_idx(line: int, col: int, text: str) -> int: lines = text.splitlines(keepends=True)