From ee614e0cc0882175c8bd170d037a16c2232c94f6 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 21 Jul 2023 15:08:16 -0500 Subject: [PATCH 001/138] Post-release version bump --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 0a0979c..0edd6d8 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('blueprint-compiler', - version: '0.10.0', + version: '0.10.1', ) subdir('docs') From 8fab7c1706d5fb4ef6a45bc82f9cd41e579c1191 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 21 Jul 2023 15:11:24 -0500 Subject: [PATCH 002/138] A couple of fixes to NEWS --- NEWS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index efdc77c..706f4f0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,12 +2,12 @@ ## Added -- The hover documentation now includes a link to the documentation for the symbol, if available. +- The hover documentation now includes a link to the online documentation for the symbol, if available. - Added hover documentation for the Adw.Breakpoint extensions, `condition` and `setters`. ## Changed -- Decompiling an empty file now produces an empty file rather than an error. +- Decompiling an empty file now produces an empty file rather than an error. (AkshayWarrier) - More relevant documentation is shown when hovering over an identifier literal (such as an enum value or an object ID). ## Fixed From 94db929f742929e25aa394fe03fd30c2447513c4 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 23 Jul 2023 18:04:10 -0500 Subject: [PATCH 003/138] Emit deprecation warnings --- blueprintcompiler/gir.py | 39 +++++++++++++++++++ .../language/gobject_property.py | 15 ++++++- blueprintcompiler/language/gobject_signal.py | 15 ++++++- blueprintcompiler/language/types.py | 11 ++++++ blueprintcompiler/typelib.py | 4 ++ tests/sample_errors/deprecations.blp | 10 +++++ tests/sample_errors/deprecations.err | 1 + tests/sample_errors/legacy_template.err | 1 + tests/test_samples.py | 8 ++++ 9 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 tests/sample_errors/deprecations.blp create mode 100644 tests/sample_errors/deprecations.err diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 6dab652..3642bd6 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -136,6 +136,14 @@ class GirType: def incomplete(self) -> bool: return False + @property + def deprecated(self) -> bool: + return False + + @property + def deprecated_doc(self) -> T.Optional[str]: + return None + class ExternType(GirType): def __init__(self, name: str) -> None: @@ -330,6 +338,13 @@ class GirNode: def type(self) -> GirType: raise NotImplementedError() + @property + def deprecated_doc(self) -> T.Optional[str]: + try: + return self.xml.get_elements("doc-deprecated")[0].cdata.strip() + except: + return None + class Property(GirNode): xml_tag = "property" @@ -365,6 +380,10 @@ class Property(GirNode): else: return None + @property + def deprecated(self) -> bool: + return self.tl.PROP_DEPRECATED == 1 + class Argument(GirNode): def __init__(self, container: GirNode, tl: typelib.Typelib) -> None: @@ -427,6 +446,10 @@ class Signal(GirNode): else: return None + @property + def deprecated(self) -> bool: + return self.tl.SIGNAL_DEPRECATED == 1 + class Interface(GirNode, GirType): xml_tag = "interface" @@ -488,6 +511,10 @@ class Interface(GirNode, GirType): else: return None + @property + def deprecated(self) -> bool: + return self.tl.INTERFACE_DEPRECATED == 1 + class Class(GirNode, GirType): xml_tag = "class" @@ -609,6 +636,10 @@ class Class(GirNode, GirType): else: return None + @property + def deprecated(self) -> bool: + return self.tl.OBJ_DEPRECATED == 1 + class TemplateType(GirType): def __init__(self, name: str, parent: T.Optional[GirType]): @@ -722,6 +753,10 @@ class Enumeration(GirNode, GirType): else: return None + @property + def deprecated(self) -> bool: + return self.tl.ENUM_DEPRECATED == 1 + class Boxed(GirNode, GirType): xml_tag = "glib:boxed" @@ -743,6 +778,10 @@ class Boxed(GirNode, GirType): else: return None + @property + def deprecated(self) -> bool: + return self.tl.STRUCT_DEPRECATED == 1 + class Bitfield(Enumeration): xml_tag = "bitfield" diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index b24cd07..526b308 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -41,9 +41,11 @@ class Property(AstNode): return self.parent.parent.gir_class @property - def gir_property(self): + def gir_property(self) -> T.Optional[gir.Property]: if self.gir_class is not None and not isinstance(self.gir_class, ExternType): return self.gir_class.properties.get(self.tokens["name"]) + else: + return None @validate() def binding_valid(self): @@ -91,6 +93,17 @@ class Property(AstNode): check=lambda child: child.tokens["name"] == self.tokens["name"], ) + @validate("name") + def deprecated(self) -> None: + if self.gir_property is not None and self.gir_property.deprecated: + hints = [] + if self.gir_property.deprecated_doc: + hints.append(self.gir_property.deprecated_doc) + raise CompileWarning( + f"{self.gir_property.signature} is deprecated", + hints=hints, + ) + @docs("name") def property_docs(self): if self.gir_property is not None: diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 2fc4699..1c60bbf 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -95,9 +95,11 @@ class Signal(AstNode): return any(x.flag == "after" for x in self.flags) @property - def gir_signal(self): + def gir_signal(self) -> T.Optional[gir.Signal]: if self.gir_class is not None and not isinstance(self.gir_class, ExternType): return self.gir_class.signals.get(self.tokens["name"]) + else: + return None @property def gir_class(self): @@ -134,6 +136,17 @@ class Signal(AstNode): if self.context[ScopeCtx].objects.get(object_id) is None: raise CompileError(f"Could not find object with ID '{object_id}'") + @validate("name") + def deprecated(self) -> None: + if self.gir_signal is not None and self.gir_signal.deprecated: + hints = [] + if self.gir_signal.deprecated_doc: + hints.append(self.gir_signal.deprecated_doc) + raise CompileWarning( + f"{self.gir_signal.signature} is deprecated", + hints=hints, + ) + @docs("name") def signal_docs(self): if self.gir_signal is not None: diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index e75ebdc..1d9911b 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -57,6 +57,17 @@ class TypeName(AstNode): if not self.tokens["extern"]: self.root.gir.validate_ns(self.tokens["namespace"]) + @validate() + def deprecated(self) -> None: + if self.gir_type and self.gir_type.deprecated: + hints = [] + if self.gir_type.deprecated_doc: + hints.append(self.gir_type.deprecated_doc) + raise CompileWarning( + f"{self.gir_type.full_name} is deprecated", + hints=hints, + ) + @property def gir_ns(self): if not self.tokens["extern"]: diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py index 2ce3e32..c8a3ff3 100644 --- a/blueprintcompiler/typelib.py +++ b/blueprintcompiler/typelib.py @@ -150,11 +150,15 @@ class Typelib: BLOB_NAME = Field(0x4, "string") + STRUCT_DEPRECATED = Field(0x2, "u16", 0, 1) + + ENUM_DEPRECATED = Field(0x2, "u16", 0, 1) ENUM_GTYPE_NAME = Field(0x8, "string") ENUM_N_VALUES = Field(0x10, "u16") ENUM_N_METHODS = Field(0x12, "u16") ENUM_VALUES = Field(0x18, "offset") + INTERFACE_DEPRECATED = Field(0x2, "u16", 0, 1) INTERFACE_GTYPE_NAME = Field(0x8, "string") INTERFACE_N_PREREQUISITES = Field(0x12, "u16") INTERFACE_N_PROPERTIES = Field(0x14, "u16") diff --git a/tests/sample_errors/deprecations.blp b/tests/sample_errors/deprecations.blp new file mode 100644 index 0000000..1554a0b --- /dev/null +++ b/tests/sample_errors/deprecations.blp @@ -0,0 +1,10 @@ +using Gtk 4.0; +using Gio 2.0; + +Dialog { + use-header-bar: 1; +} + +Window { + keys-changed => $on_window_keys_changed(); +} diff --git a/tests/sample_errors/deprecations.err b/tests/sample_errors/deprecations.err new file mode 100644 index 0000000..6059412 --- /dev/null +++ b/tests/sample_errors/deprecations.err @@ -0,0 +1 @@ +4,1,6,Gtk.Dialog is deprecated \ No newline at end of file diff --git a/tests/sample_errors/legacy_template.err b/tests/sample_errors/legacy_template.err index 876b0cf..79ad519 100644 --- a/tests/sample_errors/legacy_template.err +++ b/tests/sample_errors/legacy_template.err @@ -1,2 +1,3 @@ 3,10,12,Use type syntax here (introduced in blueprint 0.8.0) +8,1,6,Gtk.Dialog is deprecated 9,18,12,Use 'template' instead of the class name (introduced in 0.8.0) \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index 7fe7941..1273805 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -54,6 +54,14 @@ class TestSamples(unittest.TestCase): tokens = tokenizer.tokenize(blueprint) ast, errors, warnings = parser.parse(tokens) + # Ignore deprecation warnings because some of the things we're testing + # are deprecated + warnings = [ + warning + for warning in warnings + if "is deprecated" not in warning.message + ] + if errors: raise errors if len(warnings): From 950b141d2621ef45df00a7ec5a12734ad89d32e5 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 23 Jul 2023 18:04:10 -0500 Subject: [PATCH 004/138] lsp: Mark deprecation warnings Some editors use different styling (e.g. strikethrough) for deprecation warnings. --- blueprintcompiler/errors.py | 4 ++++ blueprintcompiler/language/common.py | 1 + blueprintcompiler/language/gobject_property.py | 2 +- blueprintcompiler/language/gobject_signal.py | 2 +- blueprintcompiler/language/types.py | 2 +- blueprintcompiler/lsp.py | 3 +++ blueprintcompiler/lsp_utils.py | 5 +++++ tests/test_samples.py | 9 +++++++-- 8 files changed, 23 insertions(+), 5 deletions(-) diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index 2a14040..e89ec31 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -128,6 +128,10 @@ class CompileWarning(CompileError): color = Colors.YELLOW +class DeprecatedWarning(CompileWarning): + pass + + class UpgradeWarning(CompileWarning): category = "upgrade" color = Colors.PURPLE diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index f1c6bb9..e45cddc 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -33,6 +33,7 @@ from ..errors import ( CodeAction, CompileError, CompileWarning, + DeprecatedWarning, MultipleErrors, UpgradeWarning, ) diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 526b308..68f8c6d 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -99,7 +99,7 @@ class Property(AstNode): hints = [] if self.gir_property.deprecated_doc: hints.append(self.gir_property.deprecated_doc) - raise CompileWarning( + raise DeprecatedWarning( f"{self.gir_property.signature} is deprecated", hints=hints, ) diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 1c60bbf..721c443 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -142,7 +142,7 @@ class Signal(AstNode): hints = [] if self.gir_signal.deprecated_doc: hints.append(self.gir_signal.deprecated_doc) - raise CompileWarning( + raise DeprecatedWarning( f"{self.gir_signal.signature} is deprecated", hints=hints, ) diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index 1d9911b..e7b1867 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -63,7 +63,7 @@ class TypeName(AstNode): hints = [] if self.gir_type.deprecated_doc: hints.append(self.gir_type.deprecated_doc) - raise CompileWarning( + raise DeprecatedWarning( f"{self.gir_type.full_name} is deprecated", hints=hints, ) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index e8c37f3..54a36c8 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -391,6 +391,9 @@ class LanguageServer: else DiagnosticSeverity.Error, } + if isinstance(err, DeprecationWarning): + result["tags"] = [DiagnosticTag.Deprecated] + if len(err.references) > 0: result["relatedInformation"] = [ { diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index 7f46680..2d1072e 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -119,6 +119,11 @@ class DiagnosticSeverity(enum.IntEnum): Hint = 4 +class DiagnosticTag(enum.IntEnum): + Unnecessary = 1 + Deprecated = 2 + + @dataclass class SemanticToken: start: int diff --git a/tests/test_samples.py b/tests/test_samples.py index 1273805..bacca80 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -29,7 +29,12 @@ from gi.repository import Gtk from blueprintcompiler import decompiler, parser, tokenizer, utils from blueprintcompiler.completions import complete -from blueprintcompiler.errors import CompileError, MultipleErrors, PrintableError +from blueprintcompiler.errors import ( + CompileError, + DeprecatedWarning, + MultipleErrors, + PrintableError, +) from blueprintcompiler.outputs.xml import XmlOutput from blueprintcompiler.tokenizer import Token, TokenType, tokenize @@ -59,7 +64,7 @@ class TestSamples(unittest.TestCase): warnings = [ warning for warning in warnings - if "is deprecated" not in warning.message + if not isinstance(warning, DeprecatedWarning) ] if errors: From e087aeb44f44072435a2c998b7341de28d9321fe Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 23 Jul 2023 21:11:00 -0500 Subject: [PATCH 005/138] lsp: Add document outline --- blueprintcompiler/ast_utils.py | 37 +++++++++++++++- blueprintcompiler/language/adw_breakpoint.py | 35 +++++++++++++++- .../language/adw_message_dialog.py | 19 +++++++++ blueprintcompiler/language/common.py | 9 +++- blueprintcompiler/language/gobject_object.py | 17 +++++++- .../language/gobject_property.py | 15 +++++++ blueprintcompiler/language/gobject_signal.py | 12 ++++++ blueprintcompiler/language/gtk_a11y.py | 19 +++++++++ .../language/gtk_combo_box_text.py | 21 +++++++++- blueprintcompiler/language/gtk_file_filter.py | 18 ++++++++ blueprintcompiler/language/gtk_layout.py | 19 +++++++++ .../language/gtk_list_item_factory.py | 15 ++++++- blueprintcompiler/language/gtk_menu.py | 30 +++++++++++-- blueprintcompiler/language/gtk_scale.py | 19 +++++++++ blueprintcompiler/language/gtk_size_group.py | 18 ++++++++ blueprintcompiler/language/gtk_string_list.py | 18 ++++++++ blueprintcompiler/language/gtk_styles.py | 18 ++++++++ .../language/gtkbuilder_template.py | 15 +++++-- blueprintcompiler/lsp.py | 26 ++++++++++++ blueprintcompiler/lsp_utils.py | 42 ++++++++++++++++++- blueprintcompiler/parse_tree.py | 37 +++++++++++++--- blueprintcompiler/parser.py | 3 +- blueprintcompiler/tokenizer.py | 24 +++++++++++ tests/test_samples.py | 11 ++--- 24 files changed, 469 insertions(+), 28 deletions(-) diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 7eebe45..59331f9 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -22,7 +22,8 @@ from collections import ChainMap, defaultdict from functools import cached_property from .errors import * -from .lsp_utils import SemanticToken +from .lsp_utils import DocumentSymbol, SemanticToken +from .tokenizer import Range TType = T.TypeVar("TType") @@ -54,6 +55,18 @@ class Children: return [child for child in self._children if isinstance(child, key)] +class Ranges: + def __init__(self, ranges: T.Dict[str, Range]): + self._ranges = ranges + + def __getitem__(self, key: T.Union[str, tuple[str, str]]) -> T.Optional[Range]: + if isinstance(key, str): + return self._ranges.get(key) + elif isinstance(key, tuple): + start, end = key + return Range.join(self._ranges.get(start), self._ranges.get(end)) + + TCtx = T.TypeVar("TCtx") TAttr = T.TypeVar("TAttr") @@ -102,6 +115,10 @@ class AstNode: def context(self): return Ctx(self) + @cached_property + def ranges(self): + return Ranges(self.group.ranges) + @cached_property def root(self): if self.parent is None: @@ -109,6 +126,10 @@ class AstNode: else: return self.parent.root + @property + def range(self): + return Range(self.group.start, self.group.end, self.group.text) + def parent_by_type(self, type: T.Type[TType]) -> TType: if self.parent is None: raise CompilerBugError() @@ -175,6 +196,20 @@ class AstNode: for child in self.children: yield from child.get_semantic_tokens() + @property + def document_symbol(self) -> T.Optional[DocumentSymbol]: + return None + + def get_document_symbols(self) -> T.List[DocumentSymbol]: + result = [] + for child in self.children: + if s := child.document_symbol: + s.children = child.get_document_symbols() + result.append(s) + else: + result.extend(child.get_document_symbols()) + return result + def validate_unique_in_parent( self, error: str, check: T.Optional[T.Callable[["AstNode"], bool]] = None ): diff --git a/blueprintcompiler/language/adw_breakpoint.py b/blueprintcompiler/language/adw_breakpoint.py index e71b29a..aec4ab5 100644 --- a/blueprintcompiler/language/adw_breakpoint.py +++ b/blueprintcompiler/language/adw_breakpoint.py @@ -35,6 +35,16 @@ class AdwBreakpointCondition(AstNode): def condition(self) -> str: return self.tokens["condition"] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "condition", + SymbolKind.Property, + self.range, + self.group.tokens["kw"].range, + self.condition, + ) + @docs("kw") def keyword_docs(self): klass = self.root.gir.get_type("Breakpoint", "Adw") @@ -93,6 +103,16 @@ class AdwBreakpointSetter(AstNode): else: return None + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + f"{self.object_id}.{self.property_name}", + SymbolKind.Property, + self.range, + self.group.tokens["object"].range, + self.value.range.text, + ) + @context(ValueTypeCtx) def value_type(self) -> ValueTypeCtx: if self.gir_property is not None: @@ -147,12 +167,25 @@ class AdwBreakpointSetter(AstNode): class AdwBreakpointSetters(AstNode): - grammar = ["setters", Match("{").expected(), Until(AdwBreakpointSetter, "}")] + grammar = [ + Keyword("setters"), + Match("{").expected(), + Until(AdwBreakpointSetter, "}"), + ] @property def setters(self) -> T.List[AdwBreakpointSetter]: return self.children[AdwBreakpointSetter] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "setters", + SymbolKind.Struct, + self.range, + self.group.tokens["setters"].range, + ) + @validate() def container_is_breakpoint(self): validate_parent_type(self, "Adw", "Breakpoint", "breakpoint setters") diff --git a/blueprintcompiler/language/adw_message_dialog.py b/blueprintcompiler/language/adw_message_dialog.py index 98c40cd..fb2be66 100644 --- a/blueprintcompiler/language/adw_message_dialog.py +++ b/blueprintcompiler/language/adw_message_dialog.py @@ -84,6 +84,16 @@ class ExtAdwMessageDialogResponse(AstNode): def value(self) -> StringValue: return self.children[0] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.id, + SymbolKind.Field, + self.range, + self.group.tokens["id"].range, + self.value.range.text, + ) + @context(ValueTypeCtx) def value_type(self) -> ValueTypeCtx: return ValueTypeCtx(StringType()) @@ -108,6 +118,15 @@ class ExtAdwMessageDialog(AstNode): def responses(self) -> T.List[ExtAdwMessageDialogResponse]: return self.children + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "responses", + SymbolKind.Array, + self.range, + self.group.tokens["responses"].range, + ) + @validate("responses") def container_is_message_dialog(self): validate_parent_type(self, "Adw", "MessageDialog", "responses") diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index e45cddc..e1bfa36 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -46,7 +46,14 @@ from ..gir import ( IntType, StringType, ) -from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType +from ..lsp_utils import ( + Completion, + CompletionItemKind, + DocumentSymbol, + SemanticToken, + SemanticTokenType, + SymbolKind, +) from ..parse_tree import * OBJECT_CONTENT_HOOKS = AnyOf() diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 1a42c0a..16c92e7 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -21,6 +21,9 @@ import typing as T from functools import cached_property +from blueprintcompiler.errors import T +from blueprintcompiler.lsp_utils import DocumentSymbol + from .common import * from .response_id import ExtResponse from .types import ClassName, ConcreteClassName @@ -59,8 +62,20 @@ class Object(AstNode): def signature(self) -> str: if self.id: return f"{self.class_name.gir_type.full_name} {self.id}" + elif t := self.class_name.gir_type: + return f"{t.full_name}" else: - return f"{self.class_name.gir_type.full_name}" + return f"{self.class_name.as_string}" + + @property + def document_symbol(self) -> T.Optional[DocumentSymbol]: + return DocumentSymbol( + self.class_name.as_string, + SymbolKind.Object, + self.range, + self.children[ClassName][0].range, + self.id, + ) @property def gir_class(self) -> GirType: diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 68f8c6d..3d2ac5b 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -47,6 +47,21 @@ class Property(AstNode): else: return None + @property + def document_symbol(self) -> DocumentSymbol: + if isinstance(self.value, ObjectValue): + detail = None + else: + detail = self.value.range.text + + return DocumentSymbol( + self.name, + SymbolKind.Property, + self.range, + self.group.tokens["name"].range, + detail, + ) + @validate() def binding_valid(self): if ( diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 721c443..063b2e8 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -51,12 +51,14 @@ class Signal(AstNode): ] ), "=>", + Mark("detail_start"), Optional(["$", UseLiteral("extern", True)]), UseIdent("handler").expected("the name of a function to handle the signal"), Match("(").expected("argument list"), Optional(UseIdent("object")).expected("object identifier"), Match(")").expected(), ZeroOrMore(SignalFlag), + Mark("detail_end"), ) @property @@ -105,6 +107,16 @@ class Signal(AstNode): def gir_class(self): return self.parent.parent.gir_class + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.full_name, + SymbolKind.Event, + self.range, + self.group.tokens["name"].range, + self.ranges["detail_start", "detail_end"].text, + ) + @validate("handler") def old_extern(self): if not self.tokens["extern"]: diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 6a346ec..e9850f5 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -139,6 +139,16 @@ class A11yProperty(BaseAttribute): def value_type(self) -> ValueTypeCtx: return ValueTypeCtx(get_types(self.root.gir).get(self.tokens["name"])) + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.name, + SymbolKind.Field, + self.range, + self.group.tokens["name"].range, + self.value.range.text, + ) + @validate("name") def is_valid_property(self): types = get_types(self.root.gir) @@ -172,6 +182,15 @@ class ExtAccessibility(AstNode): def properties(self) -> T.List[A11yProperty]: return self.children[A11yProperty] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "accessibility", + SymbolKind.Struct, + self.range, + self.group.tokens["accessibility"].range, + ) + @validate("accessibility") def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "accessibility properties") diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index e6c804e..4c6fda9 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -31,13 +31,23 @@ class Item(AstNode): ] @property - def name(self) -> str: + def name(self) -> T.Optional[str]: return self.tokens["name"] @property def value(self) -> StringValue: return self.children[StringValue][0] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.value.range.text, + SymbolKind.String, + self.range, + self.value.range, + self.name, + ) + @validate("name") def unique_in_parent(self): if self.name is not None: @@ -54,6 +64,15 @@ class ExtComboBoxItems(AstNode): "]", ] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "items", + SymbolKind.Array, + self.range, + self.group.tokens["items"].range, + ) + @validate("items") def container_is_combo_box_text(self): validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 53cd102..1be83e8 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -23,6 +23,15 @@ from .gobject_object import ObjectContent, validate_parent_type class Filters(AstNode): + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.tokens["tag_name"], + SymbolKind.Array, + self.range, + self.group.tokens[self.tokens["tag_name"]].range, + ) + @validate() def container_is_file_filter(self): validate_parent_type(self, "Gtk", "FileFilter", "file filter properties") @@ -46,6 +55,15 @@ class FilterString(AstNode): def item(self) -> str: return self.tokens["name"] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.item, + SymbolKind.String, + self.range, + self.group.tokens["name"].range, + ) + @validate() def unique_in_parent(self): self.validate_unique_in_parent( diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 7632c7a..892e0c6 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -36,6 +36,16 @@ class LayoutProperty(AstNode): def value(self) -> Value: return self.children[Value][0] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.name, + SymbolKind.Field, + self.range, + self.group.tokens["name"].range, + self.value.range.text, + ) + @context(ValueTypeCtx) def value_type(self) -> ValueTypeCtx: # there isn't really a way to validate these @@ -56,6 +66,15 @@ class ExtLayout(AstNode): Until(LayoutProperty, "}"), ) + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "layout", + SymbolKind.Struct, + self.range, + self.group.tokens["layout"].range, + ) + @validate("layout") def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "layout properties") diff --git a/blueprintcompiler/language/gtk_list_item_factory.py b/blueprintcompiler/language/gtk_list_item_factory.py index ba2a27f..7d05422 100644 --- a/blueprintcompiler/language/gtk_list_item_factory.py +++ b/blueprintcompiler/language/gtk_list_item_factory.py @@ -1,5 +1,9 @@ +import typing as T + +from blueprintcompiler.errors import T +from blueprintcompiler.lsp_utils import DocumentSymbol + from ..ast_utils import AstNode, validate -from ..parse_tree import Keyword from .common import * from .contexts import ScopeCtx from .gobject_object import ObjectContent, validate_parent_type @@ -17,6 +21,15 @@ class ExtListItemFactory(AstNode): def signature(self) -> str: return f"template {self.gir_class.full_name}" + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.signature, + SymbolKind.Object, + self.range, + self.group.tokens["id"].range, + ) + @property def type_name(self) -> T.Optional[TypeName]: if len(self.children[TypeName]) == 1: diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index 824ec5c..e1bbd57 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -42,6 +42,16 @@ class Menu(AstNode): else: return "Gio.Menu" + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.tokens["tag"], + SymbolKind.Object, + self.range, + self.group.tokens[self.tokens["tag"]].range, + self.id, + ) + @property def tag(self) -> str: return self.tokens["tag"] @@ -72,6 +82,18 @@ class MenuAttribute(AstNode): def value(self) -> StringValue: return self.children[StringValue][0] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.name, + SymbolKind.Field, + self.range, + self.group.tokens["name"].range + if self.group.tokens["name"] + else self.range, + self.value.range.text, + ) + @context(ValueTypeCtx) def value_type(self) -> ValueTypeCtx: return ValueTypeCtx(None) @@ -98,7 +120,7 @@ menu_attribute = Group( menu_section = Group( Menu, [ - "section", + Keyword("section"), UseLiteral("tag", "section"), Optional(UseIdent("id")), Match("{").expected(), @@ -109,7 +131,7 @@ menu_section = Group( menu_submenu = Group( Menu, [ - "submenu", + Keyword("submenu"), UseLiteral("tag", "submenu"), Optional(UseIdent("id")), Match("{").expected(), @@ -120,7 +142,7 @@ menu_submenu = Group( menu_item = Group( Menu, [ - "item", + Keyword("item"), UseLiteral("tag", "item"), Match("{").expected(), Until(menu_attribute, "}"), @@ -130,7 +152,7 @@ menu_item = Group( menu_item_shorthand = Group( Menu, [ - "item", + Keyword("item"), UseLiteral("tag", "item"), "(", Group( diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index 18452e1..504e290 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -58,6 +58,16 @@ class ExtScaleMark(AstNode): else: return None + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + str(self.value), + SymbolKind.Field, + self.range, + self.group.tokens["mark"].range, + self.label.string if self.label else None, + ) + @docs("position") def position_docs(self) -> T.Optional[str]: if member := self.root.gir.get_type("PositionType", "Gtk").members.get( @@ -88,6 +98,15 @@ class ExtScaleMarks(AstNode): def marks(self) -> T.List[ExtScaleMark]: return self.children + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "marks", + SymbolKind.Array, + self.range, + self.group.tokens["marks"].range, + ) + @validate("marks") def container_is_size_group(self): validate_parent_type(self, "Gtk", "Scale", "scale marks") diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index 2a10a35..60af861 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -30,6 +30,15 @@ class Widget(AstNode): def name(self) -> str: return self.tokens["name"] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.name, + SymbolKind.Field, + self.range, + self.group.tokens["name"].range, + ) + @validate("name") def obj_widget(self): object = self.context[ScopeCtx].objects.get(self.tokens["name"]) @@ -62,6 +71,15 @@ class ExtSizeGroupWidgets(AstNode): "]", ] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "widgets", + SymbolKind.Array, + self.range, + self.group.tokens["widgets"].range, + ) + @validate("widgets") def container_is_size_group(self): validate_parent_type(self, "Gtk", "SizeGroup", "size group properties") diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index 455960e..37a46ec 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -30,6 +30,15 @@ class Item(AstNode): def child(self) -> StringValue: return self.children[StringValue][0] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.child.range.text, + SymbolKind.String, + self.range, + self.range, + ) + class ExtStringListStrings(AstNode): grammar = [ @@ -39,6 +48,15 @@ class ExtStringListStrings(AstNode): "]", ] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "strings", + SymbolKind.Array, + self.range, + self.group.tokens["strings"].range, + ) + @validate("items") def container_is_string_list(self): validate_parent_type(self, "Gtk", "StringList", "StringList items") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 8152b82..26c0e74 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -29,6 +29,15 @@ class StyleClass(AstNode): def name(self) -> str: return self.tokens["name"] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.name, + SymbolKind.String, + self.range, + self.range, + ) + @validate("name") def unique_in_parent(self): self.validate_unique_in_parent( @@ -44,6 +53,15 @@ class ExtStyles(AstNode): "]", ] + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "styles", + SymbolKind.Array, + self.range, + self.group.tokens["styles"].range, + ) + @validate("styles") def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "style classes") diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 7af5cc8..e1e2131 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -46,10 +46,19 @@ class Template(Object): @property def signature(self) -> str: - if self.parent_type: - return f"template {self.gir_class.full_name} : {self.parent_type.gir_type.full_name}" + if self.parent_type and self.parent_type.gir_type: + return f"template {self.class_name.as_string} : {self.parent_type.gir_type.full_name}" else: - return f"template {self.gir_class.full_name}" + return f"template {self.class_name.as_string}" + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.signature, + SymbolKind.Object, + self.range, + self.group.tokens["id"].range, + ) @property def gir_class(self) -> GirType: diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index 54a36c8..ec161a8 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -203,6 +203,7 @@ class LanguageServer: "completionProvider": {}, "codeActionProvider": {}, "hoverProvider": True, + "documentSymbolProvider": True, }, "serverInfo": { "name": "Blueprint", @@ -363,6 +364,31 @@ class LanguageServer: self._send_response(id, actions) + @command("textDocument/documentSymbol") + def document_symbols(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + symbols = open_file.ast.get_document_symbols() + + def to_json(symbol: DocumentSymbol): + result = { + "name": symbol.name, + "kind": symbol.kind, + "range": utils.idxs_to_range( + symbol.range.start, symbol.range.end, open_file.text + ), + "selectionRange": utils.idxs_to_range( + symbol.selection_range.start, + symbol.selection_range.end, + open_file.text, + ), + "children": [to_json(child) for child in symbol.children], + } + if symbol.detail is not None: + result["detail"] = symbol.detail + return result + + self._send_response(id, [to_json(symbol) for symbol in symbols]) + def _send_file_updates(self, open_file: OpenFile): self._send_notification( "textDocument/publishDiagnostics", diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index 2d1072e..2225ba2 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -20,9 +20,10 @@ import enum import typing as T -from dataclasses import dataclass +from dataclasses import dataclass, field from .errors import * +from .tokenizer import Range from .utils import * @@ -129,3 +130,42 @@ class SemanticToken: start: int end: int type: SemanticTokenType + + +class SymbolKind(enum.IntEnum): + File = 1 + Module = 2 + Namespace = 3 + Package = 4 + Class = 5 + Method = 6 + Property = 7 + Field = 8 + Constructor = 9 + Enum = 10 + Interface = 11 + Function = 12 + Variable = 13 + Constant = 14 + String = 15 + Number = 16 + Boolean = 17 + Array = 18 + Object = 19 + Key = 20 + Null = 21 + EnumMember = 22 + Struct = 23 + Event = 24 + Operator = 25 + TypeParameter = 26 + + +@dataclass +class DocumentSymbol: + name: str + kind: SymbolKind + range: Range + selection_range: Range + detail: T.Optional[str] = None + children: T.List["DocumentSymbol"] = field(default_factory=list) diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 3ddac69..b29bd96 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -20,7 +20,6 @@ """ Utilities for parsing an AST from a token stream. """ import typing as T -from collections import defaultdict from enum import Enum from .ast_utils import AstNode @@ -31,7 +30,7 @@ from .errors import ( UnexpectedTokenError, assert_true, ) -from .tokenizer import Token, TokenType +from .tokenizer import Range, Token, TokenType SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE] @@ -63,14 +62,16 @@ class ParseGroup: be converted to AST nodes by passing the children and key=value pairs to the AST node constructor.""" - def __init__(self, ast_type: T.Type[AstNode], start: int): + def __init__(self, ast_type: T.Type[AstNode], start: int, text: str): self.ast_type = ast_type self.children: T.List[ParseGroup] = [] self.keys: T.Dict[str, T.Any] = {} self.tokens: T.Dict[str, T.Optional[Token]] = {} + self.ranges: T.Dict[str, Range] = {} self.start = start self.end: T.Optional[int] = None self.incomplete = False + self.text = text def add_child(self, child: "ParseGroup"): self.children.append(child) @@ -81,6 +82,10 @@ class ParseGroup: self.keys[key] = val self.tokens[key] = token + def set_range(self, key: str, range: Range): + assert_true(key not in self.ranges) + self.ranges[key] = range + def to_ast(self): """Creates an AST node from the match group.""" children = [child.to_ast() for child in self.children] @@ -104,8 +109,9 @@ class ParseGroup: class ParseContext: """Contains the state of the parser.""" - def __init__(self, tokens: T.List[Token], index=0): + def __init__(self, tokens: T.List[Token], text: str, index=0): self.tokens = tokens + self.text = text self.binding_power = 0 self.index = index @@ -113,6 +119,7 @@ class ParseContext: self.group: T.Optional[ParseGroup] = None self.group_keys: T.Dict[str, T.Tuple[T.Any, T.Optional[Token]]] = {} self.group_children: T.List[ParseGroup] = [] + self.group_ranges: T.Dict[str, Range] = {} self.last_group: T.Optional[ParseGroup] = None self.group_incomplete = False @@ -124,7 +131,7 @@ class ParseContext: context will be used to parse one node. If parsing is successful, the new context will be applied to "self". If parsing fails, the new context will be discarded.""" - ctx = ParseContext(self.tokens, self.index) + ctx = ParseContext(self.tokens, self.text, self.index) ctx.errors = self.errors ctx.warnings = self.warnings ctx.binding_power = self.binding_power @@ -140,6 +147,8 @@ class ParseContext: other.group.set_val(key, val, token) for child in other.group_children: other.group.add_child(child) + for key, range in other.group_ranges.items(): + other.group.set_range(key, range) other.group.end = other.tokens[other.index - 1].end other.group.incomplete = other.group_incomplete self.group_children.append(other.group) @@ -148,6 +157,7 @@ class ParseContext: # its matched values self.group_keys = {**self.group_keys, **other.group_keys} self.group_children += other.group_children + self.group_ranges = {**self.group_ranges, **other.group_ranges} self.group_incomplete |= other.group_incomplete self.index = other.index @@ -161,13 +171,19 @@ class ParseContext: def start_group(self, ast_type: T.Type[AstNode]): """Sets this context to have its own match group.""" assert_true(self.group is None) - self.group = ParseGroup(ast_type, self.tokens[self.index].start) + self.group = ParseGroup(ast_type, self.tokens[self.index].start, self.text) def set_group_val(self, key: str, value: T.Any, token: T.Optional[Token]): """Sets a matched key=value pair on the current match group.""" assert_true(key not in self.group_keys) self.group_keys[key] = (value, token) + def set_mark(self, key: str): + """Sets a zero-length range on the current match group at the current position.""" + self.group_ranges[key] = Range( + self.tokens[self.index].start, self.tokens[self.index].start, self.text + ) + def set_group_incomplete(self): """Marks the current match group as incomplete (it could not be fully parsed, but the parser recovered).""" @@ -604,6 +620,15 @@ class Keyword(ParseNode): return str(token) == self.kw +class Mark(ParseNode): + def __init__(self, key: str): + self.key = key + + def _parse(self, ctx: ParseContext): + ctx.set_mark(self.key) + return True + + def to_parse_node(value) -> ParseNode: if isinstance(value, str): return Match(value) diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index 8b50de5..a9cc0ae 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -30,7 +30,8 @@ def parse( """Parses a list of tokens into an abstract syntax tree.""" try: - ctx = ParseContext(tokens) + original_text = tokens[0].string if len(tokens) else "" + ctx = ParseContext(tokens, original_text) AnyOf(UI).parse(ctx) ast_node = ctx.last_group.to_ast() if ctx.last_group else None diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index bde8dd1..64abf0f 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -20,6 +20,7 @@ import re import typing as T +from dataclasses import dataclass from enum import Enum from .errors import CompileError, CompilerBugError @@ -62,6 +63,10 @@ class Token: def __str__(self) -> str: return self.string[self.start : self.end] + @property + def range(self) -> "Range": + return Range(self.start, self.end, self.string) + def get_number(self) -> T.Union[int, float]: if self.type != TokenType.NUMBER: raise CompilerBugError() @@ -103,3 +108,22 @@ def _tokenize(ui_ml: str): def tokenize(data: str) -> T.List[Token]: return list(_tokenize(data)) + + +@dataclass +class Range: + start: int + end: int + original_text: str + + @property + def text(self) -> str: + return self.original_text[self.start : self.end] + + @staticmethod + def join(a: T.Optional["Range"], b: T.Optional["Range"]) -> T.Optional["Range"]: + if a is None: + return b + if b is None: + return a + return Range(min(a.start, b.start), max(a.end, b.end), a.original_text) diff --git a/tests/test_samples.py b/tests/test_samples.py index bacca80..9ad8cc0 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -40,13 +40,12 @@ from blueprintcompiler.tokenizer import Token, TokenType, tokenize class TestSamples(unittest.TestCase): - def assert_docs_dont_crash(self, text, ast): + def assert_ast_doesnt_crash(self, text, tokens, ast): for i in range(len(text)): ast.get_docs(i) - - def assert_completions_dont_crash(self, text, ast, tokens): for i in range(len(text)): list(complete(ast, tokens, i)) + ast.get_document_symbols() def assert_sample(self, name, skip_run=False): print(f'assert_sample("{name}", skip_run={skip_run})') @@ -79,8 +78,7 @@ class TestSamples(unittest.TestCase): print("\n".join(diff)) raise AssertionError() - self.assert_docs_dont_crash(blueprint, ast) - self.assert_completions_dont_crash(blueprint, ast, tokens) + self.assert_ast_doesnt_crash(blueprint, tokens, ast) except PrintableError as e: # pragma: no cover e.pretty_print(name + ".blp", blueprint) raise AssertionError() @@ -105,8 +103,7 @@ class TestSamples(unittest.TestCase): ast, errors, warnings = parser.parse(tokens) if ast is not None: - self.assert_docs_dont_crash(blueprint, ast) - self.assert_completions_dont_crash(blueprint, ast, tokens) + self.assert_ast_doesnt_crash(blueprint, tokens, ast) if errors: raise errors From 62f74178f7c9168d578c5295190a0976eed43e85 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 25 Jul 2023 18:40:05 -0500 Subject: [PATCH 006/138] lsp: Implement "go to definition" --- blueprintcompiler/ast_utils.py | 14 ++++++++++---- blueprintcompiler/language/common.py | 1 + blueprintcompiler/language/values.py | 10 ++++++++++ blueprintcompiler/lsp.py | 16 ++++++++++++++++ blueprintcompiler/lsp_utils.py | 15 +++++++++++++++ blueprintcompiler/parse_tree.py | 2 ++ blueprintcompiler/tokenizer.py | 10 ++++++++++ 7 files changed, 64 insertions(+), 4 deletions(-) diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 59331f9..81958aa 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -22,7 +22,7 @@ from collections import ChainMap, defaultdict from functools import cached_property from .errors import * -from .lsp_utils import DocumentSymbol, SemanticToken +from .lsp_utils import DocumentSymbol, SemanticToken, LocationLink from .tokenizer import Range TType = T.TypeVar("TType") @@ -185,9 +185,8 @@ class AstNode: return getattr(self, name) for child in self.children: - if child.group.start <= idx < child.group.end: - docs = child.get_docs(idx) - if docs is not None: + if idx in child.range: + if docs := child.get_docs(idx): return docs return None @@ -196,6 +195,13 @@ class AstNode: for child in self.children: yield from child.get_semantic_tokens() + def get_reference(self, idx: int) -> T.Optional[LocationLink]: + for child in self.children: + if idx in child.range: + if ref := child.get_reference(idx): + return ref + return None + @property def document_symbol(self) -> T.Optional[DocumentSymbol]: return None diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index e1bfa36..8a6ce9b 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -50,6 +50,7 @@ from ..lsp_utils import ( Completion, CompletionItemKind, DocumentSymbol, + LocationLink, SemanticToken, SemanticTokenType, SymbolKind, diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 888d6a1..b9d719f 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -339,6 +339,16 @@ class IdentLiteral(AstNode): token = self.group.tokens["value"] yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) + def get_reference(self, _idx: int) -> T.Optional[LocationLink]: + ref = self.context[ScopeCtx].objects.get(self.ident) + if ref is None and self.root.is_legacy_template(self.ident): + ref = self.root.template + + if ref: + return LocationLink(self.range, ref.range, ref.ranges["id"]) + else: + return None + class Literal(AstNode): grammar = AnyOf( diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index ec161a8..a7e5f9b 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -204,6 +204,7 @@ class LanguageServer: "codeActionProvider": {}, "hoverProvider": True, "documentSymbolProvider": True, + "definitionProvider": True, }, "serverInfo": { "name": "Blueprint", @@ -389,6 +390,21 @@ class LanguageServer: self._send_response(id, [to_json(symbol) for symbol in symbols]) + @command("textDocument/definition") + def definition(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + idx = utils.pos_to_idx( + params["position"]["line"], params["position"]["character"], open_file.text + ) + definition = open_file.ast.get_reference(idx) + if definition is None: + self._send_response(id, None) + else: + self._send_response( + id, + definition.to_json(open_file.uri), + ) + def _send_file_updates(self, open_file: OpenFile): self._send_notification( "textDocument/publishDiagnostics", diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index 2225ba2..9da8461 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -169,3 +169,18 @@ class DocumentSymbol: selection_range: Range detail: T.Optional[str] = None children: T.List["DocumentSymbol"] = field(default_factory=list) + + +@dataclass +class LocationLink: + origin_selection_range: Range + target_range: Range + target_selection_range: Range + + def to_json(self, target_uri: str): + return { + "originSelectionRange": self.origin_selection_range.to_json(), + "targetUri": target_uri, + "targetRange": self.target_range.to_json(), + "targetSelectionRange": self.target_selection_range.to_json(), + } diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index b29bd96..a8efddb 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -81,6 +81,8 @@ class ParseGroup: self.keys[key] = val self.tokens[key] = token + if token: + self.set_range(key, token.range) def set_range(self, key: str, range: Range): assert_true(key not in self.ranges) diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index 64abf0f..e1066ac 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -24,6 +24,7 @@ from dataclasses import dataclass from enum import Enum from .errors import CompileError, CompilerBugError +from . import utils class TokenType(Enum): @@ -127,3 +128,12 @@ class Range: if b is None: return a return Range(min(a.start, b.start), max(a.end, b.end), a.original_text) + + def __contains__(self, other: T.Union[int, "Range"]) -> bool: + if isinstance(other, int): + return self.start <= other <= self.end + else: + return self.start <= other.start and self.end >= other.end + + def to_json(self): + return utils.idxs_to_range(self.start, self.end, self.original_text) From a9cb423b3b387c7bb3687b838118c256d7b86b88 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 25 Jul 2023 18:52:18 -0500 Subject: [PATCH 007/138] lsp: Add missing semantic highlight --- blueprintcompiler/language/gtk_scale.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index 504e290..a2c9168 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -68,6 +68,14 @@ class ExtScaleMark(AstNode): self.label.string if self.label else None, ) + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: + if range := self.ranges["position"]: + yield SemanticToken( + range.start, + range.end, + SemanticTokenType.EnumMember, + ) + @docs("position") def position_docs(self) -> T.Optional[str]: if member := self.root.gir.get_type("PositionType", "Gtk").members.get( From 56274d7c1faa430cb8747009d974fec4d1b4a321 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 25 Jul 2023 18:54:58 -0500 Subject: [PATCH 008/138] completions: Fix signal completion --- blueprintcompiler/completions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index b189bf1..56db53a 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -158,7 +158,7 @@ def signal_completer(ast_node, match_variables): yield Completion( signal, CompletionItemKind.Property, - snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;", + snippet=f"{signal} => \$${{1:{name}_{signal.replace('-', '_')}}}()$0;", ) From 3bcc9f4cbd2158fa7b90fb3280dbb1b7e03ddc33 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 25 Jul 2023 20:01:41 -0500 Subject: [PATCH 009/138] Use the new Range class in more places --- blueprintcompiler/ast_utils.py | 35 ++++++-------- blueprintcompiler/errors.py | 21 ++++---- blueprintcompiler/language/contexts.py | 3 +- blueprintcompiler/language/ui.py | 3 +- blueprintcompiler/lsp.py | 67 ++++++++++++-------------- blueprintcompiler/main.py | 6 +-- blueprintcompiler/parse_tree.py | 19 +++++--- blueprintcompiler/parser.py | 2 +- blueprintcompiler/tokenizer.py | 19 ++++++-- tests/test_samples.py | 6 +-- 10 files changed, 91 insertions(+), 90 deletions(-) diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 81958aa..56501e7 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -229,20 +229,23 @@ class AstNode: error, references=[ ErrorReference( - child.group.start, - child.group.end, + child.range, "previous declaration was here", ) ], ) -def validate(token_name=None, end_token_name=None, skip_incomplete=False): +def validate( + token_name: T.Optional[str] = None, + end_token_name: T.Optional[str] = None, + skip_incomplete: bool = False, +): """Decorator for functions that validate an AST node. Exceptions raised during validation are marked with range information from the tokens.""" def decorator(func): - def inner(self): + def inner(self: AstNode): if skip_incomplete and self.incomplete: return @@ -254,22 +257,14 @@ def validate(token_name=None, end_token_name=None, skip_incomplete=False): if self.incomplete: return - # 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 e.start is None: - if token := self.group.tokens.get(token_name): - e.start = token.start - else: - e.start = self.group.start - - if e.end is None: - if token := self.group.tokens.get(end_token_name): - e.end = token.end - elif token := self.group.tokens.get(token_name): - e.end = token.end - else: - e.end = self.group.end + if e.range is None: + e.range = ( + Range.join( + self.ranges[token_name], + self.ranges[end_token_name], + ) + or self.range + ) # Re-raise the exception raise e diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index e89ec31..773122a 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -23,6 +23,7 @@ import typing as T from dataclasses import dataclass from . import utils +from .tokenizer import Range from .utils import Colors @@ -36,8 +37,7 @@ class PrintableError(Exception): @dataclass class ErrorReference: - start: int - end: int + range: Range message: str @@ -50,8 +50,7 @@ class CompileError(PrintableError): def __init__( self, message: str, - start: T.Optional[int] = None, - end: T.Optional[int] = None, + range: T.Optional[Range] = None, did_you_mean: T.Optional[T.Tuple[str, T.List[str]]] = None, hints: T.Optional[T.List[str]] = None, actions: T.Optional[T.List["CodeAction"]] = None, @@ -61,8 +60,7 @@ class CompileError(PrintableError): super().__init__(message) self.message = message - self.start = start - self.end = end + self.range = range self.hints = hints or [] self.actions = actions or [] self.references = references or [] @@ -92,9 +90,9 @@ class CompileError(PrintableError): self.hint("Are your dependencies up to date?") def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None: - assert self.start is not None + assert self.range is not None - line_num, col_num = utils.idx_to_pos(self.start + 1, code) + line_num, col_num = utils.idx_to_pos(self.range.start + 1, code) line = code.splitlines(True)[line_num] # Display 1-based line numbers @@ -110,7 +108,7 @@ at {filename} line {line_num} column {col_num}: stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n") for ref in self.references: - line_num, col_num = utils.idx_to_pos(ref.start + 1, code) + line_num, col_num = utils.idx_to_pos(ref.range.start + 1, code) line = code.splitlines(True)[line_num] line_num += 1 @@ -138,14 +136,15 @@ class UpgradeWarning(CompileWarning): class UnexpectedTokenError(CompileError): - def __init__(self, start, end) -> None: - super().__init__("Unexpected tokens", start, end) + def __init__(self, range: Range) -> None: + super().__init__("Unexpected tokens", range) @dataclass class CodeAction: title: str replace_with: str + edit_range: T.Optional[Range] = None class MultipleErrors(PrintableError): diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index 2f8e22e..c5e97b3 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -70,8 +70,7 @@ class ScopeCtx: ): raise CompileError( f"Duplicate object ID '{obj.tokens['id']}'", - token.start, - token.end, + token.range, ) passed[obj.tokens["id"]] = obj diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index 1b7e6e9..3ce23da 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -62,8 +62,7 @@ class UI(AstNode): else: gir_ctx.not_found_namespaces.add(i.namespace) except CompileError as e: - e.start = i.group.tokens["namespace"].start - e.end = i.group.tokens["version"].end + e.range = i.range self._gir_errors.append(e) return gir_ctx diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index a7e5f9b..19c02e5 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -24,10 +24,12 @@ import traceback import typing as T from . import decompiler, parser, tokenizer, utils, xml_reader +from .ast_utils import AstNode from .completions import complete -from .errors import CompileError, MultipleErrors, PrintableError +from .errors import CompileError, MultipleErrors from .lsp_utils import * from .outputs.xml import XmlOutput +from .tokenizer import Token def printerr(*args, **kwargs): @@ -43,16 +45,16 @@ def command(json_method: str): class OpenFile: - def __init__(self, uri: str, text: str, version: int): + def __init__(self, uri: str, text: str, version: int) -> None: self.uri = uri self.text = text self.version = version - self.ast = None - self.tokens = None + self.ast: T.Optional[AstNode] = None + self.tokens: T.Optional[list[Token]] = None self._update() - def apply_changes(self, changes): + def apply_changes(self, changes) -> None: for change in changes: if "range" not in change: self.text = change["text"] @@ -70,8 +72,8 @@ class OpenFile: self.text = self.text[:start] + change["text"] + self.text[end:] self._update() - def _update(self): - self.diagnostics = [] + def _update(self) -> None: + self.diagnostics: list[CompileError] = [] try: self.tokens = tokenizer.tokenize(self.text) self.ast, errors, warnings = parser.parse(self.tokens) @@ -327,14 +329,17 @@ class LanguageServer: def code_actions(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] - range_start = utils.pos_to_idx( - params["range"]["start"]["line"], - params["range"]["start"]["character"], - open_file.text, - ) - range_end = utils.pos_to_idx( - params["range"]["end"]["line"], - params["range"]["end"]["character"], + range = Range( + utils.pos_to_idx( + params["range"]["start"]["line"], + params["range"]["start"]["character"], + open_file.text, + ), + utils.pos_to_idx( + params["range"]["end"]["line"], + params["range"]["end"]["character"], + open_file.text, + ), open_file.text, ) @@ -342,16 +347,14 @@ class LanguageServer: { "title": action.title, "kind": "quickfix", - "diagnostics": [ - self._create_diagnostic(open_file.text, open_file.uri, diagnostic) - ], + "diagnostics": [self._create_diagnostic(open_file.uri, diagnostic)], "edit": { "changes": { open_file.uri: [ { - "range": utils.idxs_to_range( - diagnostic.start, diagnostic.end, open_file.text - ), + "range": action.edit_range.to_json() + if action.edit_range + else diagnostic.range.to_json(), "newText": action.replace_with, } ] @@ -359,7 +362,7 @@ class LanguageServer: }, } for diagnostic in open_file.diagnostics - if not (diagnostic.end < range_start or diagnostic.start > range_end) + if range.overlaps(diagnostic.range) for action in diagnostic.actions ] @@ -374,14 +377,8 @@ class LanguageServer: result = { "name": symbol.name, "kind": symbol.kind, - "range": utils.idxs_to_range( - symbol.range.start, symbol.range.end, open_file.text - ), - "selectionRange": utils.idxs_to_range( - symbol.selection_range.start, - symbol.selection_range.end, - open_file.text, - ), + "range": symbol.range.to_json(), + "selectionRange": symbol.selection_range.to_json(), "children": [to_json(child) for child in symbol.children], } if symbol.detail is not None: @@ -411,22 +408,22 @@ class LanguageServer: { "uri": open_file.uri, "diagnostics": [ - self._create_diagnostic(open_file.text, open_file.uri, err) + self._create_diagnostic(open_file.uri, err) for err in open_file.diagnostics ], }, ) - def _create_diagnostic(self, text: str, uri: str, err: CompileError): + def _create_diagnostic(self, uri: str, err: CompileError): message = err.message - assert err.start is not None and err.end is not None + assert err.range is not None for hint in err.hints: message += "\nhint: " + hint result = { - "range": utils.idxs_to_range(err.start, err.end, text), + "range": err.range.to_json(), "message": message, "severity": DiagnosticSeverity.Warning if isinstance(err, CompileWarning) @@ -441,7 +438,7 @@ class LanguageServer: { "location": { "uri": uri, - "range": utils.idxs_to_range(ref.start, ref.end, text), + "range": ref.range.to_json(), }, "message": ref.message, } diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index db9fb65..416db47 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -24,8 +24,8 @@ import os import sys import typing as T -from . import decompiler, interactive_port, parser, tokenizer -from .errors import CompilerBugError, MultipleErrors, PrintableError, report_bug +from . import interactive_port, parser, tokenizer +from .errors import CompilerBugError, CompileError, PrintableError, report_bug from .gir import add_typelib_search_path from .lsp import LanguageServer from .outputs import XmlOutput @@ -157,7 +157,7 @@ class BlueprintApp: def cmd_port(self, opts): interactive_port.run(opts) - def _compile(self, data: str) -> T.Tuple[str, T.List[PrintableError]]: + def _compile(self, data: str) -> T.Tuple[str, T.List[CompileError]]: tokens = tokenizer.tokenize(data) ast, errors, warnings = parser.parse(tokens) diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index a8efddb..8f3ef31 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -224,11 +224,11 @@ class ParseContext: if ( len(self.errors) and isinstance((err := self.errors[-1]), UnexpectedTokenError) - and err.end == start + and err.range.end == start ): - err.end = end + err.range.end = end else: - self.errors.append(UnexpectedTokenError(start, end)) + self.errors.append(UnexpectedTokenError(Range(start, end, self.text))) def is_eof(self) -> bool: return self.index >= len(self.tokens) or self.peek_token().type == TokenType.EOF @@ -281,10 +281,11 @@ class Err(ParseNode): start_idx = ctx.start while ctx.tokens[start_idx].type in SKIP_TOKENS: start_idx += 1 - start_token = ctx.tokens[start_idx] - end_token = ctx.tokens[ctx.index] - raise CompileError(self.message, start_token.start, end_token.end) + + raise CompileError( + self.message, Range(start_token.start, start_token.start, ctx.text) + ) return True @@ -324,7 +325,9 @@ class Fail(ParseNode): start_token = ctx.tokens[start_idx] end_token = ctx.tokens[ctx.index] - raise CompileError(self.message, start_token.start, end_token.end) + raise CompileError( + self.message, Range.join(start_token.range, end_token.range) + ) return True @@ -373,7 +376,7 @@ class Statement(ParseNode): token = ctx.peek_token() if str(token) != ";": - ctx.errors.append(CompileError("Expected `;`", token.start, token.end)) + ctx.errors.append(CompileError("Expected `;`", token.range)) else: ctx.next_token() return True diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index a9cc0ae..89e1533 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -26,7 +26,7 @@ from .tokenizer import TokenType def parse( tokens: T.List[Token], -) -> T.Tuple[T.Optional[UI], T.Optional[MultipleErrors], T.List[PrintableError]]: +) -> T.Tuple[T.Optional[UI], T.Optional[MultipleErrors], T.List[CompileError]]: """Parses a list of tokens into an abstract syntax tree.""" try: diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index e1066ac..1ab6def 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -23,7 +23,6 @@ import typing as T from dataclasses import dataclass from enum import Enum -from .errors import CompileError, CompilerBugError from . import utils @@ -69,6 +68,8 @@ class Token: return Range(self.start, self.end, self.string) def get_number(self) -> T.Union[int, float]: + from .errors import CompileError, CompilerBugError + if self.type != TokenType.NUMBER: raise CompilerBugError() @@ -81,12 +82,12 @@ class Token: else: return int(string) except: - raise CompileError( - f"{str(self)} is not a valid number literal", self.start, self.end - ) + raise CompileError(f"{str(self)} is not a valid number literal", self.range) def _tokenize(ui_ml: str): + from .errors import CompileError + i = 0 while i < len(ui_ml): matched = False @@ -101,7 +102,8 @@ def _tokenize(ui_ml: str): if not matched: raise CompileError( - "Could not determine what kind of syntax is meant here", i, i + "Could not determine what kind of syntax is meant here", + Range(i, i, ui_ml), ) yield Token(TokenType.EOF, i, i, ui_ml) @@ -117,6 +119,10 @@ class Range: end: int original_text: str + @property + def length(self) -> int: + return self.end - self.start + @property def text(self) -> str: return self.original_text[self.start : self.end] @@ -137,3 +143,6 @@ class Range: def to_json(self): return utils.idxs_to_range(self.start, self.end, self.original_text) + + def overlaps(self, other: "Range") -> bool: + return not (self.end < other.start or self.start > other.end) diff --git a/tests/test_samples.py b/tests/test_samples.py index 9ad8cc0..9d891f6 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -113,9 +113,9 @@ class TestSamples(unittest.TestCase): raise MultipleErrors(warnings) except PrintableError as e: - def error_str(error): - line, col = utils.idx_to_pos(error.start + 1, blueprint) - len = error.end - error.start + def error_str(error: CompileError): + line, col = utils.idx_to_pos(error.range.start + 1, blueprint) + len = error.range.length return ",".join([str(line + 1), str(col), str(len), error.message]) if isinstance(e, CompileError): From 35ee058192cd0a22dbbed15de7aa61bff9ae51e0 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 25 Jul 2023 20:02:03 -0500 Subject: [PATCH 010/138] lsp: Add code action to add missing imports --- blueprintcompiler/gir.py | 27 +++++++++++++++++++++++++-- blueprintcompiler/language/types.py | 11 ++++++++++- blueprintcompiler/language/ui.py | 12 ++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 3642bd6..635ef7c 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -18,7 +18,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import os -import sys import typing as T from functools import cached_property @@ -29,6 +28,7 @@ from gi.repository import GIRepository # type: ignore from . import typelib, xml_reader from .errors import CompileError, CompilerBugError +from .lsp_utils import CodeAction _namespace_cache: T.Dict[str, "Namespace"] = {} _xml_cache = {} @@ -65,6 +65,27 @@ def get_namespace(namespace: str, version: str) -> "Namespace": return _namespace_cache[filename] +_available_namespaces: list[tuple[str, str]] = [] + + +def get_available_namespaces() -> T.List[T.Tuple[str, str]]: + if len(_available_namespaces): + return _available_namespaces + + search_paths: list[str] = [ + *GIRepository.Repository.get_search_path(), + *_user_search_paths, + ] + + for search_path in search_paths: + for filename in os.listdir(search_path): + if filename.endswith(".typelib"): + namespace, version = filename.removesuffix(".typelib").rsplit("-", 1) + _available_namespaces.append((namespace, version)) + + return _available_namespaces + + def get_xml(namespace: str, version: str): search_paths = [] @@ -1011,9 +1032,11 @@ class GirContext: ns = ns or "Gtk" if ns not in self.namespaces and ns not in self.not_found_namespaces: + all_available = list(set(ns for ns, _version in get_available_namespaces())) + raise CompileError( f"Namespace {ns} was not imported", - did_you_mean=(ns, self.namespaces.keys()), + did_you_mean=(ns, all_available), ) def validate_type(self, name: str, ns: str) -> None: diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index e7b1867..b3fb586 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -55,7 +55,16 @@ class TypeName(AstNode): @validate("namespace") def gir_ns_exists(self): if not self.tokens["extern"]: - self.root.gir.validate_ns(self.tokens["namespace"]) + try: + self.root.gir.validate_ns(self.tokens["namespace"]) + except CompileError as e: + ns = self.tokens["namespace"] + e.actions = [ + self.root.import_code_action(n, version) + for n, version in gir.get_available_namespaces() + if n == ns + ] + raise e @validate() def deprecated(self) -> None: diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index 3ce23da..34ba193 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -99,6 +99,18 @@ class UI(AstNode): and self.template.class_name.glib_type_name == id ) + def import_code_action(self, ns: str, version: str) -> CodeAction: + if len(self.children[Import]): + pos = self.children[Import][-1].range.end + else: + pos = self.children[GtkDirective][0].range.end + + return CodeAction( + f"Import {ns} {version}", + f"\nusing {ns} {version};", + Range(pos, pos, self.group.text), + ) + @context(ScopeCtx) def scope_ctx(self) -> ScopeCtx: return ScopeCtx(node=self) From bfa2f56e1f3281bf80f566be44f2e6eda5bf92bd Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 25 Jul 2023 20:07:37 -0500 Subject: [PATCH 011/138] Sort imports --- blueprintcompiler/ast_utils.py | 2 +- blueprintcompiler/main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 56501e7..ce15faf 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -22,7 +22,7 @@ from collections import ChainMap, defaultdict from functools import cached_property from .errors import * -from .lsp_utils import DocumentSymbol, SemanticToken, LocationLink +from .lsp_utils import DocumentSymbol, LocationLink, SemanticToken from .tokenizer import Range TType = T.TypeVar("TType") diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index 416db47..306dd7d 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -25,7 +25,7 @@ import sys import typing as T from . import interactive_port, parser, tokenizer -from .errors import CompilerBugError, CompileError, PrintableError, report_bug +from .errors import CompileError, CompilerBugError, PrintableError, report_bug from .gir import add_typelib_search_path from .lsp import LanguageServer from .outputs import XmlOutput From 582502c1b4afc53831f52bcecb2cbc891e2efdea Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Sun, 13 Aug 2023 10:42:03 +0300 Subject: [PATCH 012/138] completions: fix property value completion --- blueprintcompiler/completions.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 56db53a..386f2d7 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -130,13 +130,14 @@ def property_completer(ast_node, match_variables): matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) def prop_value_completer(ast_node, match_variables): - if isinstance(ast_node.value_type, gir.Enumeration): - for name, member in ast_node.value_type.members.items(): - yield Completion(name, CompletionItemKind.EnumMember, docs=member.doc) + if (vt := ast_node.value_type) is not None: + if isinstance(vt.value_type, gir.Enumeration): + for name, member in vt.value_type.members.items(): + yield Completion(name, CompletionItemKind.EnumMember, docs=member.doc) - elif isinstance(ast_node.value_type, gir.BoolType): - yield Completion("true", CompletionItemKind.Constant) - yield Completion("false", CompletionItemKind.Constant) + elif isinstance(vt.value_type, gir.BoolType): + yield Completion("true", CompletionItemKind.Constant) + yield Completion("false", CompletionItemKind.Constant) @completer( From bcac78845612db336a3929f8a54dad5dd80d2390 Mon Sep 17 00:00:00 2001 From: z00000000z Date: Wed, 23 Aug 2023 16:21:37 +0000 Subject: [PATCH 013/138] completions: property_completer improvements --- blueprintcompiler/completions.py | 68 ++++++++++++++----- blueprintcompiler/completions_utils.py | 4 +- .../language/adw_message_dialog.py | 2 +- blueprintcompiler/language/gtk_a11y.py | 4 +- .../language/gtk_combo_box_text.py | 2 +- blueprintcompiler/language/gtk_file_filter.py | 2 +- blueprintcompiler/language/gtk_layout.py | 2 +- blueprintcompiler/language/gtk_menu.py | 4 +- blueprintcompiler/language/gtk_scale.py | 4 +- blueprintcompiler/language/gtk_size_group.py | 2 +- blueprintcompiler/language/gtk_string_list.py | 2 +- blueprintcompiler/language/gtk_styles.py | 2 +- blueprintcompiler/lsp.py | 6 +- blueprintcompiler/lsp_utils.py | 2 + tests/test_samples.py | 3 +- 15 files changed, 76 insertions(+), 33 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 386f2d7..0a43388 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -31,13 +31,13 @@ Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] def _complete( - ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int + lsp, ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int ) -> T.Iterator[Completion]: for child in ast_node.children: if child.group.start <= idx and ( idx < child.group.end or (idx == child.group.end and child.incomplete) ): - yield from _complete(child, tokens, idx, token_idx) + yield from _complete(lsp, child, tokens, idx, token_idx) return prev_tokens: T.List[Token] = [] @@ -50,11 +50,11 @@ def _complete( token_idx -= 1 for completer in ast_node.completers: - yield from completer(prev_tokens, ast_node) + yield from completer(prev_tokens, ast_node, lsp) def complete( - ast_node: AstNode, tokens: T.List[Token], idx: int + lsp, ast_node: AstNode, tokens: T.List[Token], idx: int ) -> T.Iterator[Completion]: token_idx = 0 # find the current token @@ -67,11 +67,11 @@ def complete( idx = tokens[token_idx].start token_idx -= 1 - yield from _complete(ast_node, tokens, idx, token_idx) + yield from _complete(lsp, ast_node, tokens, idx, token_idx) @completer([language.GtkDirective]) -def using_gtk(ast_node, match_variables): +def using_gtk(lsp, ast_node, match_variables): yield Completion("using Gtk 4.0;", CompletionItemKind.Keyword) @@ -79,7 +79,7 @@ def using_gtk(ast_node, match_variables): applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns, ) -def namespace(ast_node, match_variables): +def namespace(lsp, ast_node, match_variables): yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") for ns in ast_node.root.children[language.Import]: if ns.gir_namespace is not None: @@ -97,7 +97,7 @@ def namespace(ast_node, match_variables): [(TokenType.IDENT, None), (TokenType.OP, ".")], ], ) -def object_completer(ast_node, match_variables): +def object_completer(lsp, ast_node, match_variables): ns = ast_node.root.gir.namespaces.get(match_variables[0]) if ns is not None: for c in ns.classes.values(): @@ -108,7 +108,7 @@ def object_completer(ast_node, match_variables): applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns, ) -def gtk_object_completer(ast_node, match_variables): +def gtk_object_completer(lsp, ast_node, match_variables): ns = ast_node.root.gir.namespaces.get("Gtk") if ns is not None: for c in ns.classes.values(): @@ -119,17 +119,52 @@ def gtk_object_completer(ast_node, match_variables): applies_in=[language.ObjectContent], matches=new_statement_patterns, ) -def property_completer(ast_node, match_variables): +def property_completer(lsp, ast_node, match_variables): if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): - for prop in ast_node.gir_class.properties: - yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;") + for prop_name, prop in ast_node.gir_class.properties.items(): + if ( + isinstance(prop.type, gir.BoolType) + and lsp.client_supports_completion_choice + ): + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f"{prop_name}: ${{1|true,false|}};", + ) + elif isinstance(prop.type, gir.StringType): + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f'{prop_name}: "$0";', + ) + elif ( + isinstance(prop.type, gir.Enumeration) + and len(prop.type.members) <= 10 + and lsp.client_supports_completion_choice + ): + choices = ",".join(prop.type.members.keys()) + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f"{prop_name}: ${{1|{choices}|}};", + ) + else: + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f"{prop_name}: $0;", + ) @completer( applies_in=[language.Property, language.BaseAttribute], matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) -def prop_value_completer(ast_node, match_variables): +def prop_value_completer(lsp, ast_node, match_variables): if (vt := ast_node.value_type) is not None: if isinstance(vt.value_type, gir.Enumeration): for name, member in vt.value_type.members.items(): @@ -144,7 +179,7 @@ def prop_value_completer(ast_node, match_variables): applies_in=[language.ObjectContent], matches=new_statement_patterns, ) -def signal_completer(ast_node, match_variables): +def signal_completer(lsp, ast_node, match_variables): if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): for signal in ast_node.gir_class.signals: if not isinstance(ast_node.parent, language.Object): @@ -158,13 +193,14 @@ def signal_completer(ast_node, match_variables): ) yield Completion( signal, - CompletionItemKind.Property, + CompletionItemKind.Event, + sort_text=f"1 {signal}", snippet=f"{signal} => \$${{1:{name}_{signal.replace('-', '_')}}}()$0;", ) @completer(applies_in=[language.UI], matches=new_statement_patterns) -def template_completer(ast_node, match_variables): +def template_completer(lsp, ast_node, match_variables): yield Completion( "template", CompletionItemKind.Snippet, diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 2aae874..52e325d 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -44,7 +44,7 @@ def applies_to(*ast_types): def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): def decorator(func): - def inner(prev_tokens: T.List[Token], ast_node): + def inner(prev_tokens: T.List[Token], ast_node, lsp): # For completers that apply in ObjectContent nodes, we can further # check that the object is the right class if applies_in_subclass is not None: @@ -77,7 +77,7 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None if not any_match: return - yield from func(ast_node, match_variables) + yield from func(lsp, ast_node, match_variables) for c in applies_in: c.completers.append(inner) diff --git a/blueprintcompiler/language/adw_message_dialog.py b/blueprintcompiler/language/adw_message_dialog.py index fb2be66..9b3d41c 100644 --- a/blueprintcompiler/language/adw_message_dialog.py +++ b/blueprintcompiler/language/adw_message_dialog.py @@ -141,7 +141,7 @@ class ExtAdwMessageDialog(AstNode): applies_in_subclass=("Adw", "MessageDialog"), matches=new_statement_patterns, ) -def style_completer(ast_node, match_variables): +def style_completer(lsp, ast_node, match_variables): yield Completion( "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" ) diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index e9850f5..2ad8097 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -204,7 +204,7 @@ class ExtAccessibility(AstNode): applies_in=[ObjectContent], matches=new_statement_patterns, ) -def a11y_completer(ast_node, match_variables): +def a11y_completer(lsp, ast_node, match_variables): yield Completion( "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" ) @@ -214,7 +214,7 @@ def a11y_completer(ast_node, match_variables): applies_in=[ExtAccessibility], matches=new_statement_patterns, ) -def a11y_name_completer(ast_node, match_variables): +def a11y_name_completer(lsp, ast_node, match_variables): for name, type in get_types(ast_node.root.gir).items(): yield Completion( name, diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 4c6fda9..e514e4a 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -87,5 +87,5 @@ class ExtComboBoxItems(AstNode): applies_in_subclass=("Gtk", "ComboBoxText"), matches=new_statement_patterns, ) -def items_completer(ast_node, match_variables): +def items_completer(lsp, ast_node, match_variables): yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 1be83e8..8dcbcb8 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -104,7 +104,7 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix") applies_in_subclass=("Gtk", "FileFilter"), matches=new_statement_patterns, ) -def file_filter_completer(ast_node, match_variables): +def file_filter_completer(lsp, ast_node, match_variables): yield Completion( "mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]' ) diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 892e0c6..508609d 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -89,7 +89,7 @@ class ExtLayout(AstNode): applies_in_subclass=("Gtk", "Widget"), matches=new_statement_patterns, ) -def layout_completer(ast_node, match_variables): +def layout_completer(lsp, ast_node, match_variables): yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index e1bbd57..844c309 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -221,7 +221,7 @@ from .ui import UI applies_in=[UI], matches=new_statement_patterns, ) -def menu_completer(ast_node, match_variables): +def menu_completer(lsp, ast_node, match_variables): yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}") @@ -229,7 +229,7 @@ def menu_completer(ast_node, match_variables): applies_in=[Menu], matches=new_statement_patterns, ) -def menu_content_completer(ast_node, match_variables): +def menu_content_completer(lsp, ast_node, match_variables): yield Completion( "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}" ) diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index a2c9168..ac4b77c 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -129,14 +129,14 @@ class ExtScaleMarks(AstNode): applies_in_subclass=("Gtk", "Scale"), matches=new_statement_patterns, ) -def complete_marks(ast_node, match_variables): +def complete_marks(lsp, ast_node, match_variables): yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]") @completer( applies_in=[ExtScaleMarks], ) -def complete_mark(ast_node, match_variables): +def complete_mark(lsp, ast_node, match_variables): yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),") diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index 60af861..baa4c30 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -94,5 +94,5 @@ class ExtSizeGroupWidgets(AstNode): applies_in_subclass=("Gtk", "SizeGroup"), matches=new_statement_patterns, ) -def size_group_completer(ast_node, match_variables): +def size_group_completer(lsp, ast_node, match_variables): yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]") diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index 37a46ec..be01262 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -71,5 +71,5 @@ class ExtStringListStrings(AstNode): applies_in_subclass=("Gtk", "StringList"), matches=new_statement_patterns, ) -def strings_completer(ast_node, match_variables): +def strings_completer(lsp, ast_node, match_variables): yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 26c0e74..236dde0 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -76,7 +76,7 @@ class ExtStyles(AstNode): applies_in_subclass=("Gtk", "Widget"), matches=new_statement_patterns, ) -def style_completer(ast_node, match_variables): +def style_completer(lsp, ast_node, match_variables): yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]') diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index 19c02e5..2281059 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -115,6 +115,7 @@ class LanguageServer: def __init__(self): self.client_capabilities = {} + self.client_supports_completion_choice = False self._open_files: T.Dict[str, OpenFile] = {} def run(self): @@ -187,6 +188,9 @@ class LanguageServer: from . import main self.client_capabilities = params.get("capabilities", {}) + self.client_supports_completion_choice = params.get("clientInfo", {}).get( + "name" + ) in ["Visual Studio Code", "VSCodium"] self._send_response( id, { @@ -271,7 +275,7 @@ class LanguageServer: idx = utils.pos_to_idx( params["position"]["line"], params["position"]["character"], open_file.text ) - completions = complete(open_file.ast, open_file.tokens, idx) + completions = complete(self, open_file.ast, open_file.tokens, idx) self._send_response( id, [completion.to_json(True) for completion in completions] ) diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index 9da8461..33ae923 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -80,6 +80,7 @@ class Completion: kind: CompletionItemKind signature: T.Optional[str] = None deprecated: bool = False + sort_text: T.Optional[str] = None docs: T.Optional[str] = None text: T.Optional[str] = None snippet: T.Optional[str] = None @@ -103,6 +104,7 @@ class Completion: if self.docs else None, "deprecated": self.deprecated, + "sortText": self.sort_text, "insertText": insert_text, "insertTextFormat": insert_text_format, } diff --git a/tests/test_samples.py b/tests/test_samples.py index 9d891f6..226e762 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -35,6 +35,7 @@ from blueprintcompiler.errors import ( MultipleErrors, PrintableError, ) +from blueprintcompiler.lsp import LanguageServer from blueprintcompiler.outputs.xml import XmlOutput from blueprintcompiler.tokenizer import Token, TokenType, tokenize @@ -44,7 +45,7 @@ class TestSamples(unittest.TestCase): for i in range(len(text)): ast.get_docs(i) for i in range(len(text)): - list(complete(ast, tokens, i)) + list(complete(LanguageServer(), ast, tokens, i)) ast.get_document_symbols() def assert_sample(self, name, skip_run=False): From a8512d83f30133493f454896b49983f732225922 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 30 Aug 2023 13:49:18 +0200 Subject: [PATCH 014/138] doc: Cleanup the Flatpak module --- docs/flatpak.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/flatpak.rst b/docs/flatpak.rst index 89355b7..5689163 100644 --- a/docs/flatpak.rst +++ b/docs/flatpak.rst @@ -12,6 +12,7 @@ a module in your flatpak manifest: { "name": "blueprint-compiler", "buildsystem": "meson", + "cleanup": ["*"], "sources": [ { "type": "git", From 0f5be1b0512bdd755a2901c66447db9f5fcf5bff Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 31 Aug 2023 14:58:29 -0500 Subject: [PATCH 015/138] docs: Use correct lexer name for code blocks --- docs/index.rst | 2 +- docs/packaging.rst | 2 +- docs/reference/diagnostics.rst | 4 ++-- docs/reference/document_root.rst | 6 +++--- docs/reference/expressions.rst | 4 ++-- docs/reference/extensions.rst | 18 +++++++++--------- docs/reference/menus.rst | 4 ++-- docs/reference/objects.rst | 14 +++++++------- docs/reference/templates.rst | 6 +++--- docs/reference/values.rst | 10 +++++----- docs/translations.rst | 4 ++-- 11 files changed, 37 insertions(+), 37 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index e277add..06547c2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,7 +22,7 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces. packaging -.. code-block:: +.. code-block:: blueprint using Gtk 4.0; diff --git a/docs/packaging.rst b/docs/packaging.rst index 4f248e6..78bb2c2 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -13,7 +13,7 @@ GObject Introspection Blueprint files can import GObject Introspection namespaces like this: -.. code-block:: +.. code-block:: blueprint using Gtk 4.0; using Adw 1; diff --git a/docs/reference/diagnostics.rst b/docs/reference/diagnostics.rst index b8fee83..2c0af59 100644 --- a/docs/reference/diagnostics.rst +++ b/docs/reference/diagnostics.rst @@ -166,14 +166,14 @@ version_conflict ---------------- This error occurs when two versions of a namespace are imported (possibly transitively) in the same file. For example, this will cause a version conflict: -.. code-block:: blueprintui +.. code-block:: blueprint using Gtk 4.0; using Gtk 3.0; But so will this: -.. code-block:: blueprintui +.. code-block:: blueprint using Gtk 4.0; using Handy 1; diff --git a/docs/reference/document_root.rst b/docs/reference/document_root.rst index af5357d..13dea58 100644 --- a/docs/reference/document_root.rst +++ b/docs/reference/document_root.rst @@ -17,7 +17,7 @@ A blueprint document consists of a :ref:`GTK declaration`, one s Example ~~~~~~~ -.. code-block:: blueprintui +.. code-block:: blueprint // Gtk Declaration using Gtk 4.0; @@ -43,7 +43,7 @@ Every blueprint file begins with the line ``using Gtk 4.0;``, which declares the Example ~~~~~~~ -.. code-block:: blueprintui +.. code-block:: blueprint using Gtk 4.0; @@ -68,7 +68,7 @@ The compiler requires typelib files for these libraries to be installed. They ar Example ~~~~~~~ -.. code-block:: blueprintui +.. code-block:: blueprint // Import libadwaita using Adw 1; diff --git a/docs/reference/expressions.rst b/docs/reference/expressions.rst index 042aec7..1ba50ee 100644 --- a/docs/reference/expressions.rst +++ b/docs/reference/expressions.rst @@ -6,7 +6,7 @@ Expressions make your user interface code *reactive*. This means when your application's data changes, the user interface reacts to the change automatically. -.. code-block:: blueprintui +.. code-block:: blueprint label: bind MyAppWindow.account.username; /* ^ ^ ^ @@ -81,7 +81,7 @@ Cast Expressions Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. -.. code-block:: blueprintui +.. code-block:: blueprint // Cast the result of the closure so blueprint knows it's a string label: bind $my_closure() as \ No newline at end of file diff --git a/docs/reference/extensions.rst b/docs/reference/extensions.rst index fb5a46b..9675038 100644 --- a/docs/reference/extensions.rst +++ b/docs/reference/extensions.rst @@ -78,7 +78,7 @@ Valid in `Adw.MessageDialog `_ The ``items`` block defines the items that will be added to the combo box. The optional ID can be used to refer to the item rather than its label. -.. code-block:: blueprintui +.. code-block:: blueprint ComboBoxText { items [ @@ -133,7 +133,7 @@ Valid in `Gtk.FileFilter `_. The ``mime-types``, ``patterns``, and ``suffixes`` blocks define the items that will be added to the file filter. The ``mime-types`` block accepts mime types (including wildcards for subtypes, such as ``image/*``). The ``patterns`` block accepts glob patterns, and the ``suffixes`` block accepts file extensions. -.. code-block:: blueprintui +.. code-block:: blueprint FileFilter { mime-types [ "text/plain", "image/*" ] @@ -156,7 +156,7 @@ Valid in `Gtk.Widget `_. The ``layout`` block describes how the widget should be positioned within its parent. The available properties depend on the parent widget's layout manager. -.. code-block:: blueprintui +.. code-block:: blueprint Grid { Button { @@ -196,7 +196,7 @@ The ``template`` block defines the template that will be used to create list ite The template type must be `Gtk.ListItem `_. The template object can be referenced with the ``template`` keyword. -.. code-block:: blueprintui +.. code-block:: blueprint ListView { factory: BuilderListItemFactory { @@ -244,7 +244,7 @@ Valid in `Gtk.SizeGroup `_. The ``widgets`` block defines the widgets that will be added to the size group. -.. code-block:: blueprintui +.. code-block:: blueprint Box { Button button1 {} @@ -270,7 +270,7 @@ Valid in `Gtk.StringList `_. The ``strings`` block defines the strings in the string list. -.. code-block:: blueprintui +.. code-block:: blueprint StringList { strings ["violin", "guitar", _("harp")] @@ -291,7 +291,7 @@ Valid in any `Gtk.Widget `_. The ``styles`` block defines CSS classes that will be added to the widget. -.. code-block:: blueprintui +.. code-block:: blueprint Button { styles ["suggested-action"] @@ -327,7 +327,7 @@ The ``action response`` extension sets the ``action`` child type for the child a No more than one child of a dialog or infobar may have the ``default`` flag. -.. code-block:: blueprintui +.. code-block:: blueprint Dialog { [action response=ok default] diff --git a/docs/reference/menus.rst b/docs/reference/menus.rst index 525458b..2d7bfea 100644 --- a/docs/reference/menus.rst +++ b/docs/reference/menus.rst @@ -21,7 +21,7 @@ Menus, such as the application menu, are defined using the ``menu`` keyword. Men Example ~~~~~~~ -.. code-block:: blueprintui +.. code-block:: blueprint menu my_menu { submenu { @@ -53,7 +53,7 @@ The most common menu attributes are ``label``, ``action``, and ``icon``. Because Example ~~~~~~~ -.. code-block:: blueprintui +.. code-block:: blueprint menu { item ("label") diff --git a/docs/reference/objects.rst b/docs/reference/objects.rst index fe4d1a7..699db49 100644 --- a/docs/reference/objects.rst +++ b/docs/reference/objects.rst @@ -24,7 +24,7 @@ Optionally, objects may have an ID to provide a handle for other parts of the bl Example ~~~~~~~ -.. code-block:: blueprintui +.. code-block:: blueprint Label label1 { label: "Hello, world!"; @@ -69,7 +69,7 @@ A property's value can be another object, either inline or referenced by ID. Example ~~~~~~~ -.. code-block:: blueprintui +.. code-block:: blueprint Label { label: "text"; @@ -102,7 +102,7 @@ Optionally, you can provide an object ID to use when connecting the signal. Example ~~~~~~~ -.. code-block:: blueprintui +.. code-block:: blueprint Button { clicked => $on_button_clicked(); @@ -141,7 +141,7 @@ Examples Add children to a container +++++++++++++++++++++++++++ -.. code-block:: blueprintui +.. code-block:: blueprint Button { Image {} @@ -150,7 +150,7 @@ Add children to a container Child types +++++++++++ -.. code-block:: blueprintui +.. code-block:: blueprint HeaderBar { [start] @@ -165,7 +165,7 @@ Child types Child extensions ++++++++++++++++ -.. code-block:: blueprintui +.. code-block:: blueprint Dialog { // Here, a child extension annotation defines the button's response. @@ -176,7 +176,7 @@ Child extensions Internal children +++++++++++++++++ -.. code-block:: blueprintui +.. code-block:: blueprint Dialog { [internal-child content_area] diff --git a/docs/reference/templates.rst b/docs/reference/templates.rst index fa4c264..180979f 100644 --- a/docs/reference/templates.rst +++ b/docs/reference/templates.rst @@ -15,7 +15,7 @@ Widget subclassing is one of the primary techniques for structuring an applicati You could implement this with the following blueprint: -.. code-block:: blueprintui +.. code-block:: blueprint using Gtk 4.0; @@ -39,7 +39,7 @@ We can solve these problems by giving each widget its own blueprint file, which For this to work, we need to specify in the blueprint which object is the one being instantiated. We do this with a template block: -.. code-block:: blueprintui +.. code-block:: blueprint using Gtk 4.0; @@ -56,7 +56,7 @@ This blueprint can only be used by the ``MapsHeaderBar`` constructor. Instantiat This ``MapsHeaderBar`` class, along with its blueprint template, can then be referenced in another blueprint: -.. code-block:: blueprintui +.. code-block:: blueprint using Gtk 4.0; diff --git a/docs/reference/values.rst b/docs/reference/values.rst index 035408f..a50293d 100644 --- a/docs/reference/values.rst +++ b/docs/reference/values.rst @@ -45,7 +45,7 @@ The type of a ``typeof<>`` literal is `GType ; @@ -66,7 +66,7 @@ Flags are used to specify a set of options. One or more of the available flag va Example ~~~~~~~ -.. code-block:: blueprintui +.. code-block:: blueprint Adw.TabView { shortcuts: control_tab | control_shift_tab; @@ -85,7 +85,7 @@ Translated Strings Use ``_("...")`` to mark strings as translatable. You can put a comment for translators on the line above if needed. -.. code-block:: blueprintui +.. code-block:: blueprint Gtk.Label label { /* Translators: This is the main text of the welcome screen */ @@ -94,7 +94,7 @@ Use ``_("...")`` to mark strings as translatable. You can put a comment for tran Use ``C_("context", "...")`` to add a *message context* to a string to disambiguate it, in case the same string appears in different places. Remember, two strings might be the same in one language but different in another depending on context. -.. code-block:: blueprintui +.. code-block:: blueprint Gtk.Label label { /* Translators: This is a section in the preferences window */ @@ -118,7 +118,7 @@ The simplest bindings connect to a property of another object in the blueprint. Example ~~~~~~~ -.. code-block:: blueprintui +.. code-block:: blueprint /* Use bindings to show a label when a switch * is active, without any application code */ diff --git a/docs/translations.rst b/docs/translations.rst index b143d36..7ebf929 100644 --- a/docs/translations.rst +++ b/docs/translations.rst @@ -5,7 +5,7 @@ Translations Blueprint files can be translated with xgettext. To mark a string as translated, use the following syntax: -.. code-block:: +.. code-block:: blueprint _("translated string") @@ -34,7 +34,7 @@ conflicts. Two strings that are the same in English, but appear in different contexts, might be different in another language! To disambiguate, use ``C_`` instead of ``_`` and add a context string as the first argument: -.. code-block:: +.. code-block:: blueprint C_("shortcuts window", "Quit") From 3cd5daf025ac71752a1f6b83d588b52407e4103a Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 7 Sep 2023 12:13:05 -0500 Subject: [PATCH 016/138] Fix a crash found by the fuzzer --- blueprintcompiler/language/gtkbuilder_child.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index 52c8ad4..3f19fa5 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -88,7 +88,7 @@ class Child(AstNode): hints = [ "only Gio.ListStore or Gtk.Buildable implementors can have children" ] - if "child" in gir_class.properties: + if hasattr(gir_class, "properties") and "child" in gir_class.properties: hints.append( "did you mean to assign this object to the 'child' property?" ) From 0c0219551026ec9aec5487891e07d1ced3a31112 Mon Sep 17 00:00:00 2001 From: Jerry James Date: Wed, 13 Sep 2023 08:31:22 -0600 Subject: [PATCH 017/138] Handle big endian bitfields correctly --- blueprintcompiler/gir.py | 5 +++-- blueprintcompiler/typelib.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 635ef7c..9ad5699 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -18,6 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import os +import sys import typing as T from functools import cached_property @@ -948,8 +949,8 @@ class Repository(GirNode): return self.lookup_namespace(ns).get_type(dir_entry.DIR_ENTRY_NAME) def _resolve_type_id(self, type_id: int) -> GirType: - if type_id & 0xFFFFFF == 0: - type_id = (type_id >> 27) & 0x1F + if type_id & (0xFFFFFF if sys.byteorder == "little" else 0xFFFFFF00) == 0: + type_id = ((type_id >> 27) if sys.byteorder == "little" else type_id) & 0x1F # simple type if type_id == typelib.TYPE_BOOLEAN: return BoolType() diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py index c8a3ff3..cfb750b 100644 --- a/blueprintcompiler/typelib.py +++ b/blueprintcompiler/typelib.py @@ -61,7 +61,14 @@ class Field: def __init__(self, offset: int, type: str, shift=0, mask=None): self._offset = offset self._type = type - self._shift = shift + if not mask or sys.byteorder == "little": + self._shift = shift + elif self._type == "u8" or self._type == "i8": + self._shift = 7 - shift + elif self._type == "u16" or self._type == "i16": + self._shift = 15 - shift + else: + self._shift = 31 - shift self._mask = (1 << mask) - 1 if mask else None self._name = f"{offset}__{type}__{shift}__{mask}" @@ -174,7 +181,7 @@ class Typelib: OBJ_FINAL = Field(0x02, "u16", 3, 1) OBJ_GTYPE_NAME = Field(0x08, "string") OBJ_PARENT = Field(0x10, "dir_entry") - OBJ_GTYPE_STRUCT = Field(0x14, "string") + OBJ_GTYPE_STRUCT = Field(0x12, "string") OBJ_N_INTERFACES = Field(0x14, "u16") OBJ_N_FIELDS = Field(0x16, "u16") OBJ_N_PROPERTIES = Field(0x18, "u16") @@ -259,7 +266,9 @@ class Typelib: def _int(self, size, signed) -> int: return int.from_bytes( - self._typelib_file[self._offset : self._offset + size], sys.byteorder + self._typelib_file[self._offset : self._offset + size], + sys.byteorder, + signed=signed, ) From 057c767fbb595bb31d025c76547045273948aab1 Mon Sep 17 00:00:00 2001 From: James Westman Date: Thu, 14 Sep 2023 10:19:49 -0500 Subject: [PATCH 018/138] typelib: Fix byte order issue --- blueprintcompiler/typelib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py index cfb750b..145bf57 100644 --- a/blueprintcompiler/typelib.py +++ b/blueprintcompiler/typelib.py @@ -64,11 +64,11 @@ class Field: if not mask or sys.byteorder == "little": self._shift = shift elif self._type == "u8" or self._type == "i8": - self._shift = 7 - shift + self._shift = 8 - (shift + mask) elif self._type == "u16" or self._type == "i16": - self._shift = 15 - shift + self._shift = 16 - (shift + mask) else: - self._shift = 31 - shift + self._shift = 32 - (shift + mask) self._mask = (1 << mask) - 1 if mask else None self._name = f"{offset}__{type}__{shift}__{mask}" From 80cb57cb88a532e24f5b66d940d28f11f059f899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Capypara=20K=C3=B6pcke?= Date: Sun, 10 Sep 2023 11:57:41 +0200 Subject: [PATCH 019/138] batch-compile: Fix mixing relative+absolute paths --- blueprintcompiler/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index 306dd7d..4666d78 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -124,9 +124,11 @@ class BlueprintApp: for file in opts.inputs: data = file.read() + file_abs = os.path.abspath(file.name) + input_dir_abs = os.path.abspath(opts.input_dir) try: - if not os.path.commonpath([file.name, opts.input_dir]): + if not os.path.commonpath([file_abs, input_dir_abs]): print( f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}" ) From cf136ab09ffc1c901d467d21d506ef51718e470e Mon Sep 17 00:00:00 2001 From: Urtsi Santsi Date: Sat, 2 Sep 2023 02:11:09 +0300 Subject: [PATCH 020/138] Add notice that the file is generated Fixes #123 --- blueprintcompiler/lsp.py | 2 +- blueprintcompiler/outputs/xml/xml_emitter.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index 2281059..9ef6f57 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -291,7 +291,7 @@ class LanguageServer: xml = None try: output = XmlOutput() - xml = output.emit(open_file.ast) + xml = output.emit(open_file.ast, generated_notice=False) except: printerr(traceback.format_exc()) self._send_error(id, ErrorCode.RequestFailed, "Could not compile document") diff --git a/blueprintcompiler/outputs/xml/xml_emitter.py b/blueprintcompiler/outputs/xml/xml_emitter.py index 44013da..ca87a49 100644 --- a/blueprintcompiler/outputs/xml/xml_emitter.py +++ b/blueprintcompiler/outputs/xml/xml_emitter.py @@ -25,9 +25,18 @@ from blueprintcompiler.language.types import ClassName class XmlEmitter: - def __init__(self, indent=2): + def __init__(self, indent=2, generated_notice=True): self.indent = indent self.result = '' + if generated_notice: + self.result += ( + "\n" + "" + ) self._tag_stack = [] self._needs_newline = False From cc66b05a8709abfb1685717958c1b74a46165e9c Mon Sep 17 00:00:00 2001 From: Urtsi Santsi Date: Sat, 2 Sep 2023 02:42:11 +0300 Subject: [PATCH 021/138] Add generated notice to test files --- tests/samples/accessibility.ui | 5 +++++ tests/samples/action_widgets.ui | 5 +++++ tests/samples/adw_breakpoint.ui | 5 +++++ tests/samples/child_type.ui | 5 +++++ tests/samples/combo_box_text.ui | 5 +++++ tests/samples/comments.ui | 5 +++++ tests/samples/enum.ui | 5 +++++ tests/samples/expr_closure.ui | 5 +++++ tests/samples/expr_closure_args.ui | 5 +++++ tests/samples/expr_lookup.ui | 5 +++++ tests/samples/file_filter.ui | 5 +++++ tests/samples/flags.ui | 5 +++++ tests/samples/id_prop.ui | 5 +++++ tests/samples/issue_119.ui | 5 +++++ tests/samples/layout.ui | 5 +++++ tests/samples/list_factory.ui | 10 ++++++++++ tests/samples/menu.ui | 5 +++++ tests/samples/numbers.ui | 5 +++++ tests/samples/object_prop.ui | 5 +++++ tests/samples/parseable.ui | 5 +++++ tests/samples/placeholder.ui | 5 +++++ tests/samples/property.ui | 5 +++++ tests/samples/property_binding.ui | 5 +++++ tests/samples/responses.ui | 5 +++++ tests/samples/scale_marks.ui | 5 +++++ tests/samples/signal.ui | 5 +++++ tests/samples/size_group.ui | 5 +++++ tests/samples/string_list.ui | 5 +++++ tests/samples/strings.ui | 5 +++++ tests/samples/style.ui | 5 +++++ tests/samples/subscope.ui | 10 ++++++++++ tests/samples/template.ui | 5 +++++ tests/samples/template_bind_property.ui | 5 +++++ tests/samples/template_binding.ui | 5 +++++ tests/samples/template_binding_extern.ui | 5 +++++ tests/samples/template_id.ui | 5 +++++ tests/samples/template_no_parent.ui | 5 +++++ tests/samples/template_simple_binding.ui | 5 +++++ tests/samples/translated.ui | 5 +++++ tests/samples/typeof.ui | 5 +++++ tests/samples/uint.ui | 5 +++++ tests/samples/unchecked_class.ui | 5 +++++ tests/samples/using.ui | 5 +++++ 43 files changed, 225 insertions(+) diff --git a/tests/samples/accessibility.ui b/tests/samples/accessibility.ui index 321f20f..7732a14 100644 --- a/tests/samples/accessibility.ui +++ b/tests/samples/accessibility.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/action_widgets.ui b/tests/samples/action_widgets.ui index 91b6e64..ac730a2 100644 --- a/tests/samples/action_widgets.ui +++ b/tests/samples/action_widgets.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/adw_breakpoint.ui b/tests/samples/adw_breakpoint.ui index b2d5ec3..f667958 100644 --- a/tests/samples/adw_breakpoint.ui +++ b/tests/samples/adw_breakpoint.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/child_type.ui b/tests/samples/child_type.ui index df9386e..56f03eb 100644 --- a/tests/samples/child_type.ui +++ b/tests/samples/child_type.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/combo_box_text.ui b/tests/samples/combo_box_text.ui index bb234d3..c160d80 100644 --- a/tests/samples/combo_box_text.ui +++ b/tests/samples/combo_box_text.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/comments.ui b/tests/samples/comments.ui index b916a09..bd3378a 100644 --- a/tests/samples/comments.ui +++ b/tests/samples/comments.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/enum.ui b/tests/samples/enum.ui index d2cda1e..e6ad9d4 100644 --- a/tests/samples/enum.ui +++ b/tests/samples/enum.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/expr_closure.ui b/tests/samples/expr_closure.ui index 1581d65..46300da 100644 --- a/tests/samples/expr_closure.ui +++ b/tests/samples/expr_closure.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/expr_closure_args.ui b/tests/samples/expr_closure_args.ui index 1b539ac..6c702d9 100644 --- a/tests/samples/expr_closure_args.ui +++ b/tests/samples/expr_closure_args.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/expr_lookup.ui b/tests/samples/expr_lookup.ui index 91d7590..16172d4 100644 --- a/tests/samples/expr_lookup.ui +++ b/tests/samples/expr_lookup.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/file_filter.ui b/tests/samples/file_filter.ui index 1cb0114..6ac8717 100644 --- a/tests/samples/file_filter.ui +++ b/tests/samples/file_filter.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/flags.ui b/tests/samples/flags.ui index 56fbf31..2f0a26e 100644 --- a/tests/samples/flags.ui +++ b/tests/samples/flags.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/id_prop.ui b/tests/samples/id_prop.ui index 4940824..cece573 100644 --- a/tests/samples/id_prop.ui +++ b/tests/samples/id_prop.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/issue_119.ui b/tests/samples/issue_119.ui index 2e039b5..b574549 100644 --- a/tests/samples/issue_119.ui +++ b/tests/samples/issue_119.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/layout.ui b/tests/samples/layout.ui index 027d010..a8c294f 100644 --- a/tests/samples/layout.ui +++ b/tests/samples/layout.ui @@ -1,4 +1,9 @@ + diff --git a/tests/samples/list_factory.ui b/tests/samples/list_factory.ui index 447a53a..a1e7c27 100644 --- a/tests/samples/list_factory.ui +++ b/tests/samples/list_factory.ui @@ -1,10 +1,20 @@ + +