From 8fc0efb642aab847ecac95b28fcfe249416c892f Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 24 Oct 2021 14:36:54 -0500 Subject: [PATCH] lsp: Code cleanup --- gtkblueprinttool/ast.py | 104 +++++++++++++--------------------- gtkblueprinttool/ast_utils.py | 90 +++++++++++++++++++++++++++++ gtkblueprinttool/lsp.py | 37 ++++-------- gtkblueprinttool/lsp_enums.py | 25 -------- gtkblueprinttool/lsp_utils.py | 57 +++++++++++++++++++ 5 files changed, 197 insertions(+), 116 deletions(-) create mode 100644 gtkblueprinttool/ast_utils.py delete mode 100644 gtkblueprinttool/lsp_enums.py create mode 100644 gtkblueprinttool/lsp_utils.py diff --git a/gtkblueprinttool/ast.py b/gtkblueprinttool/ast.py index 3d9c63e..71f6116 100644 --- a/gtkblueprinttool/ast.py +++ b/gtkblueprinttool/ast.py @@ -17,69 +17,15 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import typing as T +from .ast_utils import * from .errors import assert_true, AlreadyCaughtError, CompileError, CompilerBugError, MultipleErrors from .gir import GirContext, get_namespace from .utils import lazy_prop from .xml_emitter import XmlEmitter -class Validator(): - def __init__(self, func, token_name=None, end_token_name=None): - self.func = func - self.token_name = token_name - self.end_token_name = end_token_name - - def __get__(self, instance, owner): - if instance is None: - return self - - key = "_validation_result_" + self.func.__name__ - - if key + "_err" in instance.__dict__: - # If the validator has failed before, raise a generic Exception. - # We want anything that depends on this validation result to - # fail, but not report the exception twice. - raise AlreadyCaughtError() - - if key not in instance.__dict__: - try: - instance.__dict__[key] = self.func(instance) - except CompileError as e: - # Mark the validator as already failed so we don't print the - # same message again - instance.__dict__[key + "_err"] = True - - # This mess of code sets the error's start and end positions - # from the tokens passed to the decorator, if they have not - # already been set - if self.token_name is not None and e.start is None: - group = instance.group.tokens.get(self.token_name) - if self.end_token_name is not None and group is None: - group = instance.group.tokens[self.end_token_name] - e.start = group.start - if (self.token_name is not None or self.end_token_name is not None) and e.end is None: - e.end = instance.group.tokens[self.end_token_name or self.token_name].end - - # Re-raise the exception - raise e - - # Return the validation result (which other validators, or the code - # generation phase, might depend on) - return instance.__dict__[key] - - -def validate(*args, **kwargs): - """ Decorator for functions that validate an AST node. Exceptions raised - during validation are marked with range information from the tokens. Also - creates a cached property out of the function. """ - - def decorator(func): - return Validator(func, *args, **kwargs) - - return decorator - - class AstNode: """ Base class for nodes in the abstract syntax tree. """ @@ -100,19 +46,22 @@ class AstNode: return list(self._get_errors()) def _get_errors(self): - for name in dir(type(self)): - item = getattr(type(self), name) - if isinstance(item, Validator): - try: - getattr(self, name) - except AlreadyCaughtError: - pass - except CompileError as e: - yield e + for name, attr in self._attrs_by_type(Validator): + try: + getattr(self, name) + except AlreadyCaughtError: + pass + except CompileError as e: + yield e for child in self.child_nodes: yield from child._get_errors() + def _attrs_by_type(self, attr_type): + for name in dir(type(self)): + item = getattr(type(self), name) + if isinstance(item, attr_type): + yield name, item def generate(self) -> str: """ Generates an XML string from the node. """ @@ -124,6 +73,21 @@ class AstNode: """ Emits the XML representation of this AST node to the XmlEmitter. """ raise NotImplementedError() + def get_docs(self, idx: int) -> T.Optional[str]: + for name, attr in self._attrs_by_type(Docs): + if attr.token_name: + token = self.group.tokens.get(attr.token_name) + if token.start <= idx < token.end: + return getattr(self, name) + else: + return getattr(self, name) + + for child in self.child_nodes: + if child.group.start <= idx < child.group.end: + docs = child.get_docs(idx) + if docs is not None: + return docs + class UI(AstNode): """ The AST node for the entire file """ @@ -240,6 +204,16 @@ class Object(AstNode): def gir_class(self): return self.root.gir.get_class(self.class_name, self.namespace) + + @docs("namespace") + def namespace_docs(self): + return self.root.gir.namespaces[self.namespace].doc + + @docs("namespace") + def class_docs(self): + return self.gir_class.doc + + def emit_xml(self, xml: XmlEmitter): xml.start_tag("object", **{ "class": self.gir_class.glib_type_name, diff --git a/gtkblueprinttool/ast_utils.py b/gtkblueprinttool/ast_utils.py new file mode 100644 index 0000000..b3a5057 --- /dev/null +++ b/gtkblueprinttool/ast_utils.py @@ -0,0 +1,90 @@ +# ast_utils.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +from .errors import * + +class Validator: + def __init__(self, func, token_name=None, end_token_name=None): + self.func = func + self.token_name = token_name + self.end_token_name = end_token_name + + def __get__(self, instance, owner): + if instance is None: + return self + + key = "_validation_result_" + self.func.__name__ + + if key + "_err" in instance.__dict__: + # If the validator has failed before, raise a generic Exception. + # We want anything that depends on this validation result to + # fail, but not report the exception twice. + raise AlreadyCaughtError() + + if key not in instance.__dict__: + try: + instance.__dict__[key] = self.func(instance) + except CompileError as e: + # Mark the validator as already failed so we don't print the + # same message again + instance.__dict__[key + "_err"] = True + + # This mess of code sets the error's start and end positions + # from the tokens passed to the decorator, if they have not + # already been set + if self.token_name is not None and e.start is None: + group = instance.group.tokens.get(self.token_name) + if self.end_token_name is not None and group is None: + group = instance.group.tokens[self.end_token_name] + e.start = group.start + if (self.token_name is not None or self.end_token_name is not None) and e.end is None: + e.end = instance.group.tokens[self.end_token_name or self.token_name].end + + # Re-raise the exception + raise e + + # Return the validation result (which other validators, or the code + # generation phase, might depend on) + return instance.__dict__[key] + + +def validate(*args, **kwargs): + """ Decorator for functions that validate an AST node. Exceptions raised + during validation are marked with range information from the tokens. Also + creates a cached property out of the function. """ + + def decorator(func): + return Validator(func, *args, **kwargs) + + return decorator + + +class Docs: + def __init__(self, func, token_name=None): + self.func = func + self.token_name = token_name + + +def docs(*args, **kwargs): + """ Decorator for functions that return documentation for tokens. """ + + def decorator(func): + return Docs(func, *args, **kwargs) + + return decorator diff --git a/gtkblueprinttool/lsp.py b/gtkblueprinttool/lsp.py index ccf154b..f19d5dd 100644 --- a/gtkblueprinttool/lsp.py +++ b/gtkblueprinttool/lsp.py @@ -21,7 +21,7 @@ import json, sys, traceback from .errors import PrintableError, CompileError, MultipleErrors -from .lsp_enums import * +from .lsp_utils import * from . import tokenizer, parser, utils @@ -37,7 +37,7 @@ class LanguageServer: def __init__(self): self.client_capabilities = {} - self._open_files = {} + self._open_files: {str: OpenFile} = {} def run(self): try: @@ -94,7 +94,7 @@ class LanguageServer: "capabilities": { "textDocumentSync": { "openClose": True, - "change": 1 + "change": TextDocumentSyncKind.Incremental, } } }) @@ -106,41 +106,26 @@ class LanguageServer: version = doc.get("version") text = doc.get("text") - self._open_files[uri] = text - self._send_diagnostics(uri) + open_file = OpenFile(uri, text, version) + self._open_files[uri] = open_file + self._send_file_updates(open_file) @command("textDocument/didChange") def didChange(self, id, params): - text = self._open_files[params.textDocument.uri] + open_file = self._open_files[params.textDocument.uri] - for change in params.contentChanges: - start = utils.pos_to_idx(change.range.start.line, change.range.start.character, text) - end = utils.pos_to_idx(change.range.end.line, change.range.end.character, text) - text = text[:start] + change.text + text[end:] - - self._open_files[params.textDocument.uri] = text - self._send_diagnostics(uri) + open_file.apply_changes(params.contentChanges) + self._send_file_updates(open_file) @command("textDocument/didClose") def didClose(self, id, params): del self._open_files[params.textDocument.uri] - def _send_diagnostics(self, uri): - text = self._open_files[uri] - - diagnostics = [] - try: - tokens = tokenizer.tokenize(text) - ast = parser.parse(tokens) - diagnostics = [self._create_diagnostic(text, err) for err in list(ast.errors)] - except MultipleErrors as e: - diagnostics += [self._create_diagnostic(text, err) for err in e.errors] - except CompileError as e: - diagnostics += [self._create_diagnostic(text, e)] + def _send_file_updates(self, open_file: OpenFile): self._send_notification("textDocument/publishDiagnostics", { "uri": uri, - "diagnostics": diagnostics, + "diagnostics": [self._create_diagnostic(open_file.text, err) for err in open_file.diagnostics], }) def _create_diagnostic(self, text, err): diff --git a/gtkblueprinttool/lsp_enums.py b/gtkblueprinttool/lsp_enums.py deleted file mode 100644 index 054543b..0000000 --- a/gtkblueprinttool/lsp_enums.py +++ /dev/null @@ -1,25 +0,0 @@ -# lsp_enums.py -# -# Copyright 2021 James Westman -# -# This file is free software; you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as -# published by the Free Software Foundation; either version 3 of the -# License, or (at your option) any later version. -# -# This file is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program. If not, see . -# -# SPDX-License-Identifier: LGPL-3.0-or-later - -from enum import Enum - -class TextDocumentSyncKind(Enum): - None_ = 0, - Full = 1, - Incremental = 2, diff --git a/gtkblueprinttool/lsp_utils.py b/gtkblueprinttool/lsp_utils.py new file mode 100644 index 0000000..afc8bc6 --- /dev/null +++ b/gtkblueprinttool/lsp_utils.py @@ -0,0 +1,57 @@ +# lsp_enums.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from enum import Enum + +from .utils import * + + +class TextDocumentSyncKind(Enum): + None_ = 0, + Full = 1, + Incremental = 2, + + +class OpenFile: + def __init__(self, uri, text, version): + self.uri = uri + self.text = text + self.version = version + + self.diagnostics = [] + self._update() + + def apply_changes(self, changes): + for change in changes: + start = utils.pos_to_idx(change.range.start.line, change.range.start.character, self.text) + end = utils.pos_to_idx(change.range.end.line, change.range.end.character, self.text) + self.text = self.text[:start] + change.text + self.text[end:] + self._update() + + def _update(self): + self.diagnostics = [] + try: + self.tokens = tokenizer.tokenize(self.text) + self.ast = parser.parse(self.tokens) + self.diagnostics = [self._create_diagnostic(text, err) for err in list(ast.errors)] + except MultipleErrors as e: + self.diagnostics += [self._create_diagnostic(text, err) for err in e.errors] + except CompileError as e: + self.diagnostics += [self._create_diagnostic(text, e)]