lsp: Add reference documentation on hover

For most constructs and keywords, show the relevant section of the
reference documentation on hover.
This commit is contained in:
James Westman 2024-10-19 18:46:10 -05:00
parent b107a85947
commit e19975e1f8
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
28 changed files with 326 additions and 21 deletions

View file

@ -179,14 +179,16 @@ class AstNode:
token = self.group.tokens.get(attr.token_name)
if token and token.start <= idx < token.end:
return getattr(self, name)
else:
return getattr(self, name)
for child in self.children:
if idx in child.range:
if docs := child.get_docs(idx):
return docs
for name, attr in self._attrs_by_type(Docs):
if not attr.token_name:
return getattr(self, name)
return None
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:

View file

@ -207,6 +207,10 @@ class AdwBreakpointSetters(AstNode):
def unique(self):
self.validate_unique_in_parent("Duplicate setters block")
@docs("setters")
def ref_docs(self):
return get_docs_section("Syntax ExtAdwBreakpoint")
@decompiler("condition", cdata=True)
def decompile_condition(ctx: DecompileCtx, gir, cdata):

View file

@ -138,6 +138,10 @@ class ExtAdwResponseDialog(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate responses block")
@docs()
def ref_docs(self):
return get_docs_section("Syntax ExtAdwMessageDialog")
@completer(
applies_in=[ObjectContent],

View file

@ -58,6 +58,10 @@ class BindingFlag(AstNode):
"Only bindings with a single lookup can have flags",
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax Binding")
class Binding(AstNode):
grammar = [
@ -99,6 +103,10 @@ class Binding(AstNode):
actions=[CodeAction("use 'bind'", "bind")],
)
@docs("bind")
def ref_docs(self):
return get_docs_section("Syntax Binding")
@dataclass
class SimpleBinding:

View file

@ -55,6 +55,7 @@ from ..lsp_utils import (
SemanticToken,
SemanticTokenType,
SymbolKind,
get_docs_section,
)
from ..parse_tree import *

View file

@ -174,7 +174,7 @@ class LookupOp(InfixExpr):
class CastExpr(InfixExpr):
grammar = [
"as",
Keyword("as"),
AnyOf(
["<", TypeName, Match(">").expected()],
[
@ -220,6 +220,10 @@ class CastExpr(InfixExpr):
],
)
@docs("as")
def ref_docs(self):
return get_docs_section("Syntax CastExpression")
class ClosureArg(AstNode):
grammar = Expression
@ -269,6 +273,10 @@ class ClosureExpr(ExprBase):
if not self.tokens["extern"]:
raise CompileError(f"{self.closure_name} is not a builtin function")
@docs("name")
def ref_docs(self):
return get_docs_section("Syntax ClosureExpression")
expr.children = [
AnyOf(ClosureExpr, LiteralExpr, ["(", Expression, ")"]),

View file

@ -40,6 +40,10 @@ class SignalFlag(AstNode):
f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax Signal")
class Signal(AstNode):
grammar = Statement(
@ -50,7 +54,7 @@ class Signal(AstNode):
UseIdent("detail_name").expected("a signal detail name"),
]
),
"=>",
Keyword("=>"),
Mark("detail_start"),
Optional(["$", UseLiteral("extern", True)]),
UseIdent("handler").expected("the name of a function to handle the signal"),
@ -184,6 +188,10 @@ class Signal(AstNode):
if prop is not None:
return prop.doc
@docs("=>")
def ref_docs(self):
return get_docs_section("Syntax Signal")
@decompiler("signal")
def decompile_signal(

View file

@ -225,6 +225,10 @@ class ExtAccessibility(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate accessibility block")
@docs("accessibility")
def ref_docs(self):
return get_docs_section("Syntax ExtAccessibility")
@completer(
applies_in=[ObjectContent],

View file

@ -55,6 +55,10 @@ class Item(AstNode):
f"Duplicate item '{self.name}'", lambda x: x.name == self.name
)
@docs("name")
def ref_docs(self):
return get_docs_section("Syntax ExtComboBoxItems")
class ExtComboBoxItems(AstNode):
grammar = [
@ -81,6 +85,10 @@ class ExtComboBoxItems(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate items block")
@docs("items")
def ref_docs(self):
return get_docs_section("Syntax ExtComboBoxItems")
@completer(
applies_in=[ObjectContent],

View file

@ -29,25 +29,23 @@ class Filters(AstNode):
self.tokens["tag_name"],
SymbolKind.Array,
self.range,
self.group.tokens[self.tokens["tag_name"]].range,
self.group.tokens["tag_name"].range,
)
@validate()
def container_is_file_filter(self):
validate_parent_type(self, "Gtk", "FileFilter", "file filter properties")
@validate()
@validate("tag_name")
def unique_in_parent(self):
# The token argument to validate() needs to be calculated based on
# the instance, hence wrapping it like this.
@validate(self.tokens["tag_name"])
def wrapped_validator(self):
self.validate_unique_in_parent(
f"Duplicate {self.tokens['tag_name']} block",
check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"],
)
self.validate_unique_in_parent(
f"Duplicate {self.tokens['tag_name']} block",
check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"],
)
wrapped_validator(self)
@docs("tag_name")
def ref_docs(self):
return get_docs_section("Syntax ExtFileFilter")
class FilterString(AstNode):
@ -76,8 +74,7 @@ def create_node(tag_name: str, singular: str):
return Group(
Filters,
[
Keyword(tag_name),
UseLiteral("tag_name", tag_name),
UseExact("tag_name", tag_name),
"[",
Delimited(
Group(

View file

@ -83,6 +83,10 @@ class ExtLayout(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate layout block")
@docs("layout")
def ref_docs(self):
return get_docs_section("Syntax ExtLayout")
@completer(
applies_in=[ObjectContent],

View file

@ -108,3 +108,7 @@ class ExtListItemFactory(AstNode):
just hear to satisfy XmlOutput._emit_object_or_template
"""
return None
@docs("id")
def ref_docs(self):
return get_docs_section("Syntax ExtListItemFactory")

View file

@ -70,6 +70,25 @@ class Menu(AstNode):
if self.id in RESERVED_IDS:
raise CompileWarning(f"{self.id} may be a confusing object ID")
@docs("menu")
def ref_docs_menu(self):
return get_docs_section("Syntax Menu")
@docs("section")
def ref_docs_section(self):
return get_docs_section("Syntax Menu")
@docs("submenu")
def ref_docs_submenu(self):
return get_docs_section("Syntax Menu")
@docs("item")
def ref_docs_item(self):
if self.tokens["shorthand"]:
return get_docs_section("Syntax MenuItemShorthand")
else:
return get_docs_section("Syntax Menu")
class MenuAttribute(AstNode):
tag_name = "attribute"
@ -156,6 +175,7 @@ menu_item_shorthand = Group(
[
Keyword("item"),
UseLiteral("tag", "item"),
UseLiteral("shorthand", True),
"(",
Group(
MenuAttribute,

View file

@ -94,6 +94,10 @@ class ExtScaleMark(AstNode):
did_you_mean=(self.position, positions.keys()),
)
@docs("mark")
def ref_docs(self):
return get_docs_section("Syntax ExtScaleMarks")
class ExtScaleMarks(AstNode):
grammar = [
@ -123,6 +127,10 @@ class ExtScaleMarks(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate 'marks' block")
@docs("marks")
def ref_docs(self):
return get_docs_section("Syntax ExtScaleMarks")
@completer(
applies_in=[ObjectContent],

View file

@ -94,6 +94,10 @@ class ExtSizeGroupWidgets(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate widgets block")
@docs("widgets")
def ref_docs(self):
return get_docs_section("Syntax ExtSizeGroupWidgets")
@completer(
applies_in=[ObjectContent],

View file

@ -57,7 +57,7 @@ class ExtStringListStrings(AstNode):
self.group.tokens["strings"].range,
)
@validate("items")
@validate("strings")
def container_is_string_list(self):
validate_parent_type(self, "Gtk", "StringList", "StringList items")
@ -65,6 +65,10 @@ class ExtStringListStrings(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate strings block")
@docs("strings")
def ref_docs(self):
return get_docs_section("Syntax ExtStringListStrings")
@completer(
applies_in=[ObjectContent],

View file

@ -70,6 +70,10 @@ class ExtStyles(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate styles block")
@docs("styles")
def ref_docs(self):
return get_docs_section("Syntax ExtStyles")
@completer(
applies_in=[ObjectContent],

View file

@ -53,6 +53,10 @@ class ChildExtension(AstNode):
def child(self) -> ExtResponse:
return self.children[0]
@docs()
def ref_docs(self):
return get_docs_section("Syntax ChildExtension")
class ChildAnnotation(AstNode):
grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"]

View file

@ -88,6 +88,10 @@ class Template(Object):
f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",
)
@docs("id")
def ref_docs(self):
return get_docs_section("Syntax Template")
@decompiler("template")
def decompile_template(ctx: DecompileCtx, gir, klass, parent=None):

View file

@ -68,6 +68,10 @@ class GtkDirective(AstNode):
# For better error handling, just assume it's 4.0
return gir.get_namespace("Gtk", "4.0")
@docs()
def ref_docs(self):
return get_docs_section("Syntax GtkDecl")
class Import(AstNode):
grammar = Statement(
@ -105,3 +109,7 @@ class Import(AstNode):
return gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
except CompileError:
return None
@docs()
def ref_docs(self):
return get_docs_section("Syntax Using")

View file

@ -124,6 +124,16 @@ class ExtResponse(AstNode):
object = self.parent_by_type(Child).object
return object.id
@docs()
def ref_docs(self):
return get_docs_section("Syntax ExtResponse")
@docs("response_id")
def response_id_docs(self):
if enum := self.root.gir.get_type("ResponseType", "Gtk"):
if member := enum.members.get(self.response_id, None):
return member.doc
def decompile_response_type(parent_element, child_element):
obj_id = None

View file

@ -29,3 +29,7 @@ class TranslationDomain(AstNode):
@property
def domain(self):
return self.tokens["domain"]
@docs()
def ref_docs(self):
return get_docs_section("Syntax TranslationDomain")

View file

@ -20,6 +20,7 @@
import typing as T
from blueprintcompiler.gir import ArrayType
from blueprintcompiler.lsp_utils import SemanticToken
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
@ -56,6 +57,10 @@ class Translated(AstNode):
f"Cannot convert translated string to {expected_type.full_name}"
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax Translated")
class TypeLiteral(AstNode):
grammar = [
@ -101,6 +106,10 @@ class TypeLiteral(AstNode):
],
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax TypeLiteral")
class QuotedLiteral(AstNode):
grammar = UseQuoted("value")
@ -258,6 +267,10 @@ class Flags(AstNode):
if expected_type is not None and not isinstance(expected_type, gir.Bitfield):
raise CompileError(f"{expected_type.full_name} is not a bitfield type")
@docs()
def ref_docs(self):
return get_docs_section("Syntax Flags")
class IdentLiteral(AstNode):
grammar = UseIdent("value")

View file

@ -19,6 +19,8 @@
import enum
import json
import os
import typing as T
from dataclasses import dataclass, field
@ -200,3 +202,27 @@ class TextEdit:
def to_json(self):
return {"range": self.range.to_json(), "newText": self.newText}
_docs_sections: T.Optional[dict[str, T.Any]] = None
def get_docs_section(section_name: str) -> T.Optional[str]:
global _docs_sections
if _docs_sections is None:
try:
with open(
os.path.join(os.path.dirname(__file__), "reference_docs.json")
) as f:
_docs_sections = json.load(f)
except FileNotFoundError:
_docs_sections = {}
if section := _docs_sections.get(section_name):
content = section["content"]
link = section["link"]
content += f"\n\n---\n\n[Online documentation]({link})"
return content
else:
return None

136
docs/collect-sections.py Executable file
View file

@ -0,0 +1,136 @@
#!/usr/bin/env python3
import json
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
__all__ = ["get_docs_section"]
DOCS_ROOT = "https://jwestman.pages.gitlab.gnome.org/blueprint-compiler"
sections: dict[str, "Section"] = {}
@dataclass
class Section:
link: str
lines: str
def to_json(self):
return {
"content": rst_to_md(self.lines),
"link": self.link,
}
def load_reference_docs():
for filename in Path(os.path.dirname(__file__), "reference").glob("*.rst"):
with open(filename) as f:
section_name = None
lines = []
def close_section():
if section_name:
html_file = re.sub(r"\.rst$", ".html", filename.name)
anchor = re.sub(r"[^a-z0-9]+", "-", section_name.lower())
link = f"{DOCS_ROOT}/reference/{html_file}#{anchor}"
sections[section_name] = Section(link, lines)
for line in f:
if m := re.match(r"\.\.\s+_(.*):", line):
close_section()
section_name = m.group(1)
lines = []
else:
lines.append(line)
close_section()
# This isn't a comprehensive rST to markdown converter, it just needs to handle the
# small subset of rST used in the reference docs.
def rst_to_md(lines: list[str]) -> str:
result = ""
def rst_to_md_inline(line):
line = re.sub(r"``(.*?)``", r"`\1`", line)
line = re.sub(
r":ref:`(.*?)<(.*?)>`",
lambda m: f"[{m.group(1)}]({sections[m.group(2)].link})",
line,
)
line = re.sub(r"`([^`]*?) <([^`>]*?)>`_", r"[\1](\2)", line)
return line
i = 0
n = len(lines)
heading_levels = {}
def print_block(lang: str = "", code: bool = True, strip_links: bool = False):
nonlocal result, i
block = ""
while i < n:
line = lines[i].rstrip()
if line.startswith(" "):
line = line[3:]
elif line != "":
break
if strip_links:
line = re.sub(r":ref:`(.*?)<(.*?)>`", r"\1", line)
if not code:
line = rst_to_md_inline(line)
block += line + "\n"
i += 1
if code:
result += f"```{lang}\n{block.strip()}\n```\n\n"
else:
result += block
while i < n:
line = lines[i].rstrip()
i += 1
if line == ".. rst-class:: grammar-block":
print_block(strip_links=True)
elif line == ".. code-block:: blueprint":
print_block("blueprint")
elif line == ".. note::":
result += "#### Note\n"
print_block(code=False)
elif m := re.match(r"\.\. image:: (.*)", line):
result += f"![{m.group(1)}]({DOCS_ROOT}/_images/{m.group(1)})\n"
elif i < n and re.match(r"^((-+)|(~+)|(\++))$", lines[i]):
level_char = lines[i][0]
if level_char not in heading_levels:
heading_levels[level_char] = max(heading_levels.values(), default=1) + 1
result += (
"#" * heading_levels[level_char] + " " + rst_to_md_inline(line) + "\n"
)
i += 1
else:
result += rst_to_md_inline(line) + "\n"
return result
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: collect_sections.py <output_file>")
sys.exit(1)
outfile = sys.argv[1]
load_reference_docs()
# print the sections to a json file
with open(outfile, "w") as f:
json.dump(
{name: section.to_json() for name, section in sections.items()}, f, indent=2
)

View file

@ -9,3 +9,11 @@ custom_target('docs',
)
endif
custom_target('reference_docs.json',
output: 'reference_docs.json',
command: [meson.current_source_dir() / 'collect-sections.py', '@OUTPUT@'],
build_always_stale: true,
install: true,
install_dir: py.get_install_dir() / 'blueprintcompiler',
)

View file

@ -227,7 +227,7 @@ Valid in `Gtk.BuilderListItemFactory <https://docs.gtk.org/gtk4/class.BuilderLis
The ``template`` block defines the template that will be used to create list items. This block is unique within Blueprint because it defines a completely separate sub-blueprint which is used to create each list item. The sub-blueprint may not reference objects in the main blueprint or vice versa.
The template type must be `Gtk.ListItem <https://docs.gtk.org/gtk4/class.ListItem.html>`_, `Gtk.ColumnViewRow <https://docs.gtk.org/gtk4/class.ColumnViewRow.html>`_, or `Gtk.ColumnViewCell <https://docs.gtk.org/gtk4/class.ColumnViewCell.html>`_ The template object can be referenced with the ``template`` keyword.
The template type must be `Gtk.ListItem <https://docs.gtk.org/gtk4/class.ListItem.html>`_, `Gtk.ColumnViewRow <https://docs.gtk.org/gtk4/class.ColumnViewRow.html>`_, or `Gtk.ColumnViewCell <https://docs.gtk.org/gtk4/class.ColumnViewCell.html>`_. The template object can be referenced with the ``template`` keyword.
.. code-block:: blueprint

View file

@ -2,13 +2,13 @@ project('blueprint-compiler',
version: '0.14.0',
)
subdir('docs')
prefix = get_option('prefix')
datadir = join_paths(prefix, get_option('datadir'))
py = import('python').find_installation('python3')
subdir('docs')
configure_file(
input: 'blueprint-compiler.pc.in',
output: 'blueprint-compiler.pc',