lsp: Show docs on hover

Also:
- Install the script to <prefix>/bin
- Fix a bug in XmlReader that would cause "uninteresting" tags to not be
  fully ignored
- Other minor improvements
This commit is contained in:
James Westman 2021-10-24 17:10:05 -05:00
parent 8fc0efb642
commit 55a117a5b7
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
7 changed files with 85 additions and 31 deletions

View file

@ -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:

View file

@ -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. """

View file

@ -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):

View file

@ -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 <doc> 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)
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],
})

View file

@ -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

View file

@ -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,10 +74,13 @@ 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):
if not self.skipping:
self.stack[-1].cdata_chunks.append(content)

View file

@ -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()