Compare commits

...

83 commits

Author SHA1 Message Date
James Westman
2e42dc6848
decompiler: Fix bug in signals with template object
If a signal handler had the template as its object, the decompiler would
output the class name instead of the 'template' keyword.
2025-05-03 07:46:34 -05:00
James Westman
a12d3f5c81
decompile: Fix bug in lookup tags
A lookup tag with no type attribute would crash the decompiler, even if
that was valid. This wasn't caught by the tests since blueprint never
generates such XML.

Also fixed a bug in the tests that caused decompiler-only tests not to
run.
2025-04-25 20:13:01 -05:00
James Westman
a83c7e936d
black: Update formatting 2025-04-25 18:32:33 -05:00
James Westman
3816f4fe8d
Add .doap file 2025-04-25 18:29:55 -05:00
James Westman
e9d61cb6f9
Update URLs after move to GNOME namespace on GitLab 2025-04-25 18:29:55 -05:00
James Westman
6a77bfee0a
tests: Fix typing 2025-04-19 13:27:20 -05:00
James Westman
f50b898e4c adw_breakpoint: Fix crash in language server
Fix a crash that happened when an AdwBreakpointSetter rule was
incomplete, such as when you're still typing it. Fixes #189.
2025-04-01 19:27:59 -05:00
Tom Greig
cc09f3d3bb Add tests for nested templates
Basically just a copy of the list_factory test, but with an extra copy
of the list factory inside it.
2025-03-30 10:27:11 +01:00
Tom Greig
f93d5d2acd Handle nested CDATA from nested templates
When putting CDATA into the output, any instances of ']]>' in the text
are replaced with ']]]]><![CDATA[>'.  This allows nested templates, e.g.
from list views inside list views to work properly.
2025-03-28 20:53:03 +00:00
Sertonix
394014429e Sort keys in collect-sections.py
This makes sure that the reference_docs.json file
is build reproducible.

Ref https://reproducible-builds.org/
2025-03-24 22:58:43 +00:00
Chris Mayo
a4e0c3701b docs: Update overview example using format and compile 2025-03-21 01:01:24 +00:00
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
kotontrion
e07da3c339 flags: use nick instead of name 2024-12-18 17:46:26 +00:00
kotontrion
2ae41020ab Fix flag return value type 2024-12-18 17:46:26 +00:00
kotontrion
f48b840cfa compile: fix flag values
gtk builder does not support combining interger values with | in flags
properties, so the short names are used instead.
2024-12-18 17:46:26 +00: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
Luoyayu
778a979714 lsp: Fix format of JSON-RPC content part ending with \r\n 2024-12-10 01:27:28 +00:00
James Westman
6acf0fe5a0
tests: Test deprecations separately
Libraries can add new deprecations, or the environment you're running
the tests in might have old libraries where the things we test aren't
deprecated yet. Move the deprecations test into its own module with its
own code, so it can check library versions and skip the test if it won't
work.
2024-12-09 19:06:10 -06:00
Vladimir Vaskov
a42ec3a945 docs: Put the cassette in the correct alphabetical place 2024-11-03 20:43:09 +00:00
Vladimir Vaskov
90308b69e0 docs: Add app making use of Blueprint 2024-11-03 20:43:09 +00:00
James Westman
3bf8fc151a
tests: Ignore deprecation warnings
Ignore deprecation warnings in the error handling tests, except in the
test specifically for deprecations. This prevents them from breaking if
libraries introduce new deprecations.

Fixes #178.
2024-11-03 14:40:36 -06:00
James Westman
a529a61955
docs: Corrections, updates, and improvements 2024-11-03 14:17:59 -06:00
James Westman
e19975e1f8
lsp: Add reference documentation on hover
For most constructs and keywords, show the relevant section of the
reference documentation on hover.
2024-10-20 21:10:14 -05:00
James Westman
b107a85947
lsp: Add property docs on notify signal hover 2024-10-19 20:47:27 -05:00
James Westman
e5fba8f3c7
lsp: Add semantic tokens for flag members 2024-10-19 20:46:26 -05:00
James Westman
f6d05be10b
lsp: Add more "go to reference" implementations 2024-10-19 20:44:34 -05:00
James Westman
94b532bc35
build: Update Docker container
Includes a change to handle a mypy update.
2024-10-19 19:16:45 -05:00
Benedek Dévényi
c805400a39 Update file index.rst 2024-10-19 15:26:53 +00:00
James Westman
3b6dcf072d
typelib: Fix field offsets for attributes
This fixes a bug where the decompiler could not recognize enums by their
C identifiers because it could not correctly read attributes.

Fixes #177.
2024-10-19 10:21:13 -05:00
James Westman
d7097cad01 docs: Mention null in literal values section 2024-09-20 17:04:00 -05:00
James Westman
8e10fcf869 Release v0.14.0 2024-08-24 14:26:46 -05:00
James Westman
65d4612b51 decompiler: Support action widgets 2024-08-24 13:24:27 -05:00
James Westman
21d5ce86e9 decompiler: Support sub-templates
Support GtkBuilderListItemFactory syntax by decompiling the nested XML,
rather than preserving it as a string literal.
2024-08-24 13:04:21 -05:00
James Westman
25d9826aea decompiler: Fix translator comments in properties 2024-08-24 12:40:04 -05:00
James Westman
a12ac1b976 decompiler: Support Adw.Breakpoint syntax
Also, improve handling of translated strings.
2024-08-24 12:29:14 -05:00
James Westman
078ce2f5b8 cli: Add decompile command
This command converts .ui files to Blueprint, similar to the porting
tool, but on individual files.
2024-08-23 18:29:34 -05:00
James Westman
4d3dc92448 decompiler: Support list accessibility properties 2024-08-23 18:16:02 -05:00
Julian Schmidhuber
3dfce3bbe0 Allow for multiple a11y properties 2024-08-18 13:17:41 +02:00
James Westman
b308adc3af Remove backslash from f-string expression
This restriction was removed in Python 3.12, but plenty of users still
have older versions.
2024-08-15 18:28:29 -05:00
James Westman
22514b96dc completions: Fix invalid escape sequence 2024-07-29 17:36:41 -05:00
James Westman
c1a82a034b decompiler: Add more decompilable tags
Add more tags to the list of things the decompiler can handle. This
required some changes to track the containing object class in the
DecompileCtx, since some objects use the same tag names.

The improved support means we can test the decompiler on most of the
test suite. Any new test samples will by default be tested to ensure the
decompiler produces the original blueprint file.

Also, updated the decompiler to always use double quotes.
2024-07-26 23:05:37 -05:00
James Westman
ea4c7245be errors: Show error length with carets
Use multiple carets to show the span of the error (up to the end of the
first line), rather than just a caret on the first character.
2024-07-27 03:38:43 +00:00
James Westman
2dcf0c154b errors: Fix caret when tabs are present
Replace all tabs with two spaces and account for that when positioning
the caret under the error location.
2024-07-27 03:38:43 +00:00
James Westman
9570dceaa8 errors: In suggestions, use "insert" or "remove"
Previously it always said "replace", leading to incorrect wording.
2024-07-27 03:38:43 +00:00
James Westman
8d734f7bbd lsp: Add hover docs for lookup expression props 2024-07-27 02:52:56 +00:00
James Westman
8dfa10019b lsp: Fix online docs links for interfaces 2024-07-27 02:52:56 +00:00
James Westman
24eed1048e gir: Fix assignable_to for template types
If we don't know the template's parent type, we should be able to assign
it to an extern type, since we don't know anything about that either.
2024-07-27 02:40:27 +00:00
James Westman
3a712af4dd Fix crash with bad escape sequence in string array
Invalid escape sequences aren't a fatal parser error, so the AST can be
built even when one is present, in which case the string token is None.
2024-07-26 21:10:33 -05:00
James Westman
d0659a43c2
Add test for recent bugfix 2024-07-22 20:38:58 -05:00
James Westman
b33cc7ccd7
adw-breakpoint: Fix bug when setting template prop
When a breakpoint setter's target object was the template, the compiler
failed with an assertion error. Fixed by allowing TemplateType objects
there. The assertion is still needed to make the type checker happy.
2024-07-22 20:31:15 -05:00
Sonny Piers
adc2be1454 Support template without parent 2024-07-04 22:14:16 +00:00
Sonny Piers
896dd7f824 fix linter 2024-07-04 22:07:02 +00:00
Sonny Piers
b76f4eef50 lsp: Use snippet for object completion 2024-07-04 22:07:02 +00:00
Sonny Piers
8c102cf9dc lsp: Fix syntax for signal completion 2024-07-04 22:07:02 +00:00
Sonny Piers
a075b26769 lsp: Extend completion documentation 2024-07-04 22:07:02 +00:00
Diego Augusto
da5b9909fc Support array type 2024-07-04 22:02:51 +00:00
Marco Capypara Köpcke
f1cf70b6eb xgettext compatibility: Output 'yes' for translatable 2024-07-04 22:29:16 +02:00
Benoit Pierre
85630bc975 tests: fix tests when used as subproject
`meson.source_root()` will return the source root of the parent
project, not the current project when it's used as subproject.
2024-07-04 20:10:43 +00:00
Sonny Piers
e44494e6e2 decompiler: Use bind instead of bind-property 2024-06-29 13:32:34 +00:00
Szepesi Tibor
6bae860326
lsp: Fix semantic token positions 2024-06-20 13:38:12 +02:00
Valéry Febvre
aac834e1c5 docs: Fix misspelt Komikku and repo URL 2024-06-16 21:42:50 +00:00
James Westman
442fff69b6 Fix crash in validate_parent_type
If the type being checked for is not found (e.g. the library is not
installed or is out of date), parent.full_name would be None.
2024-05-30 17:51:40 -05:00
Gregor Niehl
25d08e56cb signals: Support after keyword 2024-05-30 22:34:05 +00:00
James Westman
07e824d8e7 lang: Also allow Gtk.ListHeader in factory template 2024-05-04 12:27:12 -05:00
James Westman
c502dee36b output: Don't add @generated notice to subtemplates
There's already a notice at the top of the file, it doesn't need to be
in subtemplates.

Fixes #158.
2024-05-02 20:19:00 -05:00
James Westman
988e69ab25 lang: Allow ColumnView widgets to be built
Allow BuilderListItemFactory to contain Gtk.ColumnViewRow or
Gtk.ColumnViewCell templates, in addition to Gtk.ListItem templates.
This is necessary for people to use Gtk.ColumnView idiomatically in
Blueprint.

Fixes #157.
2024-05-02 20:19:00 -05:00
Gregor Niehl
84e529a4a8 Formatter CLI: Provide option to suppress diff 2024-04-27 12:04:44 +00:00
James Westman
1c8d7daea2 lsp: Fix deprecation warnings 2024-04-06 14:33:20 -05:00
James Westman
6a078ee075 Add warning for unused imports 2024-04-06 14:33:20 -05:00
Dexter Reed
729939ad93
docs: Fix misspelt Doggo, fix duplicate Maniatic Launcher 2024-03-26 18:30:23 +00:00
Sonny Piers
7e4f80523d Post-release version bump 2024-03-21 20:40:18 +01:00
227 changed files with 2924 additions and 614 deletions

View file

@ -3,7 +3,7 @@ stages:
- pages
build:
image: registry.gitlab.gnome.org/jwestman/blueprint-compiler
image: registry.gitlab.gnome.org/gnome/blueprint-compiler
stage: build
script:
- black --check --diff ./ tests
@ -19,7 +19,7 @@ build:
- ninja -C _build docs/en
- git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git
- cd blueprint-regression-tests
- git checkout 71522af3607e08143671ee0d224e65e9b9eb9f30
- git checkout 5f9e155c1333e84e6f683cdb26b02a5925fd8db3
- ./test.sh
- cd ..
coverage: '/TOTAL.*\s([.\d]+)%/'
@ -33,7 +33,7 @@ build:
path: coverage.xml
fuzz:
image: registry.gitlab.gnome.org/jwestman/blueprint-compiler
image: registry.gitlab.gnome.org/gnome/blueprint-compiler
stage: build
script:
- meson _build

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.
4. Create a "Post-release version bump" commit.
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

73
NEWS.md
View file

@ -1,3 +1,76 @@
# 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
## Added
- Added a warning for unused imports.
- Added an option to not print the diff when formatting with the CLI. (Gregor Niehl)
- Added support for building Gtk.ColumnViewRow, Gtk.ColumnViewCell, and Gtk.ListHeader widgets with Gtk.BuilderListItemFactory.
- Added support for the `after` keyword for signals. This was previously documented but not implemented. (Gregor Niehl)
- Added support for string arrays. (Diego Augusto)
- Added hover documentation for properties in lookup expressions.
- The decompiler supports action widgets, translation domains, `typeof<>` syntax, and expressions. It also supports extension syntax for Adw.Breakpoint, Gtk.BuilderListItemFactory, Gtk.ComboBoxText, Gtk.SizeGroup, and Gtk.StringList.
- Added a `decompile` subcommand to the CLI, which decompiles an XML .ui file to blueprint.
- Accessibility relations that allow multiple values are supported using list syntax. (Julian Schmidhuber)
## Changed
- The decompiler sorts imports alphabetically.
- Translatable strings use `translatable="yes"` instead of `translatable="true"` for compatibility with xgettext. (Marco Köpcke)
- The first line of the documentation is shown in the completion list when using the language server. (Sonny Piers)
- Object autocomplete uses a snippet to add the braces and position the cursor inside them. (Sonny Piers)
- The carets in the CLI diagnostic output now span the whole error message up to the end of the first line, rather than just the first character.
- The decompiler emits double quotes, which are compatible with gettext.
## Fixed
- Fixed deprecation warnings in the language server.
- The decompiler no longer duplicates translator comments on properties.
- Subtemplates no longer output a redundant `@generated` comment.
- When extension syntax from a library that is not available is used, the compiler emits an error instead of crashing.
- The language server reports semantic token positions correctly. (Szepesi Tibor)
- The decompiler no longer emits the deprecated `bind-property` syntax. (Sonny Piers)
- Fixed the tests when used as a Meson subproject. (Benoit Pierre)
- Signal autocomplete generates correct syntax. (Sonny Piers)
- The decompiler supports templates that do not specify a parent class. (Sonny Piers)
- Adw.Breakpoint setters that set a property on the template no longer cause a crash.
- Fixed type checking with templates that do not have a parent class.
- Fixed online documentation links for interfaces.
- The wording of edit suggestions is fixed for insertions and deletions.
- When an input file uses tabs instead of spaces, the diagnostic output on the CLI aligns the caret correctly.
- The decompiler emits correct syntax when a property binding refers to the template object.
## Documentation
- Fixed typos in "Built with Blueprint" section. (Valéry Febvre, Dexter Reed)
# v0.12.0
## Added

27
blueprint-compiler.doap Normal file
View file

@ -0,0 +1,27 @@
<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
xmlns:foaf="http://xmlns.com/foaf/0.1/"
xmlns:gnome="http://api.gnome.org/doap-extensions#"
xmlns="http://usefulinc.com/ns/doap#">
<name xml:lang="en">Blueprint</name>
<shortdesc xml:lang="en">A modern language for creating GTK interfaces</shortdesc>
<description xml:lang="en">Blueprint is a language and associated tooling for building user interfaces for GTK.</description>
<category rdf:resource="http://api.gnome.org/doap-extensions#apps" />
<programming-language>Python</programming-language>
<homepage
rdf:resource="https://gnome.gitlab.gnome.org/blueprint-compiler/" />
<download-page
rdf:resource="https://gitlab.gnome.org/GNOME/blueprint-compiler/-/releases" />
<bug-database
rdf:resource="https://gitlab.gnome.org/GNOME/blueprint-compiler/issues" />
<maintainer>
<foaf:Person>
<foaf:name>James Westman</foaf:name>
<foaf:mbox rdf:resource="mailto:james@jwestman.net" />
<gnome:userid>jwestman</gnome:userid>
</foaf:Person>
</maintainer>
</Project>

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

@ -125,7 +125,7 @@ class AstNode:
return self.parent.root
@property
def range(self):
def range(self) -> Range:
return Range(self.group.start, self.group.end, self.group.text)
def parent_by_type(self, type: T.Type[TType]) -> TType:
@ -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()
@ -179,14 +184,16 @@ class AstNode:
token = self.group.tokens.get(attr.token_name)
if token and token.start <= idx < token.end:
return getattr(self, name)
else:
return getattr(self, name)
for child in self.children:
if idx in child.range:
if docs := child.get_docs(idx):
return docs
for name, attr in self._attrs_by_type(Docs):
if not attr.token_name:
return getattr(self, name)
return None
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
@ -247,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(
@ -264,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

@ -19,7 +19,7 @@
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
@ -72,7 +72,9 @@ def complete(
@completer([language.GtkDirective])
def using_gtk(lsp, ast_node, match_variables):
yield Completion("using Gtk 4.0;", CompletionItemKind.Keyword)
yield Completion(
"using Gtk 4.0", CompletionItemKind.Keyword, snippet="using Gtk 4.0;\n"
)
@completer(
@ -101,7 +103,13 @@ def object_completer(lsp, ast_node, match_variables):
ns = ast_node.root.gir.namespaces.get(match_variables[0])
if ns is not None:
for c in ns.classes.values():
yield Completion(c.name, CompletionItemKind.Class, docs=c.doc)
yield Completion(
c.name,
CompletionItemKind.Class,
snippet=f"{c.name} {{\n $0\n}}",
docs=c.doc,
detail=c.detail,
)
@completer(
@ -112,7 +120,13 @@ def gtk_object_completer(lsp, ast_node, match_variables):
ns = ast_node.root.gir.namespaces.get("Gtk")
if ns is not None:
for c in ns.classes.values():
yield Completion(c.name, CompletionItemKind.Class, docs=c.doc)
yield Completion(
c.name,
CompletionItemKind.Class,
snippet=f"{c.name} {{\n $0\n}}",
docs=c.doc,
detail=c.detail,
)
@completer(
@ -120,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)
@ -131,13 +145,23 @@ def property_completer(lsp, ast_node, match_variables):
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=f"{prop_name}: ${{1|true,false|}};",
docs=prop.doc,
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,
)
elif (
isinstance(prop.type, gir.Enumeration)
@ -150,6 +174,17 @@ def property_completer(lsp, ast_node, match_variables):
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=f"{prop_name}: ${{1|{choices}|}};",
docs=prop.doc,
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:
yield Completion(
@ -157,18 +192,25 @@ def property_completer(lsp, ast_node, match_variables):
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=f"{prop_name}: $0;",
docs=prop.doc,
detail=prop.detail,
)
@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):
if (vt := ast_node.value_type) is not None:
if isinstance(vt.value_type, gir.Enumeration):
for name, member in vt.value_type.members.items():
yield Completion(name, CompletionItemKind.EnumMember, docs=member.doc)
yield Completion(
name,
CompletionItemKind.EnumMember,
docs=member.doc,
detail=member.detail,
)
elif isinstance(vt.value_type, gir.BoolType):
yield Completion("true", CompletionItemKind.Constant)
@ -180,8 +222,8 @@ 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):
for signal in ast_node.gir_class.signals:
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"
else:
@ -192,10 +234,12 @@ def signal_completer(lsp, ast_node, match_variables):
.lower()
)
yield Completion(
signal,
signal_name,
CompletionItemKind.Event,
sort_text=f"1 {signal}",
snippet=f"{signal} => \$${{1:{name}_{signal.replace('-', '_')}}}()$0;",
sort_text=f"1 {signal_name}",
snippet=f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;",
docs=signal.doc,
detail=signal.detail,
)

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

@ -17,8 +17,8 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import re
import typing as T
from collections import defaultdict
from dataclasses import dataclass
from enum import Enum
@ -30,7 +30,7 @@ from .xml_reader import Element, parse, parse_string
__all__ = ["decompile"]
_DECOMPILERS: T.Dict = {}
_DECOMPILERS: dict[str, list] = defaultdict(list)
_CLOSING = {
"{": "}",
"[": "]",
@ -51,27 +51,32 @@ class LineType(Enum):
class DecompileCtx:
def __init__(self) -> None:
def __init__(self, parent_gir: T.Optional[GirContext] = None) -> None:
self.sub_decompiler = parent_gir is not None
self._result: str = ""
self.gir = GirContext()
self._indent: int = 0
self.gir = parent_gir or GirContext()
self._blocks_need_end: T.List[str] = []
self._last_line_type: LineType = LineType.NONE
self.template_class: T.Optional[str] = None
self._obj_type_stack: list[T.Optional[GirType]] = []
self._node_stack: list[Element] = []
self.gir.add_namespace(get_namespace("Gtk", "4.0"))
@property
def result(self) -> str:
imports = "\n".join(
imports = ""
if not self.sub_decompiler:
import_lines = sorted(
[
f"using {ns} {namespace.version};"
for ns, namespace in self.gir.namespaces.items()
if ns != "Gtk"
]
)
full_string = imports + "\n" + self._result
formatted_string = formatter.format(full_string)
return formatted_string
imports += "\n".join(["using Gtk 4.0;", *import_lines])
return formatter.format(imports + self._result)
def type_by_cname(self, cname: str) -> T.Optional[GirType]:
if type := self.gir.get_type_by_cname(cname):
@ -90,10 +95,66 @@ class DecompileCtx:
def start_block(self) -> None:
self._blocks_need_end.append("")
self._obj_type_stack.append(None)
def end_block(self) -> None:
if close := self._blocks_need_end.pop():
self.print(close)
self._obj_type_stack.pop()
@property
def current_obj_type(self) -> T.Optional[GirType]:
return next((x for x in reversed(self._obj_type_stack) if x is not None), None)
def push_obj_type(self, type: T.Optional[GirType]) -> None:
self._obj_type_stack[-1] = type
@property
def current_node(self) -> T.Optional[Element]:
if len(self._node_stack) == 0:
return None
else:
return self._node_stack[-1]
@property
def parent_node(self) -> T.Optional[Element]:
if len(self._node_stack) < 2:
return None
else:
return self._node_stack[-2]
@property
def root_node(self) -> T.Optional[Element]:
if len(self._node_stack) == 0:
return None
else:
return self._node_stack[0]
@property
def template_class(self) -> T.Optional[str]:
assert self.root_node is not None
for child in self.root_node.children:
if child.tag == "template":
return child["class"]
return None
def find_object(self, id: str) -> T.Optional[Element]:
assert self.root_node is not None
for child in self.root_node.children:
if child.tag == "template" and child["class"] == id:
return child
def find_in_children(node: Element) -> T.Optional[Element]:
if node.tag in ["object", "menu"] and node["id"] == id:
return node
else:
for child in node.children:
if result := find_in_children(child):
return result
return None
return find_in_children(self.root_node)
def end_block_with(self, text: str) -> None:
self._blocks_need_end[-1] = text
@ -105,7 +166,15 @@ class DecompileCtx:
if len(self._blocks_need_end):
self._blocks_need_end[-1] = _CLOSING[line[-1]]
def print_attribute(self, name: str, value: str, type: GirType) -> None:
# Converts a value from an XML element to a blueprint string
# based on the given type. Returns a tuple of translator comments
# (if any) and the decompiled syntax.
def decompile_value(
self,
value: str,
type: T.Optional[GirType],
translatable: T.Optional[T.Tuple[str, str, str]] = None,
) -> T.Tuple[str, str]:
def get_enum_name(value):
for member in type.members.values():
if (
@ -116,13 +185,18 @@ class DecompileCtx:
return member.name
return value.replace("-", "_")
if type is None:
self.print(f"{name}: {escape_quote(value)};")
if translatable is not None and truthy(translatable[0]):
return decompile_translatable(value, *translatable)
elif type is None:
return "", f"{escape_quote(value)}"
elif type.assignable_to(FloatType()):
self.print(f"{name}: {value};")
return "", str(value)
elif type.assignable_to(BoolType()):
val = truthy(value)
self.print(f"{name}: {'true' if val else 'false'};")
return "", ("true" if val else "false")
elif type.assignable_to(ArrayType(StringType())):
items = ", ".join([escape_quote(x) for x in value.split("\n")])
return "", f"[{items}]"
elif (
type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture"))
@ -136,67 +210,82 @@ class DecompileCtx:
self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger")
)
):
self.print(f"{name}: {escape_quote(value)};")
return "", escape_quote(value)
elif value == self.template_class:
self.print(f"{name}: template;")
return "", "template"
elif type.assignable_to(
self.gir.namespaces["Gtk"].lookup_type("GObject.Object")
):
self.print(f"{name}: {value};")
) or isinstance(type, Interface):
return "", ("null" if value == "" else value)
elif isinstance(type, Bitfield):
flags = [get_enum_name(flag) for flag in value.split("|")]
self.print(f"{name}: {' | '.join(flags)};")
return "", " | ".join(flags)
elif isinstance(type, Enumeration):
self.print(f"{name}: {get_enum_name(value)};")
return "", get_enum_name(value)
elif isinstance(type, TypeType):
if t := self.type_by_cname(value):
return "", f"typeof<{full_name(t)}>"
else:
self.print(f"{name}: {escape_quote(value)};")
return "", f"typeof<${value}>"
else:
return "", escape_quote(value)
def _decompile_element(
def decompile_element(
ctx: DecompileCtx, gir: T.Optional[GirContext], xml: Element
) -> None:
try:
decompiler = _DECOMPILERS.get(xml.tag)
if decompiler is None:
decompilers = [d for d in _DECOMPILERS[xml.tag] if d._filter(ctx)]
if len(decompilers) == 0:
raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>")
args: T.Dict[str, T.Optional[str]] = {
canon(name): value for name, value in xml.attrs.items()
}
decompiler = decompilers[0]
if decompiler._element:
args = [ctx, gir, xml]
kwargs: T.Dict[str, T.Optional[str]] = {}
else:
args = [ctx, gir]
kwargs = {canon(name): value for name, value in xml.attrs.items()}
if decompiler._cdata:
if len(xml.children):
args["cdata"] = None
kwargs["cdata"] = None
else:
args["cdata"] = xml.cdata
kwargs["cdata"] = xml.cdata
ctx._node_stack.append(xml)
ctx.start_block()
gir = decompiler(ctx, gir, **args)
try:
gir = decompiler(*args, **kwargs)
except TypeError as e:
raise UnsupportedError(tag=xml.tag)
if not decompiler._skip_children:
for child in xml.children:
_decompile_element(ctx, gir, child)
decompile_element(ctx, gir, child)
ctx.end_block()
ctx._node_stack.pop()
except UnsupportedError as e:
raise e
except TypeError as e:
raise UnsupportedError(tag=xml.tag)
def decompile(data: str) -> str:
ctx = DecompileCtx()
xml = parse(data)
_decompile_element(ctx, None, xml)
decompile_element(ctx, None, xml)
return ctx.result
def decompile_string(data):
def decompile_string(data: str) -> str:
ctx = DecompileCtx()
xml = parse_string(data)
_decompile_element(ctx, None, xml)
decompile_element(ctx, None, xml)
return ctx.result
@ -209,10 +298,10 @@ def canon(string: str) -> str:
def truthy(string: str) -> bool:
return string.lower() in ["yes", "true", "t", "y", "1"]
return string is not None and string.lower() in ["yes", "true", "t", "y", "1"]
def full_name(gir) -> str:
def full_name(gir: GirType) -> str:
return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name
@ -223,17 +312,45 @@ def lookup_by_cname(gir, cname: str) -> T.Optional[GirType]:
return gir.get_containing(Repository).get_type_by_cname(cname)
def decompiler(tag, cdata=False):
def decompiler(
tag,
cdata=False,
parent_type: T.Optional[str] = None,
parent_tag: T.Optional[str] = None,
skip_children=False,
element=False,
):
def decorator(func):
func._cdata = cdata
_DECOMPILERS[tag] = func
func._skip_children = skip_children
func._element = element
def filter(ctx):
if parent_type is not None:
if (
ctx.current_obj_type is None
or ctx.current_obj_type.full_name != parent_type
):
return False
if parent_tag is not None:
if not any(x.tag == parent_tag for x in ctx._node_stack):
return False
return True
func._filter = filter
_DECOMPILERS[tag].append(func)
return func
return decorator
@decompiler("interface")
def decompile_interface(ctx, gir):
def decompile_interface(ctx, gir, domain=None):
if domain is not None:
ctx.print(f"translation-domain {escape_quote(domain)};")
return gir
@ -252,9 +369,11 @@ def decompile_translatable(
translatable: T.Optional[str],
context: T.Optional[str],
comments: T.Optional[str],
) -> T.Tuple[T.Optional[str], str]:
) -> T.Tuple[str, str]:
if translatable is not None and truthy(translatable):
if comments is not None:
if comments is None:
comments = ""
else:
comments = comments.replace("/*", " ").replace("*/", " ")
comments = f"/* Translators: {comments} */"
@ -263,7 +382,7 @@ def decompile_translatable(
else:
return comments, f"_({escape_quote(string)})"
else:
return comments, f"{escape_quote(string)}"
return "", f"{escape_quote(string)}"
@decompiler("property", cdata=True)
@ -280,11 +399,8 @@ def decompile_property(
context=None,
):
name = name.replace("_", "-")
if comments is not None:
ctx.print(f"/* Translators: {comments} */")
if cdata is None:
ctx.print(f"{name}: ", False)
ctx.print(f"{name}: ")
ctx.end_block_with(";")
elif bind_source:
flags = ""
@ -295,7 +411,11 @@ def decompile_property(
flags += " inverted"
if "bidirectional" in bind_flags:
flags += " bidirectional"
ctx.print(f"{name}: bind-property {bind_source}.{bind_property}{flags};")
if bind_source == ctx.template_class:
bind_source = "template"
ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
elif truthy(translatable):
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
@ -305,8 +425,19 @@ def decompile_property(
ctx.print(f"{name}: {translatable};")
elif gir is None or gir.properties.get(name) is None:
ctx.print(f"{name}: {escape_quote(cdata)};")
elif (
gir.assignable_to(ctx.gir.get_class("BuilderListItemFactory", "Gtk"))
and name == "bytes"
):
sub_ctx = DecompileCtx(ctx.gir)
xml = parse_string(cdata)
decompile_element(sub_ctx, None, xml)
ctx.print(sub_ctx.result)
else:
ctx.print_attribute(name, cdata, gir.properties.get(name).type)
_, string = ctx.decompile_value(cdata, gir.properties.get(name).type)
ctx.print(f"{name}: {string};")
return gir

View file

@ -93,15 +93,28 @@ class CompileError(PrintableError):
assert self.range is not None
line_num, col_num = utils.idx_to_pos(self.range.start + 1, code)
end_line_num, end_col_num = utils.idx_to_pos(self.range.end + 1, code)
line = code.splitlines(True)[line_num] if code != "" else ""
# Display 1-based line numbers
line_num += 1
end_line_num += 1
n_spaces = col_num - 1
n_carets = (
(end_col_num - col_num)
if line_num == end_line_num
else (len(line) - n_spaces - 1)
)
n_spaces += line.count("\t", 0, col_num)
n_carets += line.count("\t", col_num, col_num + n_carets)
line = line.replace("\t", " ")
stream.write(
f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
at {filename} line {line_num} column {col_num}:
{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n"""
{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*n_spaces}{"^"*n_carets}{Colors.CLEAR}\n"""
)
for hint in self.hints:
@ -113,6 +126,14 @@ at {filename} line {line_num} column {col_num}:
if action.edit_range is not None
else self.range.text
)
if old == "":
stream.write(
f"suggestion: insert {Colors.GREEN}{action.replace_with}{Colors.CLEAR}\n"
)
elif action.replace_with == "":
stream.write(f"suggestion: remove {Colors.RED}{old}{Colors.CLEAR}\n")
else:
stream.write(
f"suggestion: replace {Colors.RED}{old}{Colors.CLEAR} with {Colors.GREEN}{action.replace_with}{Colors.CLEAR}\n"
)
@ -140,6 +161,10 @@ class DeprecatedWarning(CompileWarning):
pass
class UnusedWarning(CompileWarning):
pass
class UpgradeWarning(CompileWarning):
category = "upgrade"
color = Colors.PURPLE
@ -194,7 +219,7 @@ def report_bug(): # pragma: no cover
f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG *****
The blueprint-compiler program has crashed. Please report the above stacktrace,
along with the input file(s) if possible, on GitLab:
{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue
{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue
{Colors.CLEAR}"""
)

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 = ("{", "[")
@ -145,8 +146,10 @@ def format(data, tab_size=2, insert_space=True):
is_child_type = False
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]
if str(last_not_whitespace) != ",":
current_line += ","
commit_current_line()
current_line = "]"
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:
newlines = 2
current_line = "\n".join(
[line.rstrip() for line in current_line.split("\n")]
)
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,
]
@ -200,6 +212,10 @@ class ArrayType(GirType):
def assignable_to(self, other: GirType) -> bool:
return isinstance(other, ArrayType) and self._inner.assignable_to(other._inner)
@property
def inner(self) -> GirType:
return self._inner
@property
def name(self) -> str:
return self._inner.name + "[]"
@ -332,6 +348,17 @@ class GirNode:
def available_in(self) -> str:
return self.xml.get("version")
@cached_property
def detail(self) -> T.Optional[str]:
try:
el = self.xml.get_elements("doc")
if len(el) == 1:
return el[0].cdata.strip().partition("\n")[0]
else:
return None
except:
return None
@cached_property
def doc(self) -> T.Optional[str]:
sections = []
@ -440,7 +467,10 @@ class Signature(GirNode):
return result
@cached_property
def return_type(self) -> GirType:
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
)
@ -463,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]:
@ -534,7 +567,7 @@ class Interface(GirNode, GirType):
@property
def online_docs(self) -> T.Optional[str]:
if ns := self.get_containing(Namespace).online_docs:
return f"{ns}interface.{self.name}.html"
return f"{ns}iface.{self.name}.html"
else:
return None
@ -706,7 +739,7 @@ class TemplateType(GirType):
# we don't know the template type's interfaces, assume yes
return True
elif self.parent is None or isinstance(self.parent, ExternType):
return isinstance(other, Class)
return isinstance(other, Class) or isinstance(other, ExternType)
else:
return self.parent.assignable_to(other)
@ -875,22 +908,22 @@ 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)
def get_type_by_cname(self, cname: str) -> T.Optional[GirType]:
"""Gets a type from this namespace by its C name."""
for basic in _BASIC_TYPES.values():
if basic.glib_type_name == cname:
return basic()
for item in self.entries.values():
if hasattr(item, "cname") and item.cname == cname:
if (
hasattr(item, "cname")
and item.cname is not None
and item.cname == cname
):
return item
return None
@ -898,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)
@property
def online_docs(self) -> T.Optional[str]:
@ -923,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 = {}
@ -931,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

@ -71,7 +71,7 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
print(
f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the
porting tool. If you think it's a bug (which is likely), please file an issue on GitLab:
{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n"""
{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n"""
)
return CouldNotPort("does not compile")
@ -136,7 +136,7 @@ def step1():
wrap.write(
f"""[wrap-git]
directory = blueprint-compiler
url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git
url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git
revision = {VERSION}
depth = 1

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,
@ -41,6 +40,8 @@ from .imports import GtkDirective, Import
from .types import ClassName
from .ui import UI
from .values import (
ArrayValue,
ExprValue,
Flag,
Flags,
IdentLiteral,

View file

@ -81,8 +81,8 @@ class AdwBreakpointSetter(AstNode):
return self.tokens["property"]
@property
def value(self) -> Value:
return self.children[Value][0]
def value(self) -> T.Optional[Value]:
return self.children[Value][0] if len(self.children[Value]) > 0 else None
@property
def gir_class(self) -> T.Optional[GirType]:
@ -98,13 +98,18 @@ class AdwBreakpointSetter(AstNode):
and not isinstance(self.gir_class, ExternType)
and self.property_name is not None
):
assert isinstance(self.gir_class, gir.Class)
assert isinstance(self.gir_class, gir.Class) or isinstance(
self.gir_class, gir.TemplateType
)
return self.gir_class.properties.get(self.property_name)
else:
return None
@property
def document_symbol(self) -> DocumentSymbol:
def document_symbol(self) -> T.Optional[DocumentSymbol]:
if self.value is None:
return None
return DocumentSymbol(
f"{self.object_id}.{self.property_name}",
SymbolKind.Property,
@ -113,6 +118,17 @@ class AdwBreakpointSetter(AstNode):
self.value.range.text,
)
def get_reference(self, idx: int) -> T.Optional[LocationLink]:
if idx in self.group.tokens["object"].range:
if self.object is not None:
return LocationLink(
self.group.tokens["object"].range,
self.object.range,
self.object.ranges["id"],
)
return None
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
if self.gir_property is not None:
@ -193,3 +209,46 @@ class AdwBreakpointSetters(AstNode):
@validate()
def unique(self):
self.validate_unique_in_parent("Duplicate setters block")
@docs("setters")
def ref_docs(self):
return get_docs_section("Syntax ExtAdwBreakpoint")
@decompiler("condition", cdata=True)
def decompile_condition(ctx: DecompileCtx, gir, cdata):
ctx.print(f"condition({escape_quote(cdata)})")
@decompiler("setter", element=True)
def decompile_setter(ctx: DecompileCtx, gir, element):
assert ctx.parent_node is not None
# only run for the first setter
for child in ctx.parent_node.children:
if child.tag == "setter":
if child != element:
# already decompiled
return
else:
break
ctx.print("setters {")
for child in ctx.parent_node.children:
if child.tag == "setter":
object_id = child["object"]
property_name = child["property"]
obj = ctx.find_object(object_id)
if obj is not None:
gir_class = ctx.type_by_cname(obj["class"])
else:
gir_class = None
if object_id == ctx.template_class:
object_id = "template"
comments, string = ctx.decompile_value(
child.cdata,
gir_class,
(child["translatable"], child["context"], child["comments"]),
)
ctx.print(f"{comments} {object_id}.{property_name}: {string};")

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

View file

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

@ -58,6 +58,10 @@ class BindingFlag(AstNode):
"Only bindings with a single lookup can have flags",
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax Binding")
class Binding(AstNode):
grammar = [
@ -99,6 +103,10 @@ class Binding(AstNode):
actions=[CodeAction("use 'bind'", "bind")],
)
@docs("bind")
def ref_docs(self):
return get_docs_section("Syntax Binding")
@dataclass
class SimpleBinding:
@ -107,3 +115,9 @@ class SimpleBinding:
no_sync_create: bool = False
bidirectional: bool = False
inverted: bool = False
@decompiler("binding")
def decompile_binding(ctx: DecompileCtx, gir: gir.GirContext, name: str):
ctx.end_block_with(";")
ctx.print(f"{name}: bind ")

View file

@ -35,6 +35,7 @@ from ..errors import (
CompileWarning,
DeprecatedWarning,
MultipleErrors,
UnusedWarning,
UpgradeWarning,
)
from ..gir import (
@ -54,6 +55,7 @@ from ..lsp_utils import (
SemanticToken,
SemanticTokenType,
SymbolKind,
get_docs_section,
)
from ..parse_tree import *

View file

@ -79,3 +79,9 @@ class ScopeCtx:
for child in node.children:
if child.context[ScopeCtx] is self:
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

@ -18,9 +18,9 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..decompiler import decompile_element
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
from .gtkbuilder_template import Template
from .types import TypeName
expr = Sequence()
@ -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
@ -89,6 +81,16 @@ class LiteralExpr(ExprBase):
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
def literal(self):
from .values import Literal
@ -99,14 +101,14 @@ class LiteralExpr(ExprBase):
def type(self) -> T.Optional[GirType]:
return self.literal.value.type
@property
def type_complete(self) -> bool:
from .values import IdentLiteral
@validate()
def item_validations(self):
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 object := self.context[ScopeCtx].objects.get(self.literal.value.ident):
return not object.gir_class.incomplete
return True
if not isinstance(self.rhs.rhs, LookupOp):
raise CompileError('"item" can only be used for looking up properties')
class LookupOp(InfixExpr):
@ -130,6 +132,17 @@ class LookupOp(InfixExpr):
return None
@docs("property")
def property_docs(self):
if not (
isinstance(self.lhs.type, gir.Class)
or isinstance(self.lhs.type, gir.Interface)
):
return None
if property := self.lhs.type.properties.get(self.property_name):
return property.doc
@validate("property")
def property_exists(self):
if self.lhs.type is None:
@ -160,10 +173,28 @@ class LookupOp(InfixExpr):
did_you_mean=(self.property_name, self.lhs.type.properties.keys()),
)
@validate("property")
def property_deprecated(self):
if self.lhs.type is None or not (
isinstance(self.lhs.type, gir.Class)
or isinstance(self.lhs.type, gir.Interface)
):
return
if property := self.lhs.type.properties.get(self.property_name):
if property.deprecated:
hints = []
if property.deprecated_doc:
hints.append(property.deprecated_doc)
raise DeprecatedWarning(
f"{property.signature} is deprecated",
hints=hints,
)
class CastExpr(InfixExpr):
grammar = [
"as",
Keyword("as"),
AnyOf(
["<", TypeName, Match(">").expected()],
[
@ -182,10 +213,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:
@ -209,6 +236,10 @@ class CastExpr(InfixExpr):
],
)
@docs("as")
def ref_docs(self):
return get_docs_section("Syntax CastExpression")
class ClosureArg(AstNode):
grammar = Expression
@ -258,8 +289,96 @@ class ClosureExpr(ExprBase):
if not self.tokens["extern"]:
raise CompileError(f"{self.closure_name} is not a builtin function")
@docs("name")
def ref_docs(self):
return get_docs_section("Syntax ClosureExpression")
expr.children = [
AnyOf(ClosureExpr, LiteralExpr, ["(", Expression, ")"]),
ZeroOrMore(AnyOf(LookupOp, CastExpr)),
]
@decompiler("lookup", skip_children=True, cdata=True)
def decompile_lookup(
ctx: DecompileCtx,
gir: gir.GirContext,
cdata: str,
name: 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:
type = ""
elif t := ctx.type_by_cname(type):
type = decompile.full_name(t)
else:
type = "$" + type
assert ctx.current_node is not None
constant = None
if len(ctx.current_node.children) == 0:
constant = cdata
elif (
len(ctx.current_node.children) == 1
and ctx.current_node.children[0].tag == "constant"
):
constant = ctx.current_node.children[0].cdata
if constant is not None:
if constant == ctx.template_class:
ctx.print("template." + name)
elif constant == "":
ctx.print(f"item as <{type}>.{name}")
else:
ctx.print(constant + "." + name)
return
else:
for child in ctx.current_node.children:
decompile.decompile_element(ctx, gir, child)
ctx.print(f" as <{type}>.{name}")
@decompiler("constant", cdata=True)
def decompile_constant(
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 cdata == ctx.template_class:
ctx.print("template")
else:
ctx.print(cdata)
else:
_, string = ctx.decompile_value(cdata, ctx.type_by_cname(type))
ctx.print(string)
@decompiler("closure", skip_children=True)
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):
type = decompile.full_name(t)
else:
type = "$" + type
ctx.print(f"${function}(")
assert ctx.current_node is not None
for i, node in enumerate(ctx.current_node.children):
decompile_element(ctx, gir, node)
assert ctx.current_node is not None
if i < len(ctx.current_node.children) - 1:
ctx.print(", ")
ctx.end_block_with(f") as <{type}>")

View file

@ -28,7 +28,18 @@ from .common import *
from .response_id import ExtResponse
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):
@ -110,12 +121,12 @@ def validate_parent_type(node, ns: str, name: str, err_msg: str):
container_type = node.parent_by_type(Object).gir_class
if container_type and not container_type.assignable_to(parent):
raise CompileError(
f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}"
f"{container_type.full_name} is not a {ns}.{name}, so it doesn't have {err_msg}"
)
@decompiler("object")
def decompile_object(ctx, gir, klass, id=None):
def decompile_object(ctx: DecompileCtx, gir, klass, id=None):
gir_class = ctx.type_by_cname(klass)
klass_name = (
decompile.full_name(gir_class) if gir_class is not None else "$" + klass
@ -124,4 +135,5 @@ def decompile_object(ctx, gir, klass, id=None):
ctx.print(f"{klass_name} {{")
else:
ctx.print(f"{klass_name} {id} {{")
ctx.push_obj_type(gir_class)
return gir_class

View file

@ -21,19 +21,20 @@
from .binding import Binding
from .common import *
from .contexts import ValueTypeCtx
from .gtkbuilder_template import Template
from .values import ObjectValue, Value
from .values import ArrayValue, ExprValue, ObjectValue, Value
class Property(AstNode):
grammar = Statement(UseIdent("name"), ":", AnyOf(Binding, ObjectValue, Value))
grammar = Statement(
UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue)
)
@property
def name(self) -> str:
return self.tokens["name"]
@property
def value(self) -> T.Union[Binding, ObjectValue, Value]:
def value(self) -> T.Union[Binding, ExprValue, ObjectValue, Value, ArrayValue]:
return self.children[0]
@property
@ -49,7 +50,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,31 @@ 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")
class Signal(AstNode):
grammar = Statement(
@ -50,7 +76,7 @@ class Signal(AstNode):
UseIdent("detail_name").expected("a signal detail name"),
]
),
"=>",
Keyword("=>"),
Mark("detail_start"),
Optional(["$", UseLiteral("extern", True)]),
UseIdent("handler").expected("the name of a function to handle the signal"),
@ -88,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:
@ -109,14 +143,25 @@ 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 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(
self.group.tokens["object"].range, obj.range, obj.ranges["id"]
)
return None
@validate("handler")
def old_extern(self):
if not self.tokens["extern"]:
@ -164,13 +209,41 @@ class Signal(AstNode):
if self.gir_signal is not None:
return self.gir_signal.doc
@docs("detail_name")
def detail_docs(self):
if self.name == "notify":
if self.gir_class is not None and not isinstance(
self.gir_class, ExternType
):
prop = self.gir_class.properties.get(self.tokens["detail_name"])
if prop is not None:
return prop.doc
@docs("=>")
def ref_docs(self):
return get_docs_section("Syntax Signal")
@decompiler("signal")
def decompile_signal(ctx, gir, name, handler, swapped="false", object=None):
def decompile_signal(
ctx: DecompileCtx, gir, name, handler, swapped=None, after="false", object=None
):
object_name = object or ""
if object_name == ctx.template_class:
object_name = "template"
name = name.replace("_", "-")
line = f"{name} => ${handler}({object_name})"
if decompile.truthy(swapped):
ctx.print(f"{name} => ${handler}({object_name}) swapped;")
else:
ctx.print(f"{name} => ${handler}({object_name});")
line += " swapped"
elif swapped is not None:
line += " not-swapped"
if decompile.truthy(after):
line += " after"
line += ";"
ctx.print(line)
return gir

View file

@ -17,8 +17,8 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..decompiler import escape_quote
from .attributes import BaseAttribute
import typing as T
from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type
@ -97,6 +97,16 @@ def get_types(gir):
}
allow_duplicates = [
"controls",
"described-by",
"details",
"flow-to",
"labelled-by",
"owns",
]
def _get_docs(gir, name):
name = name.replace("-", "_")
if gir_type := (
@ -107,11 +117,11 @@ def _get_docs(gir, name):
return gir_type.doc
class A11yProperty(BaseAttribute):
class A11yProperty(AstNode):
grammar = Statement(
UseIdent("name"),
":",
Value,
AnyOf(Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]),
)
@property
@ -132,8 +142,8 @@ class A11yProperty(BaseAttribute):
return self.tokens["name"].replace("_", "-")
@property
def value(self) -> Value:
return self.children[0]
def values(self) -> T.List[Value]:
return list(self.children)
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
@ -146,7 +156,7 @@ class A11yProperty(BaseAttribute):
SymbolKind.Field,
self.range,
self.group.tokens["name"].range,
self.value.range.text,
", ".join(v.range.text for v in self.values),
)
@validate("name")
@ -165,6 +175,20 @@ class A11yProperty(BaseAttribute):
check=lambda child: child.tokens["name"] == self.tokens["name"],
)
@validate("name")
def list_only_allowed_for_subset(self):
if self.tokens["list_form"] and self.tokens["name"] not in allow_duplicates:
raise CompileError(
f"'{self.tokens['name']}' does not allow a list of values",
)
@validate("name")
def list_non_empty(self):
if len(self.values) == 0:
raise CompileError(
f"'{self.tokens['name']}' may not be empty",
)
@docs("name")
def prop_docs(self):
if self.tokens["name"] in get_types(self.root.gir):
@ -199,6 +223,10 @@ class ExtAccessibility(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate accessibility block")
@docs("accessibility")
def ref_docs(self):
return get_docs_section("Syntax ExtAccessibility")
@completer(
applies_in=[ObjectContent],
@ -223,19 +251,37 @@ def a11y_name_completer(lsp, ast_node, match_variables):
)
@decompiler("relation", cdata=True)
def decompile_relation(ctx, gir, name, cdata):
ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name))
@decompiler("state", cdata=True)
def decompile_state(ctx, gir, name, cdata, translatable="false"):
if decompile.truthy(translatable):
ctx.print(f"{name}: _({escape_quote(cdata)});")
else:
ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name))
@decompiler("accessibility")
def decompile_accessibility(ctx, gir):
@decompiler("accessibility", skip_children=True, element=True)
def decompile_accessibility(ctx: DecompileCtx, _gir, element):
ctx.print("accessibility {")
already_printed = set()
types = get_types(ctx.gir)
for child in element.children:
name = child["name"]
if name in allow_duplicates:
if name in already_printed:
continue
ctx.print(f"{name}: [")
for value in element.children:
if value["name"] == name:
comments, string = ctx.decompile_value(
value.cdata,
types.get(value["name"]),
(value["translatable"], value["context"], value["comments"]),
)
ctx.print(f"{comments} {string},")
ctx.print("];")
else:
comments, string = ctx.decompile_value(
child.cdata,
types.get(child["name"]),
(child["translatable"], child["context"], child["comments"]),
)
ctx.print(f"{comments} {name}: {string};")
already_printed.add(name)
ctx.print("}")
ctx.end_block_with("")

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
@ -55,6 +54,10 @@ class Item(AstNode):
f"Duplicate item '{self.name}'", lambda x: x.name == self.name
)
@docs("name")
def ref_docs(self):
return get_docs_section("Syntax ExtComboBoxItems")
class ExtComboBoxItems(AstNode):
grammar = [
@ -81,6 +84,10 @@ class ExtComboBoxItems(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate items block")
@docs("items")
def ref_docs(self):
return get_docs_section("Syntax ExtComboBoxItems")
@completer(
applies_in=[ObjectContent],
@ -89,3 +96,29 @@ class ExtComboBoxItems(AstNode):
)
def items_completer(lsp, ast_node, match_variables):
yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]")
@decompiler("items", parent_type="Gtk.ComboBoxText")
def decompile_items(ctx: DecompileCtx, gir: gir.GirContext):
ctx.print("items [")
@decompiler("item", parent_type="Gtk.ComboBoxText", cdata=True)
def decompile_item(
ctx: DecompileCtx,
gir: gir.GirContext,
cdata: str,
id: T.Optional[str] = None,
translatable="false",
comments=None,
context=None,
):
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
)
if comments:
ctx.print(comments)
if id:
ctx.print(f"{id}: ")
ctx.print(translatable)
ctx.print(",")

View file

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

View file

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

View file

@ -11,7 +11,13 @@ from .types import TypeName
class ExtListItemFactory(AstNode):
grammar = [UseExact("id", "template"), Optional(TypeName), ObjectContent]
grammar = [
UseExact("id", "template"),
Mark("typename_start"),
Optional(TypeName),
Mark("typename_end"),
ObjectContent,
]
@property
def id(self) -> str:
@ -39,9 +45,12 @@ class ExtListItemFactory(AstNode):
@property
def gir_class(self):
if self.type_name is not None:
return self.type_name.gir_type
else:
return self.root.gir.get_type("ListItem", "Gtk")
@validate("template")
@validate("id")
def container_is_builder_list(self):
validate_parent_type(
self,
@ -50,17 +59,24 @@ class ExtListItemFactory(AstNode):
"sub-templates",
)
@validate("template")
@validate("id")
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate template block")
@validate()
@validate("typename_start", "typename_end")
def type_is_list_item(self):
if self.type_name is not None:
if self.type_name.glib_type_name != "GtkListItem":
raise CompileError(f"Only Gtk.ListItem is allowed as a type here")
if self.type_name.glib_type_name not in (
"GtkListItem",
"GtkListHeader",
"GtkColumnViewRow",
"GtkColumnViewCell",
):
raise CompileError(
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(
@ -87,8 +103,9 @@ 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")
def ref_docs(self):
return get_docs_section("Syntax ExtListItemFactory")

View file

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

View file

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

View file

@ -39,6 +39,12 @@ class Widget(AstNode):
self.group.tokens["name"].range,
)
def get_reference(self, _idx: int) -> T.Optional[LocationLink]:
if obj := self.context[ScopeCtx].objects.get(self.name):
return LocationLink(self.range, obj.range, obj.ranges["id"])
else:
return None
@validate("name")
def obj_widget(self):
object = self.context[ScopeCtx].objects.get(self.tokens["name"])
@ -88,6 +94,10 @@ class ExtSizeGroupWidgets(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate widgets block")
@docs("widgets")
def ref_docs(self):
return get_docs_section("Syntax ExtSizeGroupWidgets")
@completer(
applies_in=[ObjectContent],
@ -96,3 +106,13 @@ class ExtSizeGroupWidgets(AstNode):
)
def size_group_completer(lsp, ast_node, match_variables):
yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]")
@decompiler("widgets")
def size_group_decompiler(ctx, gir: gir.GirContext):
ctx.print("widgets [")
@decompiler("widget")
def widget_decompiler(ctx, gir: gir.GirContext, name: str):
ctx.print(name + ",")

View file

@ -57,7 +57,7 @@ class ExtStringListStrings(AstNode):
self.group.tokens["strings"].range,
)
@validate("items")
@validate("strings")
def container_is_string_list(self):
validate_parent_type(self, "Gtk", "StringList", "StringList items")
@ -65,6 +65,10 @@ class ExtStringListStrings(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate strings block")
@docs("strings")
def ref_docs(self):
return get_docs_section("Syntax ExtStringListStrings")
@completer(
applies_in=[ObjectContent],
@ -73,3 +77,25 @@ class ExtStringListStrings(AstNode):
)
def strings_completer(lsp, ast_node, match_variables):
yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]")
@decompiler("items", parent_type="Gtk.StringList")
def decompile_strings(ctx: DecompileCtx, gir: gir.GirContext):
ctx.print("strings [")
@decompiler("item", cdata=True, parent_type="Gtk.StringList")
def decompile_item(
ctx: DecompileCtx,
gir: gir.GirContext,
translatable="false",
comments=None,
context=None,
cdata=None,
):
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
)
if comments is not None:
ctx.print(comments)
ctx.print(translatable + ",")

View file

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

View file

@ -22,7 +22,7 @@ from functools import cached_property
from .common import *
from .gobject_object import Object
from .response_id import ExtResponse
from .response_id import ExtResponse, decompile_response_type
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [
("Gtk", "Buildable"),
@ -53,6 +53,10 @@ class ChildExtension(AstNode):
def child(self) -> ExtResponse:
return self.children[0]
@docs()
def ref_docs(self):
return get_docs_section("Syntax ChildExtension")
class ChildAnnotation(AstNode):
grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"]
@ -127,10 +131,15 @@ class Child(AstNode):
)
@decompiler("child")
def decompile_child(ctx, gir, type=None, internal_child=None):
if type is not None:
@decompiler("child", element=True)
def decompile_child(ctx, gir, element):
if type := element["type"]:
if type == "action":
if decompiled := decompile_response_type(ctx.parent_node, element):
ctx.print(decompiled)
return
ctx.print(f"[{type}]")
elif internal_child is not None:
elif internal_child := element["internal-child"]:
ctx.print(f"[internal-child {internal_child}]")
return gir

View file

@ -88,6 +88,10 @@ class Template(Object):
f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",
)
@docs("id")
def ref_docs(self):
return get_docs_section("Syntax Template")
@decompiler("template")
def decompile_template(ctx: DecompileCtx, gir, klass, parent=None):
@ -97,8 +101,9 @@ def decompile_template(ctx: DecompileCtx, gir, klass, parent=None):
else:
return "$" + cname
if parent is None:
ctx.print(f"template {class_name(klass)} {{")
else:
ctx.print(f"template {class_name(klass)} : {class_name(parent)} {{")
ctx.template_class = klass
return ctx.type_by_cname(klass) or ctx.type_by_cname(parent)

View file

@ -59,15 +59,13 @@ 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")
@docs()
def ref_docs(self):
return get_docs_section("Syntax GtkDecl")
class Import(AstNode):
grammar = Statement(
@ -86,11 +84,26 @@ 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):
if self.namespace not in self.root.used_imports:
raise UnusedWarning(
f"Unused import: {self.namespace}",
self.range,
actions=[
CodeAction("Remove import", "", self.range.with_trailing_newline)
],
)
@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
@docs()
def ref_docs(self):
return get_docs_section("Syntax Using")

View file

@ -123,3 +123,42 @@ class ExtResponse(AstNode):
object = self.parent_by_type(Child).object
return object.id
@docs()
def ref_docs(self):
return get_docs_section("Syntax ExtResponse")
@docs("response_id")
def response_id_docs(self):
if enum := self.root.gir.get_type("ResponseType", "Gtk"):
if member := enum.members.get(self.response_id, None):
return member.doc
def decompile_response_type(parent_element, child_element):
obj_id = None
for obj in child_element.children:
if obj.tag == "object":
obj_id = obj["id"]
break
if obj_id is None:
return None
for child in parent_element.children:
if child.tag == "action-widgets":
for action_widget in child.children:
if action_widget.cdata == obj_id:
response_id = action_widget["response"]
is_default = (
" default" if decompile.truthy(action_widget["default"]) else ""
)
return f"[action response={response_id}{is_default}]"
return None
@decompiler("action-widgets", skip_children=True)
def decompile_action_widgets(ctx, gir):
# This is handled in the <child> decompiler and decompile_response_type above
pass

View file

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

View file

@ -78,9 +78,10 @@ class TypeName(AstNode):
)
@property
def gir_ns(self):
def gir_ns(self) -> T.Optional[gir.Namespace]:
if not self.tokens["extern"]:
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
return None
@property
def gir_type(self) -> gir.GirType:

View file

@ -27,6 +27,7 @@ from .gtk_menu import Menu, menu
from .gtkbuilder_template import Template
from .imports import GtkDirective, Import
from .translation_domain import TranslationDomain
from .types import TypeName
class UI(AstNode):
@ -121,6 +122,22 @@ class UI(AstNode):
Range(pos, pos, self.group.text),
)
@cached_property
def used_imports(self) -> T.Optional[T.Set[str]]:
def _iter_recursive(node: AstNode):
yield node
for child in node.children:
if isinstance(child, AstNode):
yield from _iter_recursive(child)
result = set()
for node in _iter_recursive(self):
if isinstance(node, TypeName):
ns = node.gir_ns
if ns is not None:
result.add(ns.name)
return result
@context(ScopeCtx)
def scope_ctx(self) -> ScopeCtx:
return ScopeCtx(node=self)

View file

@ -19,8 +19,12 @@
import typing as T
from blueprintcompiler.gir import ArrayType
from blueprintcompiler.lsp_utils import SemanticToken
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx
from .expression import Expression
from .gobject_object import Object
from .types import TypeName
@ -54,6 +58,23 @@ class Translated(AstNode):
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()
def ref_docs(self):
return get_docs_section("Syntax Translated")
class TypeLiteral(AstNode):
grammar = [
@ -99,6 +120,10 @@ class TypeLiteral(AstNode):
],
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax TypeLiteral")
class QuotedLiteral(AstNode):
grammar = UseQuoted("value")
@ -200,15 +225,22 @@ class Flag(AstNode):
return self.tokens["value"]
@property
def value(self) -> T.Optional[int]:
def value(self) -> T.Optional[str]:
type = self.context[ValueTypeCtx].value_type
if not isinstance(type, Enumeration):
return None
elif member := type.members.get(self.name):
return member.value
return member.nick
else:
return None
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
yield SemanticToken(
self.group.tokens["value"].start,
self.group.tokens["value"].end,
SemanticTokenType.EnumMember,
)
@docs()
def docs(self):
type = self.context[ValueTypeCtx].value_type
@ -249,6 +281,10 @@ class Flags(AstNode):
if expected_type is not None and not isinstance(expected_type, gir.Bitfield):
raise CompileError(f"{expected_type.full_name} is not a bitfield type")
@docs()
def ref_docs(self):
return get_docs_section("Syntax Flags")
class IdentLiteral(AstNode):
grammar = UseIdent("value")
@ -297,7 +333,12 @@ class IdentLiteral(AstNode):
if self.ident == "null":
if not self.context[ValueTypeCtx].allow_null:
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(
f"Could not find object with ID {self.ident}",
did_you_mean=(
@ -385,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):
grammar = AnyOf(Translated, Flags, Literal)
@ -395,6 +465,68 @@ class Value(AstNode):
return self.children[0]
class ArrayValue(AstNode):
grammar = ["[", Delimited(Value, ","), "]"]
@validate()
def validate_for_type(self) -> None:
expected_type = self.gir_type
if expected_type is not None and not isinstance(expected_type, gir.ArrayType):
raise CompileError(f"Cannot assign array to {expected_type.full_name}")
if expected_type is not None and not isinstance(
expected_type.inner, StringType
):
raise CompileError("Only string arrays are supported")
@validate()
def validate_invalid_newline(self) -> None:
expected_type = self.gir_type
if isinstance(expected_type, gir.ArrayType) and isinstance(
expected_type.inner, StringType
):
errors = []
for value in self.values:
if isinstance(value.child, Literal) and isinstance(
value.child.value, QuotedLiteral
):
quoted_literal = value.child.value
literal_value = quoted_literal.value
# literal_value can be None if there's an invalid escape sequence
if literal_value is not None and "\n" in literal_value:
errors.append(
CompileError(
"String literals inside arrays can't contain newlines",
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:
raise MultipleErrors(errors)
@property
def values(self) -> T.List[Value]:
return self.children
@property
def gir_type(self):
return self.parent.context[ValueTypeCtx].value_type
@context(ValueTypeCtx)
def child_value(self):
if self.gir_type is None or not isinstance(self.gir_type, ArrayType):
return ValueTypeCtx(None)
else:
return ValueTypeCtx(self.gir_type.inner)
class StringValue(AstNode):
grammar = AnyOf(Translated, QuotedLiteral)

View file

@ -102,10 +102,10 @@ class OpenFile:
]
# convert line, column numbers to deltas
for i, token_list in enumerate(token_lists[1:]):
token_list[0] -= token_lists[i][0]
if token_list[0] == 0:
token_list[1] -= token_lists[i][1]
for a, b in zip(token_lists[-2::-1], token_lists[:0:-1]):
b[0] -= a[0]
if b[0] == 0:
b[1] -= a[1]
# flatten the list
return [x for y in token_lists for x in y]
@ -118,6 +118,7 @@ class LanguageServer:
self.client_capabilities = {}
self.client_supports_completion_choice = False
self._open_files: T.Dict[str, OpenFile] = {}
self._exited = False
def run(self):
# Read <doc> tags from gir files. During normal compilation these are
@ -125,7 +126,7 @@ class LanguageServer:
xml_reader.PARSE_GIR.add("doc")
try:
while True:
while not self._exited:
line = ""
content_len = -1
while content_len == -1 or (line != "\n" and line != "\r\n"):
@ -149,7 +150,7 @@ class LanguageServer:
def _send(self, data):
data["jsonrpc"] = "2.0"
line = json.dumps(data, separators=(",", ":")) + "\r\n"
line = json.dumps(data, separators=(",", ":"))
printerr("output: " + line)
sys.stdout.write(
f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}"
@ -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")
def didOpen(self, id, params):
doc = params.get("textDocument")
@ -472,9 +481,12 @@ class LanguageServer:
),
}
if isinstance(err, DeprecationWarning):
if isinstance(err, DeprecatedWarning):
result["tags"] = [DiagnosticTag.Deprecated]
if isinstance(err, UnusedWarning):
result["tags"] = [DiagnosticTag.Unnecessary]
if len(err.references) > 0:
result["relatedInformation"] = [
{

View file

@ -19,6 +19,8 @@
import enum
import json
import os
import typing as T
from dataclasses import dataclass, field
@ -84,6 +86,7 @@ class Completion:
docs: T.Optional[str] = None
text: T.Optional[str] = None
snippet: T.Optional[str] = None
detail: T.Optional[str] = None
def to_json(self, snippets: bool):
insert_text = self.text or self.label
@ -96,7 +99,8 @@ class Completion:
"label": self.label,
"kind": self.kind,
"tags": [CompletionItemTag.Deprecated] if self.deprecated else None,
"detail": self.signature,
# https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemLabelDetails
"labelDetails": ({"detail": self.signature} if self.signature else None),
"documentation": (
{
"kind": "markdown",
@ -109,6 +113,7 @@ class Completion:
"sortText": self.sort_text,
"insertText": insert_text,
"insertTextFormat": insert_text_format,
"detail": self.detail if self.detail else None,
}
return {k: v for k, v in result.items() if v is not None}
@ -197,3 +202,27 @@ class TextEdit:
def to_json(self):
return {"range": self.range.to_json(), "newText": self.newText}
_docs_sections: T.Optional[dict[str, T.Any]] = None
def get_docs_section(section_name: str) -> T.Optional[str]:
global _docs_sections
if _docs_sections is None:
try:
with open(
os.path.join(os.path.dirname(__file__), "reference_docs.json")
) as f:
_docs_sections = json.load(f)
except FileNotFoundError:
_docs_sections = {}
if section := _docs_sections.get(section_name):
content = section["content"]
link = section["link"]
content += f"\n\n---\n\n[Online documentation]({link})"
return content
else:
return None

View file

@ -25,6 +25,7 @@ import sys
import typing as T
from . import formatter, interactive_port, parser, tokenizer
from .decompiler import decompile_string
from .errors import CompileError, CompilerBugError, PrintableError, report_bug
from .gir import add_typelib_search_path
from .lsp import LanguageServer
@ -90,12 +91,28 @@ class BlueprintApp:
default=2,
type=int,
)
format.add_argument(
"-n",
"--no-diff",
help="Do not print a full diff of the changes",
default=False,
action="store_true",
)
format.add_argument(
"inputs",
nargs="+",
metavar="filenames",
)
decompile = self.add_subcommand(
"decompile", "Convert .ui XML files to blueprint", self.cmd_decompile
)
decompile.add_argument("--output", dest="output", default="-")
decompile.add_argument("--typelib-path", nargs="?", action="append")
decompile.add_argument(
"input", metavar="filename", default=sys.stdin, type=argparse.FileType("r")
)
port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port)
lsp = self.add_subcommand(
@ -221,6 +238,7 @@ class BlueprintApp:
file.write(formatted_str)
happened = "Formatted"
if not opts.no_diff:
diff_lines = []
a_lines = data.splitlines(keepends=True)
b_lines = formatted_str.splitlines(keepends=True)
@ -238,6 +256,7 @@ class BlueprintApp:
diff_lines.append("\\ No newline at end of file\n")
print("".join(diff_lines))
to_print = Colors.BOLD
if errored:
to_print += f"{Colors.RED}Skipped {file.name}: Will not overwrite file with compile errors"
@ -291,6 +310,24 @@ class BlueprintApp:
if panic:
sys.exit(1)
def cmd_decompile(self, opts):
if opts.typelib_path != None:
for typelib_path in opts.typelib_path:
add_typelib_search_path(typelib_path)
data = opts.input.read()
try:
decompiled = decompile_string(data)
if opts.output == "-":
print(decompiled)
else:
with open(opts.output, "w") as file:
file.write(decompiled)
except PrintableError as e:
e.pretty_print(opts.input.name, data, stream=sys.stderr)
sys.exit(1)
def cmd_lsp(self, opts):
langserv = LanguageServer()
langserv.run()

View file

@ -134,11 +134,25 @@ class XmlOutput(OutputFormat):
self._emit_expression(value.expression, xml)
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):
xml.start_tag("property", **props)
self._emit_object(value.object, xml)
xml.end_tag()
elif isinstance(value, ArrayValue):
xml.start_tag("property", **props)
values = list(value.values)
for value in values[:-1]:
self._emit_value(value, xml)
xml.put_text("\n")
self._emit_value(values[-1], xml)
xml.end_tag()
else:
raise CompilerBugError()
@ -150,7 +164,7 @@ class XmlOutput(OutputFormat):
elif isinstance(translated, QuotedLiteral):
return {}
else:
return {"translatable": "true", "context": translated.translate_context}
return {"translatable": "yes", "context": translated.translate_context}
def _emit_signal(self, signal: Signal, xml: XmlEmitter):
name = signal.name
@ -160,7 +174,8 @@ 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
),
@ -208,12 +223,6 @@ class XmlOutput(OutputFormat):
xml.put_text(
"|".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:
raise CompilerBugError()
@ -235,6 +244,9 @@ class XmlOutput(OutputFormat):
raise CompilerBugError()
def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter):
if expr.is_this:
return
if expr.is_object:
xml.start_tag("constant")
else:
@ -282,8 +294,11 @@ class XmlOutput(OutputFormat):
def _emit_extensions(self, extension, xml: XmlEmitter):
if isinstance(extension, ExtAccessibility):
xml.start_tag("accessibility")
for prop in extension.properties:
self._emit_attribute(prop.tag_name, "name", prop.name, prop.value, xml)
for property in extension.properties:
for val in property.values:
self._emit_attribute(
property.tag_name, "name", property.name, val, xml
)
xml.end_tag()
elif isinstance(extension, AdwBreakpointCondition):
@ -293,6 +308,9 @@ class XmlOutput(OutputFormat):
elif isinstance(extension, AdwBreakpointSetters):
for setter in extension.setters:
if setter.value is None:
continue
attrs = {}
if isinstance(setter.value.child, Translated):
@ -353,12 +371,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)
@ -375,9 +394,9 @@ class XmlOutput(OutputFormat):
xml.end_tag()
elif isinstance(extension, ExtListItemFactory):
child_xml = XmlEmitter()
child_xml = XmlEmitter(generated_notice=False)
child_xml.start_tag("interface")
child_xml.start_tag("template", **{"class": "GtkListItem"})
child_xml.start_tag("template", **{"class": extension.gir_class})
self._emit_object_or_template(extension, child_xml)
child_xml.end_tag()
child_xml.end_tag()

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():
@ -71,6 +73,7 @@ class XmlEmitter:
self._needs_newline = False
def put_cdata(self, text: str):
text = text.replace("]]>", "]]]]><![CDATA[>")
self.result += f"<![CDATA[{text}]]>"
self._needs_newline = False

View file

@ -17,7 +17,7 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
""" Utilities for parsing an AST from a token stream. """
"""Utilities for parsing an AST from a token stream."""
import typing as T
from enum import Enum
@ -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

@ -33,7 +33,9 @@ def parse(
original_text = tokens[0].string if len(tokens) else ""
ctx = ParseContext(tokens, original_text)
AnyOf(UI).parse(ctx)
ast_node = ctx.last_group.to_ast() if ctx.last_group else None
assert ctx.last_group is not None
ast_node = ctx.last_group.to_ast()
errors = [*ctx.errors, *ast_node.errors]
warnings = [*ctx.warnings, *ast_node.warnings]

View file

@ -127,6 +127,13 @@ class Range:
def text(self) -> str:
return self.original_text[self.start : self.end]
@property
def with_trailing_newline(self) -> "Range":
if len(self.original_text) > self.end and self.original_text[self.end] == "\n":
return Range(self.start, self.end + 1, self.original_text)
else:
return self
@staticmethod
def join(a: T.Optional["Range"], b: T.Optional["Range"]) -> T.Optional["Range"]:
if a is None:

View file

@ -148,8 +148,8 @@ class Typelib:
SIGNATURE_ARGUMENTS = Field(0x8, "offset")
ATTR_OFFSET = Field(0x0, "u32")
ATTR_NAME = Field(0x0, "string")
ATTR_VALUE = Field(0x0, "string")
ATTR_NAME = Field(0x4, "string")
ATTR_VALUE = Field(0x8, "string")
TYPE_BLOB_TAG = Field(0x0, "u8", 3, 5)
TYPE_BLOB_INTERFACE = Field(0x2, "dir_entry")

View file

@ -109,14 +109,14 @@ class UnescapeError(Exception):
def escape_quote(string: str) -> str:
return (
"'"
'"'
+ (
string.replace("\\", "\\\\")
.replace("'", "\\'")
.replace('"', '\\"')
.replace("\n", "\\n")
.replace("\t", "\\t")
)
+ "'"
+ '"'
)

View file

@ -7,8 +7,8 @@ git clone --depth=1 https://gitlab.gnome.org/GNOME/gtk.git
cd gtk
meson setup builddir \
--prefix=/usr \
-Dgtk_doc=true \
-Ddemos=false \
-Ddocumentation=true \
-Dbuild-demos=false \
-Dbuild-examples=false \
-Dbuild-tests=false \
-Dbuild-testsuite=false

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

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

View file

@ -16,8 +16,8 @@ a module in your flatpak manifest:
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/jwestman/blueprint-compiler",
"tag": "v0.12.0"
"url": "https://gitlab.gnome.org/GNOME/blueprint-compiler",
"tag": "v0.16.0"
}
]
}

View file

@ -26,7 +26,7 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces.
using Gtk 4.0;
template MyAppWindow : ApplicationWindow {
template $MyAppWindow: ApplicationWindow {
default-width: 600;
default-height: 300;
title: _("Hello, Blueprint!");
@ -35,7 +35,7 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces.
HeaderBar {}
Label {
label: bind MyAppWindow.main_text;
label: bind template.main_text;
}
}
@ -59,7 +59,7 @@ Features
Links
-----
- `Source code <https://gitlab.gnome.org/jwestman/blueprint-compiler>`_
- `Source code <https://gitlab.gnome.org/GNOME/blueprint-compiler>`_
- `Workbench <https://github.com/sonnyp/Workbench>`_ lets you try, preview and export Blueprint
- `GNOME Builder <https://developer.gnome.org/documentation/introduction/builder.html>`_ provides builtin support
- `Vim syntax highlighting plugin by thetek42 <https://github.com/thetek42/vim-blueprint-syntax>`_
@ -82,10 +82,12 @@ Built with Blueprint
- `Blurble <https://gitlab.gnome.org/World/Blurble>`_
- `Bottles <https://github.com/bottlesdevs/Bottles>`_
- `Cartridges <https://github.com/kra-mo/cartridges>`_
- `Cassette <https://gitlab.gnome.org/Rirusha/Cassette>`_
- `Cavalier <https://github.com/NickvisionApps/Cavalier>`_
- `Chance <https://zelikos.dev/apps/rollit>`_
- `Commit <https://github.com/sonnyp/Commit/>`_
- `Confy <https://confy.kirgroup.net/>`_
- `Cozy <https://github.com/geigi/cozy>`_
- `Daikhan <https://github.com/flathub/io.gitlab.daikhan.stable>`_
- `Damask <https://gitlab.gnome.org/subpop/damask>`_
- `Denaro <https://github.com/NickvisionApps/Denaro>`_
@ -93,7 +95,7 @@ Built with Blueprint
- `Dev Toolbox <https://github.com/aleiepure/devtoolbox>`_
- `Dialect <https://github.com/dialect-app/dialect>`_
- `Diccionario de la Lengua <https://codeberg.org/rafaelmardojai/diccionario-lengua>`_
- `Dogg <https://gitlab.gnome.org/sungsphinx/Doggo>`_
- `Doggo <https://gitlab.gnome.org/sungsphinx/Doggo>`_
- `Dosage <https://github.com/diegopvlk/Dosage>`_
- `Dynamic Wallpaper <https://github.com/dusansimic/dynamic-wallpaper>`_
- `Extension Manager <https://github.com/mjakeman/extension-manager>`_
@ -115,11 +117,10 @@ Built with Blueprint
- `Identity <https://gitlab.gnome.org/YaLTeR/identity>`_
- `Jogger <https://codeberg.org/baarkerlounger/jogger>`_
- `Junction <https://github.com/sonnyp/Junction/>`_
- `Komiku <https://github.com/flathub/info.febvre.Komikku>`_
- `Komikku <https://codeberg.org/valos/Komikku>`_
- `Letterpress <https://gitlab.gnome.org/World/Letterpress>`_
- `Login Manager Settings <https://github.com/realmazharhussain/gdm-settings>`_
- `Maniatic Launcher <https://github.com/santiagocezar/maniatic-launcher/>`_
- `Maniatic Launcher <https://github.com/santiagocezar/maniatic-launcher/>`_
- `Master Key <https://gitlab.com/guillermop/master-key/>`_
- `Misson Center <https://github.com/flathub/io.missioncenter.MissionCenter>`_
- `NewCaw <https://github.com/CodedOre/NewCaw>`_

View file

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

View file

@ -21,7 +21,7 @@ The tokenizer encountered an unexpected sequence of characters that aren't part
child_not_accepted
------------------
The parent class does not have child widgets (it does not implement `Gtk.Buildable <https://docs.gtk.org/gtk4/iface.Buildable.html>`_ and is not a subclass of `Gio.ListStore <https://docs.gtk.org/gio/class.ListStore.html>`_). Some classes use properties instead of children to add widgets. Check the parent class's documentation.
The parent class does not have child objects (it does not implement `Gtk.Buildable <https://docs.gtk.org/gtk4/iface.Buildable.html>`_ and is not a subclass of `Gio.ListStore <https://docs.gtk.org/gio/class.ListStore.html>`_). Some classes use properties instead of children to add widgets. Check the parent class's documentation.
.. _Diagnostic conversion_error:

View file

@ -8,7 +8,7 @@ automatically.
.. code-block:: blueprint
label: bind MyAppWindow.account.username;
label: bind template.account.username;
/* ^ ^ ^
| creates lookup expressions that are re-evaluated when
| the account's username *or* the account itself changes
@ -42,22 +42,22 @@ Expressions are composed of property lookups and/or closures. Property lookups a
.. _Syntax LookupExpression:
Lookup Expressions
------------------
Lookups
-------
.. rst-class:: grammar-block
LookupExpression = '.' <property::ref:`IDENT<Syntax IDENT>`>
Lookup expressions perform a GObject property lookup on the preceding expression. They are recalculated whenever the property changes, using the `notify signal <https://docs.gtk.org/gobject/signal.Object.notify.html>`_
Lookup expressions perform a GObject property lookup on the preceding expression. They are recalculated whenever the property changes, using the `notify signal <https://docs.gtk.org/gobject/signal.Object.notify.html>`_.
The type of a property expression is the type of the property it refers to.
.. _Syntax ClosureExpression:
Closure Expressions
-------------------
Closures
--------
.. rst-class:: grammar-block
@ -65,23 +65,48 @@ Closure Expressions
Closure expressions allow you to perform additional calculations that aren't supported in blueprint by writing those calculations as application code. These application-defined functions are created in the same way as :ref:`signal handlers<Syntax Signal>`.
Expressions are only reevaluated when their inputs change. Because blueprint doesn't manage a closure's application code, it can't tell what changes might affect the result. Therefore, closures must be *pure*, or deterministic. They may only calculate the result based on their immediate inputs, properties of their inputs or outside variables.
Expressions are only reevaluated when their inputs change. Because blueprint doesn't manage a closure's application code, it can't tell what changes might affect the result. Therefore, closures must be *pure*, or deterministic. They may only calculate the result based on their immediate inputs, not properties of their inputs or outside variables.
Blueprint doesn't know the closure's return type, so closure expressions must be cast to the correct return type using a :ref:`cast expression<Syntax CastExpression>`.
.. _Syntax CastExpression:
Cast Expressions
----------------
Casts
-----
.. rst-class:: grammar-block
CastExpression = 'as' '<' :ref:`TypeName<Syntax TypeName>` '>'
Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it.
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
// 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

@ -10,7 +10,7 @@ Properties are the main way to set values on objects, but they are limited by th
Extensions are a feature of ``Gtk.Buildable``--see `Gtk.Buildable.custom_tag_start() <https://docs.gtk.org/gtk4/vfunc.Buildable.custom_tag_start.html>`_ for internal details.
Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue <https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues>`_.
Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue <https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues>`_.
.. rst-class:: grammar-block
@ -37,12 +37,15 @@ Accessibility Properties
.. rst-class:: grammar-block
ExtAccessibility = 'accessibility' '{' ExtAccessibilityProp* '}'
ExtAccessibilityProp = <name::ref:`IDENT<Syntax IDENT>`> ':' :ref:`Value <Syntax Value>` ';'
ExtAccessibilityProp = <name::ref:`IDENT<Syntax IDENT>`> ':' (:ref:`Value <Syntax Value>` | ('[' (:ref: Value <Syntax Value>),* ']') ) ';'
Valid in any `Gtk.Widget <https://docs.gtk.org/gtk4/class.Widget.html>`_.
The ``accessibility`` block defines values relevant to accessibility software. The property names and acceptable values are described in the `Gtk.AccessibleRelation <https://docs.gtk.org/gtk4/enum.AccessibleRelation.html>`_, `Gtk.AccessibleState <https://docs.gtk.org/gtk4/enum.AccessibleState.html>`_, and `Gtk.AccessibleProperty <https://docs.gtk.org/gtk4/enum.AccessibleProperty.html>`_ enums.
.. note::
Relations which allow for a list of values, for example `labelled-by`, must be given as a single relation with a list of values instead of duplicating the relation like done in Gtk.Builder.
.. _Syntax ExtAdwBreakpoint:
@ -224,7 +227,7 @@ Valid in `Gtk.BuilderListItemFactory <https://docs.gtk.org/gtk4/class.BuilderLis
The ``template`` block defines the template that will be used to create list items. This block is unique within Blueprint because it defines a completely separate sub-blueprint which is used to create each list item. The sub-blueprint may not reference objects in the main blueprint or vice versa.
The template type must be `Gtk.ListItem <https://docs.gtk.org/gtk4/class.ListItem.html>`_. The template object can be referenced with the ``template`` keyword.
The template type must be `Gtk.ListItem <https://docs.gtk.org/gtk4/class.ListItem.html>`_, `Gtk.ColumnViewRow <https://docs.gtk.org/gtk4/class.ColumnViewRow.html>`_, or `Gtk.ColumnViewCell <https://docs.gtk.org/gtk4/class.ColumnViewCell.html>`_. The template object can be referenced with the ``template`` keyword.
.. code-block:: blueprint

View file

@ -31,7 +31,7 @@ Tokens
IDENT
~~~~~
An identifier starts with an ASCII underscore ``_`` or letter ``[A-Za-z]`` and consists of ASCII underscores, letters, digits ``[0-9]``, and dashes ``-``. Dashes are included for historical reasons, since GObject properties are traditionally kebab-case.
An identifier starts with an ASCII underscore ``_`` or letter ``[A-Za-z]`` and consists of ASCII underscores, letters, digits ``[0-9]``, and dashes ``-``. Dashes are included for historical reasons, since GObject properties and signals are traditionally kebab-case.
.. _Syntax NUMBER:

View file

@ -58,7 +58,7 @@ Properties
.. 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.
@ -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

@ -90,6 +90,7 @@ To reference the template object in a binding or expression, use the ``template`
Language Implementations
------------------------
- ``gtk_widget_class_set_template ()`` in C: https://docs.gtk.org/gtk4/class.Widget.html#building-composite-widgets-from-template-xml
- ``#[template]`` in gtk-rs: https://gtk-rs.org/gtk4-rs/stable/latest/book/composite_templates.html
- ``GObject.registerClass()`` in GJS: https://gjs.guide/guides/gtk/3/14-templates.html
- **C** ``gtk_widget_class_set_template ()``: https://docs.gtk.org/gtk4/class.Widget.html#building-composite-widgets-from-template-xml
- **gtk-rs** ``#[template]``: https://gtk-rs.org/gtk4-rs/stable/latest/book/composite_templates.html
- **GJS** ``GObject.registerClass()``: https://gjs.guide/guides/gtk/3/14-templates.html
- **PyGObject** ``@Gtk.Template``: https://pygobject.gnome.org/guide/gtk_template.html

View file

@ -25,8 +25,7 @@ Literals
NumberLiteral = ( '-' | '+' )? <value::ref:`NUMBER<Syntax NUMBER>`>
IdentLiteral = <ident::ref:`IDENT<Syntax IDENT>`>
Literals are used to specify values for properties. They can be strings, numbers, references to objects, types, boolean values, or enum members.
Literals are used to specify values for properties. They can be strings, numbers, references to objects, ``null``, types, boolean values, or enum members.
.. _Syntax TypeLiteral:
@ -110,7 +109,7 @@ Bindings
.. rst-class:: grammar-block
Binding = 'bind' :ref:`Expression<Syntax Expression>` (BindingFlag)*
BindingFlag = 'inverted' | 'bidirectional' | 'sync-create'
BindingFlag = 'inverted' | 'bidirectional' | 'no-sync-create'
Bindings keep a property updated as other properties change. They can be used to keep the UI in sync with application data, or to connect two parts of the UI.
@ -121,8 +120,8 @@ Simple Bindings
A binding that consists of a source object and a single lookup is called a "simple binding". These are implemented using `GObject property bindings <https://docs.gtk.org/gobject/method.Object.bind_property.html>`_ and support a few flags:
- ``bidirectional``: The binding is two-way, so changes to the target property will also update the source property.
- ``inverted``: For boolean properties, the target is set to the inverse of the source property.
- ``bidirectional``: The binding is two-way, so changes to the target property will also update the source property.
- ``no-sync-create``: Normally, when a binding is created, the target property is immediately updated with the current value of the source property. This flag disables that behavior, and the bound property will be updated the next time the source property changes.
Complex Bindings
@ -138,14 +137,13 @@ Example
/* Use bindings to show a label when a switch
* is active, without any application code */
Switch advanced_feature {}
Switch show_label {}
Label warning {
visible: bind advanced_feature.active;
label: _("This is an advanced feature. Use with caution!");
Label {
visible: bind show_label.active;
label: _("I'm a label that's only visible when the switch is enabled!");
}
.. _Syntax ObjectValue:
Object Values
@ -170,3 +168,14 @@ String Values
StringValue = :ref:`Translated<Syntax Translated>` | :ref:`QuotedLiteral<Syntax Literal>`
Menus, as well as some :ref:`extensions<Syntax Extension>`, have properties that can only be string literals or translated strings.
.. _Syntax ArrayValue:
Array Values
-------------
.. rst-class:: grammar-block
ArrayValue = '[' (:ref:`StringValue<Syntax StringValue>`),* ']'
For now, it only supports :ref:`Strings<Syntax StringValue>`. This is because Gtk.Builder only supports string arrays.

View file

@ -8,7 +8,7 @@ Setting up Blueprint on a new or existing project
Using the porting tool
~~~~~~~~~~~~~~~~~~~~~~
Clone `blueprint-compiler <https://gitlab.gnome.org/jwestman/blueprint-compiler>`_
Clone `blueprint-compiler <https://gitlab.gnome.org/GNOME/blueprint-compiler>`_
from source. You can install it using ``meson _build`` and ``ninja -C _build install``,
or you can leave it uninstalled.
@ -29,7 +29,7 @@ blueprint-compiler works as a meson subproject.
[wrap-git]
directory = blueprint-compiler
url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git
url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git
revision = main
depth = 1

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')
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
--------

View file

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

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;
styles [
"destructive"
"destructive",
]
}

View file

@ -1 +1 @@
test('tests', py, args: ['-m', 'unittest'], workdir: meson.source_root())
test('tests', py, args: ['-m', 'unittest'], workdir: meson.project_source_root())

View file

@ -0,0 +1,9 @@
using Gtk 4.0;
Box {
accessibility {
label: _("Hello, world!");
labelled-by: [];
checked: true;
}
}

View file

@ -0,0 +1 @@
6,5,11,'labelled-by' may not be empty

View file

@ -0,0 +1,15 @@
using Gtk 4.0;
Box {
accessibility {
label: _("Hello, world!");
active-descendant: [my_label1, my_label2, my_label3];
checked: true;
}
}
Label my_label1 {}
Label my_label2 {}
Label my_label3 {}

View file

@ -0,0 +1 @@
6,5,17,'active-descendant' does not allow a list of values

View file

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

View file

@ -0,0 +1 @@
4,12,3,Cannot assign array to string

View file

@ -0,0 +1,6 @@
using Gtk 4.0;
AboutDialog about {
valign: center;
authors: [1];
}

View file

@ -0,0 +1 @@
5,15,1,Cannot convert number to string

View file

@ -1,10 +0,0 @@
using Gtk 4.0;
using Gio 2.0;
Dialog {
use-header-bar: 1;
}
Window {
keys-changed => $on_window_keys_changed();
}

View file

@ -1 +0,0 @@
4,1,6,Gtk.Dialog is deprecated

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

@ -1,3 +1,2 @@
3,10,12,Use type syntax here (introduced in blueprint 0.8.0)
8,1,6,Gtk.Dialog is deprecated
9,18,12,Use 'template' instead of the class name (introduced in 0.8.0)

View file

@ -1 +1 @@
4,3,17,Only Gtk.ListItem is allowed as a type here
4,11,6,Only Gtk.ListItem, Gtk.ListHeader, Gtk.ColumnViewRow, or Gtk.ColumnViewCell is allowed as a type here

View file

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

Some files were not shown because too many files have changed in this diff Show more