From 55a117a5b77968fe393fff2d59f9d5a6bc1a34f7 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 24 Oct 2021 17:10:05 -0500 Subject: [PATCH] lsp: Show docs on hover Also: - Install the script to /bin - Fix a bug in XmlReader that would cause "uninteresting" tags to not be fully ignored - Other minor improvements --- gtkblueprinttool/ast.py | 29 +++++++++++++++++++---- gtkblueprinttool/ast_utils.py | 5 ++++ gtkblueprinttool/gir.py | 6 ++--- gtkblueprinttool/lsp.py | 42 ++++++++++++++++++++++++++-------- gtkblueprinttool/lsp_utils.py | 15 ++++-------- gtkblueprinttool/xml_reader.py | 13 ++++++++--- meson.build | 6 +++++ 7 files changed, 85 insertions(+), 31 deletions(-) diff --git a/gtkblueprinttool/ast.py b/gtkblueprinttool/ast.py index 71f6116..429d414 100644 --- a/gtkblueprinttool/ast.py +++ b/gtkblueprinttool/ast.py @@ -77,7 +77,7 @@ class AstNode: 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: + if token and token.start <= idx < token.end: return getattr(self, name) else: return getattr(self, name) @@ -180,6 +180,15 @@ class Template(AstNode): return self.root.gir.get_class(self.parent_class, self.parent_namespace) + @docs("namespace") + def namespace_docs(self): + return self.root.gir.namespaces[self.namespace].doc + + @docs("class_name") + def class_docs(self): + return self.gir_class.doc + + def emit_xml(self, xml: XmlEmitter): xml.start_tag("template", **{ "class": self.name, @@ -209,7 +218,7 @@ class Object(AstNode): def namespace_docs(self): return self.root.gir.namespaces[self.namespace].doc - @docs("namespace") + @docs("class_name") def class_docs(self): return self.gir_class.doc @@ -293,7 +302,6 @@ class Property(AstNode): else: raise CompilerBugError() - @validate("name") def property_exists(self): if self.gir_class is None: @@ -313,6 +321,12 @@ class Property(AstNode): ) + @docs("name") + def property_docs(self): + if self.gir_property is not None: + return self.gir_property.doc + + def emit_xml(self, xml: XmlEmitter): props = { "name": self.name, @@ -360,7 +374,6 @@ class Signal(AstNode): else: raise CompilerBugError() - @validate("name") def signal_exists(self): if self.gir_class is None: @@ -374,12 +387,18 @@ class Signal(AstNode): return if self.gir_signal is None: - print(self.gir_class.signals.keys()) raise CompileError( f"Class {self.gir_class.full_name} does not contain a signal called {self.name}", did_you_mean=(self.name, self.gir_class.signals.keys()) ) + + @docs("name") + def signal_docs(self): + if self.gir_signal is not None: + return self.gir_signal.doc + + def emit_xml(self, xml: XmlEmitter): name = self.name if self.detail_name: diff --git a/gtkblueprinttool/ast_utils.py b/gtkblueprinttool/ast_utils.py index b3a5057..c240d00 100644 --- a/gtkblueprinttool/ast_utils.py +++ b/gtkblueprinttool/ast_utils.py @@ -80,6 +80,11 @@ class Docs: self.func = func self.token_name = token_name + def __get__(self, instance, owner): + if instance is None: + return self + return self.func(instance) + def docs(*args, **kwargs): """ Decorator for functions that return documentation for tokens. """ diff --git a/gtkblueprinttool/gir.py b/gtkblueprinttool/gir.py index b215387..1c493d8 100644 --- a/gtkblueprinttool/gir.py +++ b/gtkblueprinttool/gir.py @@ -72,10 +72,10 @@ class GirNode: @lazy_prop def doc(self) -> str: - el = self.xml.find("doc") - if el is None: + el = self.xml.get_elements("doc") + if len(el) != 1: return None - return el.cdata + return el[0].cdata.strip() class Property(GirNode): diff --git a/gtkblueprinttool/lsp.py b/gtkblueprinttool/lsp.py index f19d5dd..1ac2756 100644 --- a/gtkblueprinttool/lsp.py +++ b/gtkblueprinttool/lsp.py @@ -22,7 +22,7 @@ import json, sys, traceback from .errors import PrintableError, CompileError, MultipleErrors from .lsp_utils import * -from . import tokenizer, parser, utils +from . import tokenizer, parser, utils, xml_reader def command(json_method): @@ -40,6 +40,10 @@ class LanguageServer: self._open_files: {str: OpenFile} = {} def run(self): + # Read tags from gir files. During normal compilation these are + # ignored. + xml_reader.PARSE_GIR.add("doc") + try: while True: line = "" @@ -72,7 +76,10 @@ class LanguageServer: sys.stdout.flush() def _log(self, msg): - pass + if self.logfile is not None: + self.logfile.write(str(msg)) + self.logfile.write("\n") + self.logfile.flush() def _send_response(self, id, result): self._send({ @@ -94,8 +101,9 @@ class LanguageServer: "capabilities": { "textDocumentSync": { "openClose": True, - "change": TextDocumentSyncKind.Incremental, - } + "change": 2, # incremental + }, + "hoverProvider": True, } }) @@ -112,19 +120,33 @@ class LanguageServer: @command("textDocument/didChange") def didChange(self, id, params): - open_file = self._open_files[params.textDocument.uri] - - open_file.apply_changes(params.contentChanges) - self._send_file_updates(open_file) + if params is not None: + open_file = self._open_files[params["textDocument"]["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] + del self._open_files[params["textDocument"]["uri"]] + + @command("textDocument/hover") + def hover(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + docs = open_file.ast.get_docs(utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text)) + if docs is not None: + self._send_response(id, { + "contents": { + "kind": "markdown", + "value": docs, + } + }) + else: + self._send_response(id, None) def _send_file_updates(self, open_file: OpenFile): self._send_notification("textDocument/publishDiagnostics", { - "uri": uri, + "uri": open_file.uri, "diagnostics": [self._create_diagnostic(open_file.text, err) for err in open_file.diagnostics], }) diff --git a/gtkblueprinttool/lsp_utils.py b/gtkblueprinttool/lsp_utils.py index afc8bc6..53ea2a1 100644 --- a/gtkblueprinttool/lsp_utils.py +++ b/gtkblueprinttool/lsp_utils.py @@ -20,22 +20,17 @@ from enum import Enum +from . import tokenizer, parser +from .errors import * 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): @@ -50,8 +45,8 @@ class OpenFile: 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)] + self.diagnostics += self.ast.errors except MultipleErrors as e: - self.diagnostics += [self._create_diagnostic(text, err) for err in e.errors] + self.diagnostics += e.errors except CompileError as e: - self.diagnostics += [self._create_diagnostic(text, e)] + self.diagnostics += e diff --git a/gtkblueprinttool/xml_reader.py b/gtkblueprinttool/xml_reader.py index 94061fe..94c49d4 100644 --- a/gtkblueprinttool/xml_reader.py +++ b/gtkblueprinttool/xml_reader.py @@ -24,9 +24,10 @@ from xml import sax from .utils import lazy_prop +# To speed up parsing, we ignore all tags except these PARSE_GIR = set([ "repository", "namespace", "class", "interface", "property", "glib:signal", - "include", "implements", + "include", "implements" ]) @@ -52,10 +53,13 @@ class Handler(sax.handler.ContentHandler): def __init__(self, parse_type): self.root = None self.stack = [] + self.skipping = 0 self._interesting_elements = parse_type def startElement(self, name, attrs): if name not in self._interesting_elements: + self.skipping += 1 + if self.skipping > 0: return element = Element(name, attrs.copy()) @@ -70,11 +74,14 @@ class Handler(sax.handler.ContentHandler): def endElement(self, name): - if name in self._interesting_elements: + if self.skipping == 0: self.stack.pop() + if name not in self._interesting_elements: + self.skipping -= 1 def characters(self, content): - self.stack[-1].cdata_chunks.append(content) + if not self.skipping: + self.stack[-1].cdata_chunks.append(content) def parse(filename, parse_type): diff --git a/meson.build b/meson.build index fba217e..9aa0ee5 100644 --- a/meson.build +++ b/meson.build @@ -15,6 +15,12 @@ configure_file( install_dir: join_paths(libdir, 'pkgconfig'), ) +install_data( + 'gtk-blueprint-tool.py', + install_dir: get_option('bindir'), + rename: 'gtk-blueprint-tool', +) + meson.override_find_program('gtk-blueprint-tool', find_program('gtk-blueprint-tool.py')) if not meson.is_subproject()