Compare commits

...

14 commits

Author SHA1 Message Date
kotontrion
c1fbcef6d0 Merge branch blueprint-compiler:main into main 2025-03-02 15:26:42 +00:00
James Westman
404ae76787
Update MAINTENANCE.md 2025-01-17 17:25:21 -06:00
James Westman
04ef0944db
Release v0.16.0 2025-01-17 17:04:52 -06:00
James Westman
aa13c8f5af
Warn about single-quoted translated strings
gettext only recognizes double quoted strings
2025-01-05 14:27:59 -06:00
Alexey Yerin
29e4a56bfc Formatter: Remove trailing whitespace from comments
Fixes #153
2025-01-04 17:17:53 +00:00
James Westman
8c6f8760f7 language: Add expression literals
Add expression literals, so you can set properties of type
Gtk.Expression.
2025-01-04 17:09:57 +00:00
Alexey Yerin
b9f58aeab5 Formatter: Add trailing commas in lists 2025-01-04 16:29:15 +00:00
James Westman
55e5095fba
values: Don't allow translated strings in arrays
Gtk.Builder has no way to translate individual strings in a string
array, so don't allow it in the syntax.
2025-01-03 18:56:24 -06:00
Alexey Yerin
f3faf4b993 LSP: Handle shutdown commands
This fixes the issue with terminal-based editor Helix which asks
language servers to shut down when trying to close the editor. Since
blueprint-compiler's server implementation didn't handle this request,
Helix ended up waiting for a response until timing out after a few
seconds and forcefully terminating the language server process.

Besides fixing Helix, this patch should also make user-initiated server
restarts more robust.
2025-01-03 22:49:36 +03: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
94 changed files with 850 additions and 232 deletions

View file

@ -8,7 +8,7 @@ in the NEWS file.
3. Make a new commit with just these two changes. Use `Release v{version}` as the commit message. Tag the commit as `v{version}` and push the tag. 3. Make a new commit with just these two changes. Use `Release v{version}` as the commit message. Tag the commit as `v{version}` and push the tag.
4. Create a "Post-release version bump" commit. 4. Create a "Post-release version bump" commit.
5. Go to the Releases page in GitLab and create a new release from the tag. 5. Go to the Releases page in GitLab and create a new release from the tag.
6. Announce the release through relevant channels (Twitter, TWIG, etc.) 6. Announce the release through relevant channels (Mastodon, TWIG, etc.)
## Related projects ## Related projects

32
NEWS.md
View file

@ -1,3 +1,35 @@
# v0.16.0
## Added
- Added more "go to reference" implementations in the language server
- Added semantic token support for flag members in the language server
- Added property documentation to the hover tooltip for notify signals
- The language server now shows relevant sections of the reference documentation when hovering over keywords and symbols
- Added `not-swapped` flag to signal handlers, which may be needed for signal handlers that specify an object
- Added expression literals, which allow you to specify a Gtk.Expression property (as opposed to the existing expression support, which is for property bindings)
## Changed
- The formatter adds trailing commas to lists (Alexey Yerin)
- The formatter removes trailing whitespace from comments (Alexey Yerin)
- Autocompleting a commonly translated property automatically adds the `_("")` syntax
- Marking a single-quoted string as translatable now generates a warning, since gettext does not recognize it when using the configuration recommended in the blueprint documentation
## Fixed
- Added support for libgirepository-2.0 so that blueprint doesn't crash due to import conflicts on newer versions of PyGObject (Jordan Petridis)
- Fixed a bug when decompiling/porting files with enum values
- Fixed several issues where tests would fail with versions of GTK that added new deprecations
- Addressed a problem with the language server protocol in some editors (Luoyayu)
- Fixed an issue where the compiler would crash instead of reporting compiler errors
- Fixed a crash in the language server that occurred when a detailed signal (e.g. `notify::*`) was not complete
- The language server now properly implements the shutdown command, fixing support for some editors and improving robustness when restarting (Alexey Yerin)
- Marking a string in an array as translatable now generates an error, since it doesn't work
-
## Documentation
- Added mention of `null` in the Literal Values section
- Add apps to Built with Blueprint section (Benedek Dévényi, Vladimir Vaskov)
- Corrected and updated many parts of the documentation
# v0.14.0 # v0.14.0
## Added ## Added

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 yield e
if e.fatal: if e.fatal:
return return
except MultipleErrors as e:
for error in e.errors:
yield error
if error.fatal:
return
for child in self.children: for child in self.children:
yield from child._get_errors() yield from child._get_errors()
@ -249,14 +254,7 @@ def validate(
if skip_incomplete and self.incomplete: if skip_incomplete and self.incomplete:
return return
try: def fill_error(e: CompileError):
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
if e.range is None: if e.range is None:
e.range = ( e.range = (
Range.join( Range.join(
@ -266,8 +264,26 @@ def validate(
or self.range 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 # Re-raise the exception
raise e raise e
except MultipleErrors as e:
if self.incomplete:
return
for error in e.errors:
fill_error(error)
raise e
inner._validator = True inner._validator = True
return inner return inner

View file

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

View file

@ -20,7 +20,8 @@
import re import re
from enum import Enum from enum import Enum
from . import tokenizer, utils from . import tokenizer
from .errors import CompilerBugError
from .tokenizer import TokenType from .tokenizer import TokenType
OPENING_TOKENS = ("{", "[") OPENING_TOKENS = ("{", "[")
@ -145,8 +146,10 @@ def format(data, tab_size=2, insert_space=True):
is_child_type = False is_child_type = False
elif str_item in CLOSING_TOKENS: elif str_item in CLOSING_TOKENS:
if str_item == "]" and last_not_whitespace != ",": if str_item == "]" and str(last_not_whitespace) != "[":
current_line = current_line[:-1] current_line = current_line[:-1]
if str(last_not_whitespace) != ",":
current_line += ","
commit_current_line() commit_current_line()
current_line = "]" current_line = "]"
elif str(last_not_whitespace) in OPENING_TOKENS: elif str(last_not_whitespace) in OPENING_TOKENS:
@ -190,10 +193,13 @@ def format(data, tab_size=2, insert_space=True):
elif prev_line_type in require_extra_newline: elif prev_line_type in require_extra_newline:
newlines = 2 newlines = 2
current_line = "\n".join(
[line.rstrip() for line in current_line.split("\n")]
)
commit_current_line(LineType.COMMENT, newlines_before=newlines) commit_current_line(LineType.COMMENT, newlines_before=newlines)
else: else: # pragma: no cover
commit_current_line() raise CompilerBugError()
elif str_item == "(" and ( elif str_item == "(" and (
re.match(r"^([A-Za-z_\-])+\s*\(", current_line) or watch_parentheses 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 import gi # type: ignore
gi.require_version("GIRepository", "2.0") try:
from gi.repository import GIRepository # type: ignore 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 . import typelib, xml_reader
from .errors import CompileError, CompilerBugError from .errors import CompileError, CompilerBugError
@ -42,7 +54,7 @@ def add_typelib_search_path(path: str):
def get_namespace(namespace: str, version: str) -> "Namespace": 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" filename = f"{namespace}-{version}.typelib"
@ -74,7 +86,7 @@ def get_available_namespaces() -> T.List[T.Tuple[str, str]]:
return _available_namespaces return _available_namespaces
search_paths: list[str] = [ search_paths: list[str] = [
*GIRepository.Repository.get_search_path(), *_repo.get_search_path(),
*_user_search_paths, *_user_search_paths,
] ]
@ -455,10 +467,13 @@ class Signature(GirNode):
return result return result
@cached_property @cached_property
def return_type(self) -> GirType: def return_type(self) -> T.Optional[GirType]:
return self.get_containing(Repository)._resolve_type_id( if self.tl.SIGNATURE_RETURN_TYPE == 0:
self.tl.SIGNATURE_RETURN_TYPE return None
) else:
return self.get_containing(Repository)._resolve_type_id(
self.tl.SIGNATURE_RETURN_TYPE
)
class Signal(GirNode): class Signal(GirNode):
@ -478,7 +493,10 @@ class Signal(GirNode):
args = ", ".join( args = ", ".join(
[f"{a.type.full_name} {a.name}" for a in self.gir_signature.args] [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 @property
def online_docs(self) -> T.Optional[str]: def online_docs(self) -> T.Optional[str]:
@ -890,14 +908,6 @@ class Namespace(GirNode):
if isinstance(entry, Class) 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]: def get_type(self, name) -> T.Optional[GirType]:
"""Gets a type (class, interface, enum, etc.) from this namespace.""" """Gets a type (class, interface, enum, etc.) from this namespace."""
return self.entries.get(name) 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 """Looks up a type in the scope of this namespace (including in the
namespace's dependencies).""" namespace's dependencies)."""
if type_name in _BASIC_TYPES: ns, name = type_name.split(".", 1)
return _BASIC_TYPES[type_name]() return self.get_containing(Repository).get_type(name, ns)
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)
@property @property
def online_docs(self) -> T.Optional[str]: def online_docs(self) -> T.Optional[str]:
@ -946,7 +951,7 @@ class Repository(GirNode):
self.includes = { self.includes = {
name: get_namespace(name, version) for name, version in deps name: get_namespace(name, version) for name, version in deps
} }
except: except: # pragma: no cover
raise CompilerBugError(f"Failed to load dependencies.") raise CompilerBugError(f"Failed to load dependencies.")
else: else:
self.includes = {} self.includes = {}
@ -954,12 +959,6 @@ class Repository(GirNode):
def get_type(self, name: str, ns: str) -> T.Optional[GirType]: def get_type(self, name: str, ns: str) -> T.Optional[GirType]:
return self.lookup_namespace(ns).get_type(name) 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): def lookup_namespace(self, ns: str):
"""Finds a namespace among this namespace's dependencies.""" """Finds a namespace among this namespace's dependencies."""
if ns == self.namespace.name: if ns == self.namespace.name:

View file

@ -4,7 +4,6 @@ from .adw_breakpoint import (
AdwBreakpointSetters, AdwBreakpointSetters,
) )
from .adw_response_dialog import ExtAdwResponseDialog from .adw_response_dialog import ExtAdwResponseDialog
from .attributes import BaseAttribute
from .binding import Binding from .binding import Binding
from .common import * from .common import *
from .contexts import ScopeCtx, ValueTypeCtx from .contexts import ScopeCtx, ValueTypeCtx
@ -20,7 +19,7 @@ from .expression import (
from .gobject_object import Object, ObjectContent from .gobject_object import Object, ObjectContent
from .gobject_property import Property from .gobject_property import Property
from .gobject_signal import Signal 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_combo_box_text import ExtComboBoxItems
from .gtk_file_filter import ( from .gtk_file_filter import (
Filters, Filters,
@ -42,6 +41,7 @@ from .types import ClassName
from .ui import UI from .ui import UI
from .values import ( from .values import (
ArrayValue, ArrayValue,
ExprValue,
Flag, Flag,
Flags, Flags,
IdentLiteral, IdentLiteral,

View file

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

@ -79,3 +79,9 @@ class ScopeCtx:
for child in node.children: for child in node.children:
if child.context[ScopeCtx] is self: if child.context[ScopeCtx] is self:
yield from self._iter_recursive(child) yield from self._iter_recursive(child)
@dataclass
class ExprValueCtx:
"""Indicates that the context is an expression literal, where the
"item" keyword may be used."""

View file

@ -38,10 +38,6 @@ class ExprBase(AstNode):
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
raise NotImplementedError() raise NotImplementedError()
@property
def type_complete(self) -> bool:
return True
@property @property
def rhs(self) -> T.Optional["ExprBase"]: def rhs(self) -> T.Optional["ExprBase"]:
if isinstance(self.parent, Expression): if isinstance(self.parent, Expression):
@ -65,10 +61,6 @@ class Expression(ExprBase):
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
return self.last.type return self.last.type
@property
def type_complete(self) -> bool:
return self.last.type_complete
class InfixExpr(ExprBase): class InfixExpr(ExprBase):
@property @property
@ -89,6 +81,16 @@ class LiteralExpr(ExprBase):
or self.root.is_legacy_template(self.literal.value.ident) or self.root.is_legacy_template(self.literal.value.ident)
) )
@property
def is_this(self) -> bool:
from .values import IdentLiteral
return (
not self.is_object
and isinstance(self.literal.value, IdentLiteral)
and self.literal.value.ident == "item"
)
@property @property
def literal(self): def literal(self):
from .values import Literal from .values import Literal
@ -99,14 +101,14 @@ class LiteralExpr(ExprBase):
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
return self.literal.value.type return self.literal.value.type
@property @validate()
def type_complete(self) -> bool: def item_validations(self):
from .values import IdentLiteral if self.is_this:
if not isinstance(self.rhs, CastExpr):
raise CompileError('"item" must be cast to its object type')
if isinstance(self.literal.value, IdentLiteral): if not isinstance(self.rhs.rhs, LookupOp):
if object := self.context[ScopeCtx].objects.get(self.literal.value.ident): raise CompileError('"item" can only be used for looking up properties')
return not object.gir_class.incomplete
return True
class LookupOp(InfixExpr): class LookupOp(InfixExpr):
@ -211,10 +213,6 @@ class CastExpr(InfixExpr):
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
return self.children[TypeName][0].gir_type return self.children[TypeName][0].gir_type
@property
def type_complete(self) -> bool:
return True
@validate() @validate()
def cast_makes_sense(self): def cast_makes_sense(self):
if self.type is None or self.lhs.type is None: if self.type is None or self.lhs.type is None:
@ -306,6 +304,9 @@ expr.children = [
def decompile_lookup( def decompile_lookup(
ctx: DecompileCtx, gir: gir.GirContext, cdata: str, name: str, type: str ctx: DecompileCtx, gir: gir.GirContext, cdata: str, name: str, type: str
): ):
if ctx.parent_node is not None and ctx.parent_node.tag == "property":
ctx.print("expr ")
if t := ctx.type_by_cname(type): if t := ctx.type_by_cname(type):
type = decompile.full_name(t) type = decompile.full_name(t)
else: else:
@ -325,6 +326,8 @@ def decompile_lookup(
if constant is not None: if constant is not None:
if constant == ctx.template_class: if constant == ctx.template_class:
ctx.print("template." + name) ctx.print("template." + name)
elif constant == "":
ctx.print("item as <" + type + ">." + name)
else: else:
ctx.print(constant + "." + name) ctx.print(constant + "." + name)
return return
@ -339,6 +342,9 @@ def decompile_lookup(
def decompile_constant( def decompile_constant(
ctx: DecompileCtx, gir: gir.GirContext, cdata: str, type: T.Optional[str] = None ctx: DecompileCtx, gir: gir.GirContext, cdata: str, type: T.Optional[str] = None
): ):
if ctx.parent_node is not None and ctx.parent_node.tag == "property":
ctx.print("expr ")
if type is None: if type is None:
if cdata == ctx.template_class: if cdata == ctx.template_class:
ctx.print("template") ctx.print("template")
@ -351,6 +357,9 @@ def decompile_constant(
@decompiler("closure", skip_children=True) @decompiler("closure", skip_children=True)
def decompile_closure(ctx: DecompileCtx, gir: gir.GirContext, function: str, type: str): def decompile_closure(ctx: DecompileCtx, gir: gir.GirContext, function: str, type: str):
if ctx.parent_node is not None and ctx.parent_node.tag == "property":
ctx.print("expr ")
if t := ctx.type_by_cname(type): if t := ctx.type_by_cname(type):
type = decompile.full_name(t) type = decompile.full_name(t)
else: else:

View file

@ -28,7 +28,18 @@ from .common import *
from .response_id import ExtResponse from .response_id import ExtResponse
from .types import ClassName, ConcreteClassName from .types import ClassName, ConcreteClassName
RESERVED_IDS = {"this", "self", "template", "true", "false", "null", "none"} RESERVED_IDS = {
"this",
"self",
"template",
"true",
"false",
"null",
"none",
"item",
"expr",
"typeof",
}
class ObjectContent(AstNode): class ObjectContent(AstNode):

View file

@ -21,13 +21,12 @@
from .binding import Binding from .binding import Binding
from .common import * from .common import *
from .contexts import ValueTypeCtx from .contexts import ValueTypeCtx
from .gtkbuilder_template import Template from .values import ArrayValue, ExprValue, ObjectValue, Value
from .values import ArrayValue, ObjectValue, Value
class Property(AstNode): class Property(AstNode):
grammar = Statement( grammar = Statement(
UseIdent("name"), ":", AnyOf(Binding, ObjectValue, Value, ArrayValue) UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue)
) )
@property @property
@ -35,7 +34,7 @@ class Property(AstNode):
return self.tokens["name"] return self.tokens["name"]
@property @property
def value(self) -> T.Union[Binding, ObjectValue, Value, ArrayValue]: def value(self) -> T.Union[Binding, ExprValue, ObjectValue, Value, ArrayValue]:
return self.children[0] return self.children[0]
@property @property
@ -51,7 +50,7 @@ class Property(AstNode):
@property @property
def document_symbol(self) -> DocumentSymbol: def document_symbol(self) -> DocumentSymbol:
if isinstance(self.value, ObjectValue): if isinstance(self.value, ObjectValue) or self.value is None:
detail = None detail = None
else: else:
detail = self.value.range.text detail = self.value.range.text

View file

@ -27,6 +27,7 @@ from .gtkbuilder_template import Template
class SignalFlag(AstNode): class SignalFlag(AstNode):
grammar = AnyOf( grammar = AnyOf(
UseExact("flag", "swapped"), UseExact("flag", "swapped"),
UseExact("flag", "not-swapped"),
UseExact("flag", "after"), UseExact("flag", "after"),
) )
@ -40,6 +41,27 @@ class SignalFlag(AstNode):
f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag 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() @docs()
def ref_docs(self): def ref_docs(self):
return get_docs_section("Syntax Signal") return get_docs_section("Syntax Signal")
@ -92,9 +114,17 @@ class Signal(AstNode):
def flags(self) -> T.List[SignalFlag]: def flags(self) -> T.List[SignalFlag]:
return self.children[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 @property
def is_swapped(self) -> bool: def is_swapped(self) -> T.Optional[bool]:
return any(x.flag == "swapped" for x in self.flags) for flag in self.flags:
if flag.flag == "swapped":
return True
elif flag.flag == "not-swapped":
return False
return None
@property @property
def is_after(self) -> bool: def is_after(self) -> bool:
@ -113,16 +143,17 @@ class Signal(AstNode):
@property @property
def document_symbol(self) -> DocumentSymbol: def document_symbol(self) -> DocumentSymbol:
detail = self.ranges["detail_start", "detail_end"]
return DocumentSymbol( return DocumentSymbol(
self.full_name, self.full_name,
SymbolKind.Event, SymbolKind.Event,
self.range, self.range,
self.group.tokens["name"].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]: 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) obj = self.context[ScopeCtx].objects.get(self.object_id)
if obj is not None: if obj is not None:
return LocationLink( return LocationLink(
@ -194,15 +225,16 @@ class Signal(AstNode):
@decompiler("signal") @decompiler("signal")
def decompile_signal( def decompile_signal(ctx, gir, name, handler, swapped=None, after="false", object=None):
ctx, gir, name, handler, swapped="false", after="false", object=None
):
object_name = object or "" object_name = object or ""
name = name.replace("_", "-") name = name.replace("_", "-")
line = f"{name} => ${handler}({object_name})" line = f"{name} => ${handler}({object_name})"
if decompile.truthy(swapped): if decompile.truthy(swapped):
line += " swapped" line += " swapped"
elif swapped is not None:
line += " not-swapped"
if decompile.truthy(after): if decompile.truthy(after):
line += " after" line += " after"

View file

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

View file

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

View file

@ -50,7 +50,7 @@ class ExtListItemFactory(AstNode):
else: else:
return self.root.gir.get_type("ListItem", "Gtk") return self.root.gir.get_type("ListItem", "Gtk")
@validate("template") @validate("id")
def container_is_builder_list(self): def container_is_builder_list(self):
validate_parent_type( validate_parent_type(
self, self,
@ -59,7 +59,7 @@ class ExtListItemFactory(AstNode):
"sub-templates", "sub-templates",
) )
@validate("template") @validate("id")
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate template block") 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" 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): def type_name_upgrade(self):
if self.type_name is None: if self.type_name is None:
raise UpgradeWarning( raise UpgradeWarning(
@ -103,10 +103,7 @@ class ExtListItemFactory(AstNode):
@property @property
def action_widgets(self): def action_widgets(self):
""" # The sub-template shouldn't have its own actions, this is just here to satisfy XmlOutput._emit_object_or_template
The sub-template shouldn't have it`s own actions this is
just hear to satisfy XmlOutput._emit_object_or_template
"""
return None return None
@docs("id") @docs("id")

View file

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

View file

@ -23,7 +23,8 @@ from blueprintcompiler.gir import ArrayType
from blueprintcompiler.lsp_utils import SemanticToken from blueprintcompiler.lsp_utils import SemanticToken
from .common import * from .common import *
from .contexts import ScopeCtx, ValueTypeCtx from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx
from .expression import Expression
from .gobject_object import Object from .gobject_object import Object
from .types import TypeName from .types import TypeName
@ -57,6 +58,19 @@ class Translated(AstNode):
f"Cannot convert translated string to {expected_type.full_name}" f"Cannot convert translated string to {expected_type.full_name}"
) )
@validate("context")
def context_double_quoted(self):
if self.translate_context is None:
return
if not str(self.group.tokens["context"]).startswith('"'):
raise CompileWarning("gettext may not recognize single-quoted strings")
@validate("string")
def string_double_quoted(self):
if not str(self.group.tokens["string"]).startswith('"'):
raise CompileWarning("gettext may not recognize single-quoted strings")
@docs() @docs()
def ref_docs(self): def ref_docs(self):
return get_docs_section("Syntax Translated") return get_docs_section("Syntax Translated")
@ -319,7 +333,12 @@ class IdentLiteral(AstNode):
if self.ident == "null": if self.ident == "null":
if not self.context[ValueTypeCtx].allow_null: if not self.context[ValueTypeCtx].allow_null:
raise CompileError("null is not permitted here") raise CompileError("null is not permitted here")
else: elif self.ident == "item":
if not self.context[ExprValueCtx]:
raise CompileError(
'"item" can only be used in an expression literal'
)
elif self.ident not in ["true", "false"]:
raise CompileError( raise CompileError(
f"Could not find object with ID {self.ident}", f"Could not find object with ID {self.ident}",
did_you_mean=( did_you_mean=(
@ -407,6 +426,35 @@ class ObjectValue(AstNode):
) )
class ExprValue(AstNode):
grammar = [Keyword("expr"), Expression]
@property
def expression(self) -> Expression:
return self.children[Expression][0]
@validate("expr")
def validate_for_type(self) -> None:
expected_type = self.parent.context[ValueTypeCtx].value_type
expr_type = self.root.gir.get_type("Expression", "Gtk")
if expected_type is not None and not expected_type.assignable_to(expr_type):
raise CompileError(
f"Cannot convert Gtk.Expression to {expected_type.full_name}"
)
@docs("expr")
def ref_docs(self):
return get_docs_section("Syntax ExprValue")
@context(ExprValueCtx)
def expr_literal(self):
return ExprValueCtx()
@context(ValueTypeCtx)
def value_type(self):
return ValueTypeCtx(None, must_infer_type=True)
class Value(AstNode): class Value(AstNode):
grammar = AnyOf(Translated, Flags, Literal) grammar = AnyOf(Translated, Flags, Literal)
@ -452,6 +500,14 @@ class ArrayValue(AstNode):
range=quoted_literal.range, range=quoted_literal.range,
) )
) )
elif isinstance(value.child, Translated):
errors.append(
CompileError(
"Arrays can't contain translated strings",
range=value.child.range,
)
)
if len(errors) > 0: if len(errors) > 0:
raise MultipleErrors(errors) raise MultipleErrors(errors)

View file

@ -118,6 +118,7 @@ class LanguageServer:
self.client_capabilities = {} self.client_capabilities = {}
self.client_supports_completion_choice = False self.client_supports_completion_choice = False
self._open_files: T.Dict[str, OpenFile] = {} self._open_files: T.Dict[str, OpenFile] = {}
self._exited = False
def run(self): def run(self):
# Read <doc> tags from gir files. During normal compilation these are # Read <doc> tags from gir files. During normal compilation these are
@ -125,7 +126,7 @@ class LanguageServer:
xml_reader.PARSE_GIR.add("doc") xml_reader.PARSE_GIR.add("doc")
try: try:
while True: while not self._exited:
line = "" line = ""
content_len = -1 content_len = -1
while content_len == -1 or (line != "\n" and line != "\r\n"): while content_len == -1 or (line != "\n" and line != "\r\n"):
@ -221,6 +222,14 @@ class LanguageServer:
}, },
) )
@command("shutdown")
def shutdown(self, id, params):
self._send_response(id, None)
@command("exit")
def exit(self, id, params):
self._exited = True
@command("textDocument/didOpen") @command("textDocument/didOpen")
def didOpen(self, id, params): def didOpen(self, id, params):
doc = params.get("textDocument") doc = params.get("textDocument")

View file

@ -134,6 +134,11 @@ class XmlOutput(OutputFormat):
self._emit_expression(value.expression, xml) self._emit_expression(value.expression, xml)
xml.end_tag() xml.end_tag()
elif isinstance(value, ExprValue):
xml.start_tag("property", **props)
self._emit_expression(value.expression, xml)
xml.end_tag()
elif isinstance(value, ObjectValue): elif isinstance(value, ObjectValue):
xml.start_tag("property", **props) xml.start_tag("property", **props)
self._emit_object(value.object, xml) self._emit_object(value.object, xml)
@ -169,7 +174,7 @@ class XmlOutput(OutputFormat):
"signal", "signal",
name=name, name=name,
handler=signal.handler, handler=signal.handler,
swapped=signal.is_swapped or None, swapped=signal.is_swapped,
after=signal.is_after or None, after=signal.is_after or None,
object=( object=(
self._object_id(signal, signal.object_id) if signal.object_id else None self._object_id(signal, signal.object_id) if signal.object_id else None
@ -218,12 +223,6 @@ class XmlOutput(OutputFormat):
xml.put_text( xml.put_text(
"|".join([str(flag.value or flag.name) for flag in value.child.flags]) "|".join([str(flag.value or flag.name) for flag in value.child.flags])
) )
elif isinstance(value.child, Translated):
raise CompilerBugError("translated values must be handled in the parent")
elif isinstance(value.child, TypeLiteral):
xml.put_text(value.child.type_name.glib_type_name)
elif isinstance(value.child, ObjectValue):
self._emit_object(value.child.object, xml)
else: else:
raise CompilerBugError() raise CompilerBugError()
@ -245,6 +244,9 @@ class XmlOutput(OutputFormat):
raise CompilerBugError() raise CompilerBugError()
def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter): def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter):
if expr.is_this:
return
if expr.is_object: if expr.is_object:
xml.start_tag("constant") xml.start_tag("constant")
else: else:
@ -366,12 +368,13 @@ class XmlOutput(OutputFormat):
elif isinstance(extension, ExtScaleMarks): elif isinstance(extension, ExtScaleMarks):
xml.start_tag("marks") 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( xml.start_tag(
"mark", "mark",
value=mark.value, value=mark.value,
position=mark.position, position=mark.position,
**self._translated_string_attrs(mark.label and mark.label.child), **self._translated_string_attrs(label),
) )
if mark.label is not None: if mark.label is not None:
xml.put_text(mark.label.string) xml.put_text(mark.label.string)

View file

@ -40,7 +40,9 @@ class XmlEmitter:
self._tag_stack = [] self._tag_stack = []
self._needs_newline = False 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._indent()
self.result += f"<{tag}" self.result += f"<{tag}"
for key, val in attrs.items(): for key, val in attrs.items():

View file

@ -95,19 +95,11 @@ class ParseGroup:
try: try:
return self.ast_type(self, children, self.keys, incomplete=self.incomplete) return self.ast_type(self, children, self.keys, incomplete=self.incomplete)
except TypeError as e: except TypeError: # pragma: no cover
raise CompilerBugError( raise CompilerBugError(
f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace." 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: class ParseContext:
"""Contains the state of the parser.""" """Contains the state of the parser."""
@ -265,10 +257,6 @@ class ParseNode:
"""Convenience method for err().""" """Convenience method for err()."""
return self.err("Expected " + expect) 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): class Err(ParseNode):
"""ParseNode that emits a compile error if it fails to parse.""" """ParseNode that emits a compile error if it fails to parse."""
@ -290,27 +278,6 @@ class Err(ParseNode):
return True 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): class Fail(ParseNode):
"""ParseNode that emits a compile error if it parses successfully.""" """ParseNode that emits a compile error if it parses successfully."""

View file

@ -17,7 +17,7 @@ a module in your flatpak manifest:
{ {
"type": "git", "type": "git",
"url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler",
"tag": "v0.14.0" "tag": "v0.16.0"
} }
] ]
} }

View file

@ -42,8 +42,8 @@ Expressions are composed of property lookups and/or closures. Property lookups a
.. _Syntax LookupExpression: .. _Syntax LookupExpression:
Lookup Expressions Lookups
------------------ -------
.. rst-class:: grammar-block .. rst-class:: grammar-block
@ -56,8 +56,8 @@ The type of a property expression is the type of the property it refers to.
.. _Syntax ClosureExpression: .. _Syntax ClosureExpression:
Closure Expressions Closures
------------------- --------
.. rst-class:: grammar-block .. rst-class:: grammar-block
@ -72,8 +72,8 @@ Blueprint doesn't know the closure's return type, so closure expressions must be
.. _Syntax CastExpression: .. _Syntax CastExpression:
Cast Expressions Casts
---------------- -----
.. rst-class:: grammar-block .. rst-class:: grammar-block
@ -81,7 +81,32 @@ Cast Expressions
Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. This is necessary for closures and for properties of application-defined types. Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. This is necessary for closures and for properties of application-defined types.
Example
~~~~~~~
.. code-block:: blueprint .. code-block:: blueprint
// Cast the result of the closure so blueprint knows it's a string // Cast the result of the closure so blueprint knows it's a string
label: bind $my_closure() as <string> label: bind $format_bytes(template.file-size) as <string>
.. _Syntax ExprValue:
Expression Values
-----------------
.. rst-class:: grammar-block
ExprValue = 'expr' :ref:`Expression<Syntax Expression>`
Some APIs take *an expression itself*--not its result--as a property value. For example, `Gtk.BoolFilter <https://docs.gtk.org/gtk4/class.BoolFilter.html>`_ has an ``expression`` property of type `Gtk.Expression <https://docs.gtk.org/gtk4/class.Expression.html>`_. This expression is evaluated for every item in a list model to determine whether the item should be filtered.
To define an expression for such a property, use ``expr`` instead of ``bind``. Inside the expression, you can use the ``item`` keyword to refer to the item being evaluated. You must cast the item to the correct type using the ``as`` keyword, and you can only use ``item`` in a property lookup--you may not pass it to a closure.
Example
~~~~~~~
.. code-block:: blueprint
BoolFilter {
expression: expr item as <$UserAccount>.active;
}

View file

@ -58,7 +58,7 @@ Properties
.. rst-class:: grammar-block .. rst-class:: grammar-block
Property = <name::ref:`IDENT<Syntax IDENT>`> ':' ( :ref:`Binding<Syntax Binding>` | :ref:`ObjectValue<Syntax ObjectValue>` | :ref:`Value<Syntax Value>` ) ';' Property = <name::ref:`IDENT<Syntax IDENT>`> ':' ( :ref:`Binding<Syntax Binding>` | :ref:`ExprValue<Syntax ExprValue>` | :ref:`ObjectValue<Syntax ObjectValue>` | :ref:`Value<Syntax Value>` ) ';'
Properties specify the details of each object, like a label's text, an image's icon name, or the margins on a container. Properties specify the details of each object, like a label's text, an image's icon name, or the margins on a container.
@ -91,7 +91,7 @@ Signal Handlers
.. rst-class:: grammar-block .. 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)* ';' 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>`_). 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. 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 Example
~~~~~~~ ~~~~~~~
@ -108,7 +110,6 @@ Example
clicked => $on_button_clicked(); clicked => $on_button_clicked();
} }
.. _Syntax Child: .. _Syntax Child:
Children Children

View file

@ -24,6 +24,8 @@ If you're using Meson's `i18n module <https://mesonbuild.com/i18n-module.html#i1
i18n.gettext('package name', preset: 'glib') i18n.gettext('package name', preset: 'glib')
You must use double quotes for the translated strings in order for gettext to recognize them. Newer versions of blueprint will warn you if you use single quotes.
Contexts Contexts
-------- --------

View file

@ -1,5 +1,5 @@
project('blueprint-compiler', project('blueprint-compiler',
version: '0.14.0', version: '0.16.0',
) )
prefix = get_option('prefix') prefix = get_option('prefix')

View file

@ -0,0 +1,4 @@
using Gtk 4.0;
//comment
// Trailing whitespace:
//

View file

@ -0,0 +1,4 @@
using Gtk 4.0;
// comment
// Trailing whitespace:
//

View file

@ -0,0 +1,21 @@
using Gtk 4.0;
Box {
styles []
}
Box {
styles ["a"]
}
Box {
styles ["a",]
}
Box {
styles ["a", "b"]
}
Box {
styles ["a", "b",]
}

View file

@ -0,0 +1,31 @@
using Gtk 4.0;
Box {
styles []
}
Box {
styles [
"a",
]
}
Box {
styles [
"a",
]
}
Box {
styles [
"a",
"b",
]
}
Box {
styles [
"a",
"b",
]
}

View file

@ -11,7 +11,7 @@ Overlay {
notify::icon-name => $on_icon_name_changed(label) swapped; notify::icon-name => $on_icon_name_changed(label) swapped;
styles [ styles [
"destructive" "destructive",
] ]
} }

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr item.visible;
}

View file

@ -0,0 +1 @@
4,20,4,"item" must be cast to its object type

View file

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

View file

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

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr $closure(item as <Entry>) as <bool>;
}

View file

@ -0,0 +1 @@
4,29,4,"item" can only be used for looking up properties

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr item as <Label>;
}

View file

@ -0,0 +1 @@
4,20,4,"item" can only be used for looking up properties

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;
Label {
label: _('Hello, World!');
}

View file

@ -0,0 +1 @@
4,12,15,gettext may not recognize single-quoted strings

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,7 @@
using Gtk 4.0;
StringList {
strings: [
_("Test")
];
}

View file

@ -0,0 +1 @@
5,5,9,Arrays can't contain translated strings

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

@ -0,0 +1,9 @@
using Gtk 4.0;
BoolFilter filter1 {
expression: expr true;
}
BoolFilter filter2 {
expression: bind filter1.expression;
}

View file

@ -0,0 +1,17 @@
<?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="GtkBoolFilter" id="filter1">
<property name="expression">
<constant type="gboolean">true</constant>
</property>
</object>
<object class="GtkBoolFilter" id="filter2">
<property name="expression" bind-source="filter1" bind-property="expression" bind-flags="sync-create"/>
</object>
</interface>

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Label {
label: bind "Hello, world!";
}

View file

@ -0,0 +1,14 @@
<?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="GtkLabel">
<binding name="label">
<constant type="gchararray">Hello, world!</constant>
</binding>
</object>
</interface>

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr item as <Entry>.visible;
}

View file

@ -0,0 +1,14 @@
<?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="GtkBoolFilter">
<property name="expression">
<lookup name="visible" type="GtkEntry"></lookup>
</property>
</object>
</interface>

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr $closure(item as <Entry>.visible) as <bool>;
}

View file

@ -0,0 +1,16 @@
<?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="GtkBoolFilter">
<property name="expression">
<closure function="closure" type="gboolean">
<lookup name="visible" type="GtkEntry"></lookup>
</closure>
</property>
</object>
</interface>

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr true;
}

View file

@ -0,0 +1,14 @@
<?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="GtkBoolFilter">
<property name="expression">
<constant type="gboolean">true</constant>
</property>
</object>
</interface>

View file

@ -4,6 +4,7 @@ Box {
visible: bind box2.visible inverted; visible: bind box2.visible inverted;
orientation: bind box2.orientation; orientation: bind box2.orientation;
spacing: bind box2.spacing no-sync-create; spacing: bind box2.spacing no-sync-create;
tooltip-text: bind box2.tooltip-text bidirectional;
} }
Box box2 { 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="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="orientation" bind-source="box2" bind-property="orientation" bind-flags="sync-create"/>
<property name="spacing" bind-source="box2" bind-property="spacing"/> <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>
<object class="GtkBox" id="box2"> <object class="GtkBox" id="box2">
<property name="spacing">6</property> <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

@ -5,6 +5,6 @@ AboutDialog about {
authors: [ authors: [
"Jane doe <jane-doe@email.com>", "Jane doe <jane-doe@email.com>",
"Jhonny D <jd@email.com>" "Jhonny D <jd@email.com>",
]; ];
} }

View file

@ -3,6 +3,6 @@ using Gtk 4.0;
Label { Label {
styles [ styles [
"class-1", "class-1",
"class-2" "class-2",
] ]
} }

View file

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

View file

@ -28,6 +28,7 @@ gi.require_version("Gtk", "4.0")
from gi.repository import Gtk from gi.repository import Gtk
from blueprintcompiler import decompiler, parser, tokenizer, utils from blueprintcompiler import decompiler, parser, tokenizer, utils
from blueprintcompiler.ast_utils import AstNode
from blueprintcompiler.completions import complete from blueprintcompiler.completions import complete
from blueprintcompiler.errors import ( from blueprintcompiler.errors import (
CompileError, CompileError,
@ -61,11 +62,14 @@ class TestSamples(unittest.TestCase):
except: except:
pass 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)): for i in range(len(text)):
ast.get_docs(i) ast.get_docs(i)
for i in range(len(text)): 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() ast.get_document_symbols()
def assert_sample(self, name, skip_run=False): def assert_sample(self, name, skip_run=False):
@ -194,8 +198,10 @@ class TestSamples(unittest.TestCase):
"adw_breakpoint_template", "adw_breakpoint_template",
"expr_closure", "expr_closure",
"expr_closure_args", "expr_closure_args",
"expr_value_closure",
"parseable", "parseable",
"signal", "signal",
"signal_not_swapped",
"template", "template",
"template_binding", "template_binding",
"template_binding_extern", "template_binding_extern",