Compare commits

...

7 commits

Author SHA1 Message Date
Ivan Molodetskikh
b20888586c Merge branch 'YaLTeR-main-patch-76590' into 'main'
setup.rst: Fix meson dependencies

See merge request jwestman/blueprint-compiler!2
2025-01-02 20:48:07 +00:00
James Westman
d6f4b88d35
lsp: Fix crash on incomplete detailed signal 2024-12-25 10:31:35 -06:00
James Westman
a6d57cebec language: Add not-swapped flag for signals
This is needed because GtkBuilder defaults to swapped when you specify
the object attribute.
2024-12-23 02:46:52 +00:00
James Westman
9b9fab832b
Add tests, remove unused code, fix bugs
- Added tests for more error messages
- Test the "go to reference" feature at every character index of every
test case
- Delete unused code and imports
- Fix some bugs I found along the way
2024-12-22 18:00:39 -06:00
James Westman
5b0f662478
completions: Detect translatable properties
Looked through the Gtk documentation (and a few other libraries) to make
a list of all the properties that should probably be translated. If a
property is on the list, the language server will mark it as translated
in completions.
2024-12-21 17:47:36 -06:00
Jordan Petridis
ac70ea7403 Port to libgirepository-2.0
pygobject 3.52 has switched [1] to using libgirepository-2.0 which
comes from glib itself now, rather than the 1.0 which came from
gobject-introspection.

This means that it fails to load the incompatible "GIRepository 2.0"
and thus must be ported to 3.0 (which is provided by
libgirepository-2.0).

Migration guide is here [2]

[1]: https://gitlab.gnome.org/GNOME/pygobject/-/merge_requests/320
[2]: https://docs.gtk.org/girepository/migrating-gi.html

This commit adds suppport for importing with
"gi.require_version("GIRepository", "3.0") and falling
back to the existing "GIRepository 2.0" if not found.
2024-12-16 13:37:40 +00:00
Ivan Molodetskikh
9f9a2d94e0 setup.rst: Fix meson dependencies
The previous way did not establish dependencies between .ui files and the gresource properly.
2021-12-03 16:58:24 +00:00
58 changed files with 448 additions and 210 deletions

View file

@ -0,0 +1,191 @@
# annotations.py
#
# Copyright 2024 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
# Extra information about types in common libraries that's used for things like completions.
import typing as T
from dataclasses import dataclass
from . import gir
@dataclass
class Annotation:
translatable_properties: T.List[str]
def is_property_translated(property: gir.Property):
ns = property.get_containing(gir.Namespace)
ns_name = ns.name + "-" + ns.version
if annotation := _ANNOTATIONS.get(ns_name):
assert property.container is not None
return (
property.container.name + ":" + property.name
in annotation.translatable_properties
)
else:
return False
_ANNOTATIONS = {
"Gtk-4.0": Annotation(
translatable_properties=[
"AboutDialog:comments",
"AboutDialog:translator-credits",
"AboutDialog:website-label",
"AlertDialog:detail",
"AlertDialog:message",
"AppChooserButton:heading",
"AppChooserDialog:heading",
"AppChooserWidget:default-text",
"AssistantPage:title",
"Button:label",
"CellRendererText:markup",
"CellRendererText:placeholder-text",
"CellRendererText:text",
"CheckButton:label",
"ColorButton:title",
"ColorDialog:title",
"ColumnViewColumn:title",
"ColumnViewRow:accessible-description",
"ColumnViewRow:accessible-label",
"Entry:placeholder-text",
"Entry:primary-icon-tooltip-markup",
"Entry:primary-icon-tooltip-text",
"Entry:secondary-icon-tooltip-markup",
"Entry:secondary-icon-tooltip-text",
"EntryBuffer:text",
"Expander:label",
"FileChooserNative:accept-label",
"FileChooserNative:cancel-label",
"FileChooserWidget:subtitle",
"FileDialog:accept-label",
"FileDialog:title",
"FileDialog:initial-name",
"FileFilter:name",
"FontButton:title",
"FontDialog:title",
"Frame:label",
"Inscription:markup",
"Inscription:text",
"Label:label",
"ListItem:accessible-description",
"ListItem:accessible-label",
"LockButton:text-lock",
"LockButton:text-unlock",
"LockButton:tooltip-lock",
"LockButton:tooltip-not-authorized",
"LockButton:tooltip-unlock",
"MenuButton:label",
"MessageDialog:secondary-text",
"MessageDialog:text",
"NativeDialog:title",
"NotebookPage:menu-label",
"NotebookPage:tab-label",
"PasswordEntry:placeholder-text",
"Picture:alternative-text",
"PrintDialog:accept-label",
"PrintDialog:title",
"Printer:name",
"PrintJob:title",
"PrintOperation:custom-tab-label",
"PrintOperation:export-filename",
"PrintOperation:job-name",
"ProgressBar:text",
"SearchEntry:placeholder-text",
"ShortcutLabel:disabled-text",
"ShortcutsGroup:title",
"ShortcutsSection:title",
"ShortcutsShortcut:title",
"ShortcutsShortcut:subtitle",
"StackPage:title",
"Text:placeholder-text",
"TextBuffer:text",
"TreeViewColumn:title",
"Widget:tooltip-markup",
"Widget:tooltip-text",
"Window:title",
"Editable:text",
"FontChooser:preview-text",
]
),
"Adw-1": Annotation(
translatable_properties=[
"AboutDialog:comments",
"AboutDialog:translator-credits",
"AboutWindow:comments",
"AboutWindow:translator-credits",
"ActionRow:subtitle",
"ActionRow:title",
"AlertDialog:body",
"AlertDialog:heading",
"Avatar:text",
"Banner:button-label",
"Banner:title",
"ButtonContent:label",
"Dialog:title",
"ExpanderRow:subtitle",
"MessageDialog:body",
"MessageDialog:heading",
"NavigationPage:title",
"PreferencesGroup:description",
"PreferencesGroup:title",
"PreferencesPage:description",
"PreferencesPage:title",
"PreferencesRow:title",
"SplitButton:dropdown-tooltip",
"SplitButton:label",
"StatusPage:description",
"StatusPage:title",
"TabPage:indicator-tooltip",
"TabPage:keyword",
"TabPage:title",
"Toast:button-label",
"Toast:title",
"ViewStackPage:title",
"ViewSwitcherTitle:subtitle",
"ViewSwitcherTitle:title",
"WindowTitle:subtitle",
"WindowTitle:title",
]
),
"Shumate-1.0": Annotation(
translatable_properties=[
"License:extra-text",
"MapSource:license",
"MapSource:name",
]
),
"GtkSource-5": Annotation(
translatable_properties=[
"CompletionCell:markup",
"CompletionCell:text",
"CompletionSnippets:title",
"CompletionWords:title",
"GutterRendererText:markup",
"GutterRendererText:text",
"SearchSettings:search-text",
"Snippet:description",
"Snippet:name",
"SnippetChunk:tooltip-text",
"StyleScheme:description",
"StyleScheme:name",
]
),
}

View file

@ -160,6 +160,11 @@ class AstNode:
yield e
if e.fatal:
return
except MultipleErrors as e:
for error in e.errors:
yield error
if error.fatal:
return
for child in self.children:
yield from child._get_errors()
@ -249,14 +254,7 @@ def validate(
if skip_incomplete and self.incomplete:
return
try:
func(self)
except CompileError as e:
# If the node is only partially complete, then an error must
# have already been reported at the parsing stage
if self.incomplete:
return
def fill_error(e: CompileError):
if e.range is None:
e.range = (
Range.join(
@ -266,8 +264,26 @@ def validate(
or self.range
)
try:
func(self)
except CompileError as e:
# If the node is only partially complete, then an error must
# have already been reported at the parsing stage
if self.incomplete:
return
fill_error(e)
# Re-raise the exception
raise e
except MultipleErrors as e:
if self.incomplete:
return
for error in e.errors:
fill_error(error)
raise e
inner._validator = True
return inner

View file

@ -17,10 +17,9 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import sys
import typing as T
from . import gir, language
from . import annotations, gir, language
from .ast_utils import AstNode
from .completions_utils import *
from .language.types import ClassName
@ -31,10 +30,6 @@ from .tokenizer import Token, TokenType
Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]]
def debug(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def _complete(
lsp, ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int
) -> T.Iterator[Completion]:
@ -139,7 +134,7 @@ def gtk_object_completer(lsp, ast_node, match_variables):
matches=new_statement_patterns,
)
def property_completer(lsp, ast_node, match_variables):
if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType):
if ast_node.gir_class and hasattr(ast_node.gir_class, "properties"):
for prop_name, prop in ast_node.gir_class.properties.items():
if (
isinstance(prop.type, gir.BoolType)
@ -154,11 +149,17 @@ def property_completer(lsp, ast_node, match_variables):
detail=prop.detail,
)
elif isinstance(prop.type, gir.StringType):
snippet = (
f'{prop_name}: _("$0");'
if annotations.is_property_translated(prop)
else f'{prop_name}: "$0";'
)
yield Completion(
prop_name,
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=f'{prop_name}: "$0";',
snippet=snippet,
docs=prop.doc,
detail=prop.detail,
)
@ -188,7 +189,7 @@ def property_completer(lsp, ast_node, match_variables):
@completer(
applies_in=[language.Property, language.BaseAttribute],
applies_in=[language.Property, language.A11yProperty],
matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]],
)
def prop_value_completer(lsp, ast_node, match_variables):
@ -212,7 +213,7 @@ def prop_value_completer(lsp, ast_node, match_variables):
matches=new_statement_patterns,
)
def signal_completer(lsp, ast_node, match_variables):
if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType):
if ast_node.gir_class and hasattr(ast_node.gir_class, "signals"):
for signal_name, signal in ast_node.gir_class.signals.items():
if not isinstance(ast_node.parent, language.Object):
name = "on"

View file

@ -31,17 +31,6 @@ new_statement_patterns = [
]
def applies_to(*ast_types):
"""Decorator describing which AST nodes the completer should apply in."""
def decorator(func):
for c in ast_types:
c.completers.append(func)
return func
return decorator
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, lsp):

View file

@ -20,7 +20,8 @@
import re
from enum import Enum
from . import tokenizer, utils
from . import tokenizer
from .errors import CompilerBugError
from .tokenizer import TokenType
OPENING_TOKENS = ("{", "[")
@ -192,8 +193,8 @@ def format(data, tab_size=2, insert_space=True):
commit_current_line(LineType.COMMENT, newlines_before=newlines)
else:
commit_current_line()
else: # pragma: no cover
raise CompilerBugError()
elif str_item == "(" and (
re.match(r"^([A-Za-z_\-])+\s*\(", current_line) or watch_parentheses

View file

@ -24,8 +24,20 @@ from functools import cached_property
import gi # type: ignore
gi.require_version("GIRepository", "2.0")
from gi.repository import GIRepository # type: ignore
try:
gi.require_version("GIRepository", "3.0")
from gi.repository import GIRepository # type: ignore
_repo = GIRepository.Repository()
except ValueError:
# We can remove this once we can bump the minimum dependencies
# to glib 2.80 and pygobject 3.52
# dependency('glib-2.0', version: '>= 2.80.0')
# dependency('girepository-2.0', version: '>= 2.80.0')
gi.require_version("GIRepository", "2.0")
from gi.repository import GIRepository # type: ignore
_repo = GIRepository.Repository
from . import typelib, xml_reader
from .errors import CompileError, CompilerBugError
@ -42,7 +54,7 @@ def add_typelib_search_path(path: str):
def get_namespace(namespace: str, version: str) -> "Namespace":
search_paths = [*GIRepository.Repository.get_search_path(), *_user_search_paths]
search_paths = [*_repo.get_search_path(), *_user_search_paths]
filename = f"{namespace}-{version}.typelib"
@ -74,7 +86,7 @@ def get_available_namespaces() -> T.List[T.Tuple[str, str]]:
return _available_namespaces
search_paths: list[str] = [
*GIRepository.Repository.get_search_path(),
*_repo.get_search_path(),
*_user_search_paths,
]
@ -455,10 +467,13 @@ class Signature(GirNode):
return result
@cached_property
def return_type(self) -> GirType:
return self.get_containing(Repository)._resolve_type_id(
self.tl.SIGNATURE_RETURN_TYPE
)
def return_type(self) -> T.Optional[GirType]:
if self.tl.SIGNATURE_RETURN_TYPE == 0:
return None
else:
return self.get_containing(Repository)._resolve_type_id(
self.tl.SIGNATURE_RETURN_TYPE
)
class Signal(GirNode):
@ -478,7 +493,10 @@ class Signal(GirNode):
args = ", ".join(
[f"{a.type.full_name} {a.name}" for a in self.gir_signature.args]
)
return f"signal {self.container.full_name}::{self.name} ({args})"
result = f"signal {self.container.full_name}::{self.name} ({args})"
if self.gir_signature.return_type is not None:
result += f" -> {self.gir_signature.return_type.full_name}"
return result
@property
def online_docs(self) -> T.Optional[str]:
@ -890,14 +908,6 @@ class Namespace(GirNode):
if isinstance(entry, Class)
}
@cached_property
def interfaces(self) -> T.Mapping[str, Interface]:
return {
name: entry
for name, entry in self.entries.items()
if isinstance(entry, Interface)
}
def get_type(self, name) -> T.Optional[GirType]:
"""Gets a type (class, interface, enum, etc.) from this namespace."""
return self.entries.get(name)
@ -921,13 +931,8 @@ class Namespace(GirNode):
"""Looks up a type in the scope of this namespace (including in the
namespace's dependencies)."""
if type_name in _BASIC_TYPES:
return _BASIC_TYPES[type_name]()
elif "." in type_name:
ns, name = type_name.split(".", 1)
return self.get_containing(Repository).get_type(name, ns)
else:
return self.get_type(type_name)
ns, name = type_name.split(".", 1)
return self.get_containing(Repository).get_type(name, ns)
@property
def online_docs(self) -> T.Optional[str]:
@ -946,7 +951,7 @@ class Repository(GirNode):
self.includes = {
name: get_namespace(name, version) for name, version in deps
}
except:
except: # pragma: no cover
raise CompilerBugError(f"Failed to load dependencies.")
else:
self.includes = {}
@ -954,12 +959,6 @@ class Repository(GirNode):
def get_type(self, name: str, ns: str) -> T.Optional[GirType]:
return self.lookup_namespace(ns).get_type(name)
def get_type_by_cname(self, name: str) -> T.Optional[GirType]:
for ns in [self.namespace, *self.includes.values()]:
if type := ns.get_type_by_cname(name):
return type
return None
def lookup_namespace(self, ns: str):
"""Finds a namespace among this namespace's dependencies."""
if ns == self.namespace.name:

View file

@ -4,7 +4,6 @@ from .adw_breakpoint import (
AdwBreakpointSetters,
)
from .adw_response_dialog import ExtAdwResponseDialog
from .attributes import BaseAttribute
from .binding import Binding
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
@ -20,7 +19,7 @@ from .expression import (
from .gobject_object import Object, ObjectContent
from .gobject_property import Property
from .gobject_signal import Signal
from .gtk_a11y import ExtAccessibility
from .gtk_a11y import A11yProperty, ExtAccessibility
from .gtk_combo_box_text import ExtComboBoxItems
from .gtk_file_filter import (
Filters,

View file

@ -20,7 +20,6 @@
from ..decompiler import decompile_translatable, truthy
from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue
@ -94,10 +93,6 @@ class ExtAdwResponseDialogResponse(AstNode):
self.value.range.text,
)
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(StringType())
@validate("id")
def unique_in_parent(self):
self.validate_unique_in_parent(

View file

@ -1,32 +0,0 @@
# attributes.py
#
# Copyright 2022 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .common import *
class BaseAttribute(AstNode):
"""A helper class for attribute syntax of the form `name: literal_value;`"""
tag_name: str = ""
attr_name: str = "name"
@property
def name(self):
return self.tokens["name"]

View file

@ -38,10 +38,6 @@ class ExprBase(AstNode):
def type(self) -> T.Optional[GirType]:
raise NotImplementedError()
@property
def type_complete(self) -> bool:
return True
@property
def rhs(self) -> T.Optional["ExprBase"]:
if isinstance(self.parent, Expression):
@ -65,10 +61,6 @@ class Expression(ExprBase):
def type(self) -> T.Optional[GirType]:
return self.last.type
@property
def type_complete(self) -> bool:
return self.last.type_complete
class InfixExpr(ExprBase):
@property
@ -99,15 +91,6 @@ class LiteralExpr(ExprBase):
def type(self) -> T.Optional[GirType]:
return self.literal.value.type
@property
def type_complete(self) -> bool:
from .values import IdentLiteral
if isinstance(self.literal.value, IdentLiteral):
if object := self.context[ScopeCtx].objects.get(self.literal.value.ident):
return not object.gir_class.incomplete
return True
class LookupOp(InfixExpr):
grammar = [".", UseIdent("property")]
@ -211,10 +194,6 @@ class CastExpr(InfixExpr):
def type(self) -> T.Optional[GirType]:
return self.children[TypeName][0].gir_type
@property
def type_complete(self) -> bool:
return True
@validate()
def cast_makes_sense(self):
if self.type is None or self.lhs.type is None:

View file

@ -51,7 +51,7 @@ class Property(AstNode):
@property
def document_symbol(self) -> DocumentSymbol:
if isinstance(self.value, ObjectValue):
if isinstance(self.value, ObjectValue) or self.value is None:
detail = None
else:
detail = self.value.range.text

View file

@ -27,6 +27,7 @@ from .gtkbuilder_template import Template
class SignalFlag(AstNode):
grammar = AnyOf(
UseExact("flag", "swapped"),
UseExact("flag", "not-swapped"),
UseExact("flag", "after"),
)
@ -40,6 +41,27 @@ class SignalFlag(AstNode):
f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag
)
@validate()
def swapped_exclusive(self):
if self.flag in ["swapped", "not-swapped"]:
self.validate_unique_in_parent(
"'swapped' and 'not-swapped' flags cannot be used together",
lambda x: x.flag in ["swapped", "not-swapped"],
)
@validate()
def swapped_unnecessary(self):
if self.flag == "not-swapped" and self.parent.object_id is None:
raise CompileWarning(
"'not-swapped' is the default for handlers that do not specify an object",
actions=[CodeAction("Remove 'not-swapped' flag", "")],
)
elif self.flag == "swapped" and self.parent.object_id is not None:
raise CompileWarning(
"'swapped' is the default for handlers that specify an object",
actions=[CodeAction("Remove 'swapped' flag", "")],
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax Signal")
@ -92,9 +114,17 @@ class Signal(AstNode):
def flags(self) -> T.List[SignalFlag]:
return self.children[SignalFlag]
# Returns True if the "swapped" flag is present, False if "not-swapped" is present, and None if neither are present.
# GtkBuilder's default if swapped is not specified is to not swap the arguments if no object is specified, and to
# swap them if an object is specified.
@property
def is_swapped(self) -> bool:
return any(x.flag == "swapped" for x in self.flags)
def is_swapped(self) -> T.Optional[bool]:
for flag in self.flags:
if flag.flag == "swapped":
return True
elif flag.flag == "not-swapped":
return False
return None
@property
def is_after(self) -> bool:
@ -113,16 +143,17 @@ class Signal(AstNode):
@property
def document_symbol(self) -> DocumentSymbol:
detail = self.ranges["detail_start", "detail_end"]
return DocumentSymbol(
self.full_name,
SymbolKind.Event,
self.range,
self.group.tokens["name"].range,
self.ranges["detail_start", "detail_end"].text,
detail.text if detail is not None else None,
)
def get_reference(self, idx: int) -> T.Optional[LocationLink]:
if idx in self.group.tokens["object"].range:
if self.object_id is not None and idx in self.group.tokens["object"].range:
obj = self.context[ScopeCtx].objects.get(self.object_id)
if obj is not None:
return LocationLink(
@ -194,15 +225,16 @@ class Signal(AstNode):
@decompiler("signal")
def decompile_signal(
ctx, gir, name, handler, swapped="false", after="false", object=None
):
def decompile_signal(ctx, gir, name, handler, swapped=None, after="false", object=None):
object_name = object or ""
name = name.replace("_", "-")
line = f"{name} => ${handler}({object_name})"
if decompile.truthy(swapped):
line += " swapped"
elif swapped is not None:
line += " not-swapped"
if decompile.truthy(after):
line += " after"

View file

@ -19,8 +19,6 @@
import typing as T
from ..decompiler import escape_quote
from .attributes import BaseAttribute
from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type
@ -119,7 +117,7 @@ def _get_docs(gir, name):
return gir_type.doc
class A11yProperty(BaseAttribute):
class A11yProperty(AstNode):
grammar = Statement(
UseIdent("name"),
":",

View file

@ -19,7 +19,6 @@
from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue

View file

@ -50,7 +50,7 @@ class ExtListItemFactory(AstNode):
else:
return self.root.gir.get_type("ListItem", "Gtk")
@validate("template")
@validate("id")
def container_is_builder_list(self):
validate_parent_type(
self,
@ -59,7 +59,7 @@ class ExtListItemFactory(AstNode):
"sub-templates",
)
@validate("template")
@validate("id")
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate template block")
@ -76,7 +76,7 @@ class ExtListItemFactory(AstNode):
f"Only Gtk.ListItem, Gtk.ListHeader, Gtk.ColumnViewRow, or Gtk.ColumnViewCell is allowed as a type here"
)
@validate("template")
@validate("id")
def type_name_upgrade(self):
if self.type_name is None:
raise UpgradeWarning(
@ -103,10 +103,7 @@ class ExtListItemFactory(AstNode):
@property
def action_widgets(self):
"""
The sub-template shouldn't have it`s own actions this is
just hear to satisfy XmlOutput._emit_object_or_template
"""
# The sub-template shouldn't have its own actions, this is just here to satisfy XmlOutput._emit_object_or_template
return None
@docs("id")

View file

@ -59,14 +59,8 @@ class GtkDirective(AstNode):
@property
def gir_namespace(self):
# validate the GTK version first to make sure the more specific error
# message is emitted
self.gtk_version()
if self.tokens["version"] is not None:
return gir.get_namespace("Gtk", self.tokens["version"])
else:
# For better error handling, just assume it's 4.0
return gir.get_namespace("Gtk", "4.0")
# For better error handling, just assume it's 4.0
return gir.get_namespace("Gtk", "4.0")
@docs()
def ref_docs(self):
@ -90,7 +84,7 @@ class Import(AstNode):
@validate("namespace", "version")
def namespace_exists(self):
gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
gir.get_namespace(self.namespace, self.version)
@validate()
def unused(self):
@ -106,7 +100,7 @@ class Import(AstNode):
@property
def gir_namespace(self):
try:
return gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
return gir.get_namespace(self.namespace, self.version)
except CompileError:
return None

View file

@ -169,7 +169,7 @@ class XmlOutput(OutputFormat):
"signal",
name=name,
handler=signal.handler,
swapped=signal.is_swapped or None,
swapped=signal.is_swapped,
after=signal.is_after or None,
object=(
self._object_id(signal, signal.object_id) if signal.object_id else None
@ -366,12 +366,13 @@ class XmlOutput(OutputFormat):
elif isinstance(extension, ExtScaleMarks):
xml.start_tag("marks")
for mark in extension.children:
for mark in extension.marks:
label = mark.label.child if mark.label is not None else None
xml.start_tag(
"mark",
value=mark.value,
position=mark.position,
**self._translated_string_attrs(mark.label and mark.label.child),
**self._translated_string_attrs(label),
)
if mark.label is not None:
xml.put_text(mark.label.string)

View file

@ -40,7 +40,9 @@ class XmlEmitter:
self._tag_stack = []
self._needs_newline = False
def start_tag(self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None]):
def start_tag(
self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None, float]
):
self._indent()
self.result += f"<{tag}"
for key, val in attrs.items():

View file

@ -95,19 +95,11 @@ class ParseGroup:
try:
return self.ast_type(self, children, self.keys, incomplete=self.incomplete)
except TypeError as e:
except TypeError: # pragma: no cover
raise CompilerBugError(
f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace."
)
def __str__(self):
result = str(self.ast_type.__name__)
result += "".join([f"\n{key}: {val}" for key, val in self.keys.items()]) + "\n"
result += "\n".join(
[str(child) for children in self.children.values() for child in children]
)
return result.replace("\n", "\n ")
class ParseContext:
"""Contains the state of the parser."""
@ -265,10 +257,6 @@ class ParseNode:
"""Convenience method for err()."""
return self.err("Expected " + expect)
def warn(self, message) -> "Warning":
"""Causes this ParseNode to emit a warning if it parses successfully."""
return Warning(self, message)
class Err(ParseNode):
"""ParseNode that emits a compile error if it fails to parse."""
@ -290,27 +278,6 @@ class Err(ParseNode):
return True
class Warning(ParseNode):
"""ParseNode that emits a compile warning if it parses successfully."""
def __init__(self, child, message: str):
self.child = to_parse_node(child)
self.message = message
def _parse(self, ctx: ParseContext):
ctx.skip()
start_idx = ctx.index
if self.child.parse(ctx).succeeded():
start_token = ctx.tokens[start_idx]
end_token = ctx.tokens[ctx.index]
ctx.warnings.append(
CompileWarning(self.message, start_token.start, end_token.end)
)
return True
else:
return False
class Fail(ParseNode):
"""ParseNode that emits a compile error if it parses successfully."""

View file

@ -91,7 +91,7 @@ Signal Handlers
.. rst-class:: grammar-block
Signal = <name::ref:`IDENT<Syntax IDENT>`> ('::' <detail::ref:`IDENT<Syntax IDENT>`>)? '=>' '$' <handler::ref:`IDENT<Syntax IDENT>`> '(' <object::ref:`IDENT<Syntax IDENT>`>? ')' (SignalFlag)* ';'
SignalFlag = 'after' | 'swapped'
SignalFlag = 'after' | 'swapped' | 'not-swapped'
Signals are one way to respond to user input (another is `actions <https://docs.gtk.org/gtk4/actions.html>`_, which use the `action-name property <https://docs.gtk.org/gtk4/property.Actionable.action-name.html>`_).
@ -99,6 +99,8 @@ Signals provide a handle for your code to listen to events in the UI. The handle
Optionally, you can provide an object ID to use when connecting the signal.
The ``swapped`` flag is used to swap the order of the object and userdata arguments in C applications. If an object argument is specified, then this is the default behavior, so the ``not-swapped`` flag can be used to prevent the swap.
Example
~~~~~~~
@ -108,7 +110,6 @@ Example
clicked => $on_button_clicked();
}
.. _Syntax Child:
Children

View file

@ -48,12 +48,19 @@ blueprint-compiler works as a meson subproject.
.. code-block:: meson.build
blps = [
# LIST YOUR BLUEPRINT FILES HERE
]
uis = []
foreach blp : blps
uis += blp.replace('.blp', '.ui')
endforeach
blueprints = custom_target('blueprints',
input: files(
# LIST YOUR BLUEPRINT FILES HERE
),
output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
input: blps,
output: uis,
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTDIR@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
)
#. In the same ``meson.build`` file, add this argument to your ``gnome.compile_resources`` command:

View file

@ -0,0 +1,2 @@
using Gtk 4.0;
//comment

View file

@ -0,0 +1,2 @@
using Gtk 4.0;
// comment

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Entry {
margin-bottom: 10.5;
}

View file

@ -0,0 +1 @@
4,18,4,Cannot convert 10.5 to integer

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Label {
notify::
}

View file

@ -0,0 +1,2 @@
5,1,0,Expected a signal detail name
4,9,3,Unexpected tokens

View file

@ -0,0 +1,3 @@
using Gtk 4.0;
int {}

View file

@ -0,0 +1 @@
3,1,3,int is not a class

View file

@ -0,0 +1,7 @@
using Gtk 4.0;
Overlay {
child: my_menu;
}
menu my_menu {}

View file

@ -0,0 +1 @@
4,10,7,Cannot assign Gio.Menu to Gtk.Widget

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
$MyObject obj {
signal1 => $handler() swapped not-swapped;
}

View file

@ -0,0 +1 @@
4,33,11,'swapped' and 'not-swapped' flags cannot be used together

View file

@ -0,0 +1,6 @@
using Gtk 4.0;
$MyObject obj {
signal1 => $handler() not-swapped;
signal2 => $handler(obj) swapped;
}

View file

@ -0,0 +1,2 @@
4,25,11,'not-swapped' is the default for handlers that do not specify an object
5,28,7,'swapped' is the default for handlers that specify an object

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Entry {
margin-bottom: "10";
}

View file

@ -0,0 +1 @@
4,18,4,Cannot convert string to number

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Button {
child: "Click me";
}

View file

@ -0,0 +1 @@
4,10,10,Cannot convert string to Gtk.Widget

View file

@ -0,0 +1,6 @@
using Gtk 4.0;
using Gio 2.0;
Gio.ListStore {
item-type: "Button";
}

View file

@ -0,0 +1 @@
5,14,8,Cannot convert string to GType

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Button {
child: _("Click me");
}

View file

@ -0,0 +1 @@
4,10,13,Cannot convert translated string to Gtk.Widget

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Button {
label: typeof<Button>;
}

View file

@ -0,0 +1 @@
4,10,14,Cannot convert GType to string

View file

@ -0,0 +1 @@
~

View file

@ -0,0 +1 @@
1,1,0,Could not determine what kind of syntax is meant here

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Button btn {
label: bind btn.label sync-create;
}

View file

@ -0,0 +1 @@
4,25,11,'sync-create' is now the default. Use 'no-sync-create' if this is not wanted.

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BuilderListItemFactory {
template {}
}

View file

@ -0,0 +1 @@
4,3,8,Expected type name after 'template' keyword

View file

@ -4,6 +4,7 @@ Box {
visible: bind box2.visible inverted;
orientation: bind box2.orientation;
spacing: bind box2.spacing no-sync-create;
tooltip-text: bind box2.tooltip-text bidirectional;
}
Box box2 {

View file

@ -10,6 +10,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
<property name="visible" bind-source="box2" bind-property="visible" bind-flags="sync-create|invert-boolean"/>
<property name="orientation" bind-source="box2" bind-property="orientation" bind-flags="sync-create"/>
<property name="spacing" bind-source="box2" bind-property="spacing"/>
<property name="tooltip-text" bind-source="box2" bind-property="tooltip-text" bind-flags="sync-create|bidirectional"/>
</object>
<object class="GtkBox" id="box2">
<property name="spacing">6</property>

View file

@ -1,11 +0,0 @@
using Gtk 4.0;
Box {
visible: bind box2.visible inverted;
orientation: bind box2.orientation;
spacing: bind box2.spacing no-sync-create;
}
Box box2 {
spacing: 6;
}

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Button obj {
clicked => $handler(obj) not-swapped;
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkButton" id="obj">
<signal name="clicked" handler="handler" swapped="False" object="obj"/>
</object>
</interface>

View file

@ -46,3 +46,4 @@ class TestFormatter(unittest.TestCase):
self.assert_format_test("in2.blp", "out.blp")
self.assert_format_test("correct1.blp", "correct1.blp")
self.assert_format_test("string_in.blp", "string_out.blp")
self.assert_format_test("comment_in.blp", "comment_out.blp")

View file

@ -28,6 +28,7 @@ gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
from blueprintcompiler import decompiler, parser, tokenizer, utils
from blueprintcompiler.ast_utils import AstNode
from blueprintcompiler.completions import complete
from blueprintcompiler.errors import (
CompileError,
@ -61,11 +62,14 @@ class TestSamples(unittest.TestCase):
except:
pass
def assert_ast_doesnt_crash(self, text, tokens, ast):
def assert_ast_doesnt_crash(self, text, tokens, ast: AstNode):
lsp = LanguageServer()
for i in range(len(text)):
ast.get_docs(i)
for i in range(len(text)):
list(complete(LanguageServer(), ast, tokens, i))
list(complete(lsp, ast, tokens, i))
for i in range(len(text)):
ast.get_reference(i)
ast.get_document_symbols()
def assert_sample(self, name, skip_run=False):
@ -196,6 +200,7 @@ class TestSamples(unittest.TestCase):
"expr_closure_args",
"parseable",
"signal",
"signal_not_swapped",
"template",
"template_binding",
"template_binding_extern",