Compare commits

..

291 commits
v0.2.0 ... main

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
Sonny Piers
66b43c36cf Release v0.12.0 2024-03-21 20:37:27 +01:00
Sonny Piers
aa19e06d28 docs: Add more apps making use of Blueprint 2024-03-21 20:21:08 +01:00
Sonny Piers
d47955c5a2 Document AdwMessageDialog and AdwAlertDialog separately 2024-02-09 12:40:24 +01:00
Sonny Piers
05d6ff1fd7 Disable completer if applies_in_subclass type is not found
https://gitlab.gnome.org/jwestman/blueprint-compiler/-/merge_requests/177#note_1990521
2024-02-02 11:03:16 +01:00
Sonny Piers
ba8b492134 Add support for Adw.AlertDialog 2024-02-02 11:03:16 +01:00
James Westman
6522421251 Fix formatting 2024-02-01 19:38:19 -06:00
James Westman
dc42556487 ci: Add glslc to Dockerfile
New GTK dependency.
2024-02-01 19:34:14 -06:00
James Westman
a689150a8b errors: Print code actions in error message
Previously they were only exposed by the language server, but now the
command line will print the old and new text.
2024-01-25 20:02:02 -06:00
James Westman
dd2e9a10cb docs: Fix another upgrade warning 2024-01-25 19:49:00 -06:00
James Westman
2bf4fa855e docs: Fix typo 2024-01-25 19:46:29 -06:00
James Westman
71a43a4a86 docs: Add section on referencing templates 2024-01-08 19:30:27 -06:00
James Westman
8a6ad847b6 docs: Fix typo 2024-01-08 19:30:27 -06:00
Gregor Niehl
b79c78bb74 tests: Update to reflect current foramtting style 2023-12-29 02:30:13 +00:00
Gregor Niehl
cb30bec7b1 decompiler: Format resulting Blueprints 2023-12-29 02:30:13 +00:00
James Westman
2e9db2eca5
errors: Fix bug when compiling empty file 2023-12-24 21:40:45 -06:00
James Westman
a8c6d5d342 docs: Fix typo 2023-12-21 19:38:12 -06:00
Gregor Niehl
0cdccf5f54 Formatter CLI: Error if no files are found 2023-12-22 01:33:17 +00:00
gregorni
e5cde71fc1 Tiny formatter improvements 2023-12-19 02:29:09 +00:00
James Westman
e261180dcc language: Add translation-domain
This allows you to set the translation domain of a blueprint file.
2023-12-13 23:43:29 +00:00
gregorni
c5fa33363f formatter: Handle Inline comments 2023-12-13 02:12:50 +00:00
gregorni
9cfacb9898 Apply isort and black formatting everywhere 2023-12-13 01:36:18 +00:00
James Westman
80aaee374d formatter: Tweak whitespace in special cases 2023-11-04 16:54:03 -05:00
James Westman
d39257cabf formatter: Ensure the file ends with one newline 2023-11-04 15:57:31 -05:00
Gregor Niehl
703e2626dd Formatter: Close empty objects on same line 2023-11-04 13:49:56 +00:00
Sonny Piers
3c424d03a4 lsp: Fix bad argument for compile 2023-11-03 23:14:42 +01:00
JCWasmx86
ceb70271fd lsp: Fix classname 2023-11-03 15:26:08 +00:00
Gregor Niehl
4fa64cdf33 Add a formatter 2023-11-03 06:49:22 -05:00
James Westman
2faa9207de tokenizer: Allow escaped newlines
The docs said multi-line strings were possible by escaping the newline
character, but this was not actually implemented.

Fixes #132.
2023-10-26 18:50:07 -05:00
gregorni
9543b78138 Add justfile 2023-10-26 23:45:34 +00:00
James Westman
09bed9a9f5 tokenizer: Fix QUOTED regex
unescape_quote() assumed that a QUOTED token wouldn't end in the middle
of an escape sequence, but that assumption could fail (a bug found by
the fuzzer).
2023-09-28 18:21:07 -05:00
James Westman
7c072c0a32 tests: Use assertEqual instead of custom diff code 2023-09-28 17:59:23 -05:00
James Westman
3d5a5521aa decompiler: Use single quotes 2023-09-28 17:18:45 -05:00
James Westman
ea92838cf3 Parse escape sequences instead of using replace
That way we can warn about invalid sequences. Also, the previous code had at least one subtle bug (`\\\\'`).
2023-09-28 17:18:45 -05:00
James Westman
bc798c544c docs: Fix grammar for bindings
Binding flags were missing from the documented grammar. Also added prose
documentation about the available flags.
2023-09-27 10:51:48 -05:00
Urtsi Santsi
1371dec494 Use the updated test repo 2023-09-17 02:25:39 +03:00
Urtsi Santsi
cc66b05a87 Add generated notice to test files 2023-09-17 02:25:39 +03:00
Urtsi Santsi
cf136ab09f Add notice that the file is generated
Fixes #123
2023-09-17 02:25:39 +03:00
Marco Capypara Köpcke
80cb57cb88 batch-compile: Fix mixing relative+absolute paths 2023-09-16 16:41:43 +00:00
James Westman
057c767fbb typelib: Fix byte order issue 2023-09-14 10:19:49 -05:00
Jerry James
0c02195510
Handle big endian bitfields correctly 2023-09-13 08:43:54 -06:00
James Westman
3cd5daf025
Fix a crash found by the fuzzer 2023-09-07 12:13:05 -05:00
James Westman
0f5be1b051 docs: Use correct lexer name for code blocks 2023-08-31 14:58:29 -05:00
Sonny Piers
a8512d83f3 doc: Cleanup the Flatpak module 2023-08-30 13:49:18 +02:00
z00000000z
bcac788456 completions: property_completer improvements 2023-08-23 16:21:37 +00:00
Ivan Kalinin
582502c1b4 completions: fix property value completion 2023-08-13 10:42:03 +03:00
James Westman
bfa2f56e1f Sort imports 2023-07-25 20:07:37 -05:00
James Westman
35ee058192 lsp: Add code action to add missing imports 2023-07-25 20:02:03 -05:00
James Westman
3bcc9f4cbd Use the new Range class in more places 2023-07-25 20:01:41 -05:00
James Westman
56274d7c1f completions: Fix signal completion 2023-07-25 18:54:58 -05:00
James Westman
a9cb423b3b lsp: Add missing semantic highlight 2023-07-25 18:52:43 -05:00
James Westman
62f74178f7 lsp: Implement "go to definition" 2023-07-25 18:40:05 -05:00
James Westman
e087aeb44f lsp: Add document outline 2023-07-25 17:59:52 -05:00
James Westman
950b141d26 lsp: Mark deprecation warnings
Some editors use different styling (e.g. strikethrough) for deprecation
warnings.
2023-07-23 18:17:48 -05:00
James Westman
94db929f74 Emit deprecation warnings 2023-07-23 18:09:29 -05:00
James Westman
8fab7c1706 A couple of fixes to NEWS 2023-07-21 15:11:24 -05:00
James Westman
ee614e0cc0 Post-release version bump 2023-07-21 15:08:16 -05:00
James Westman
2a39a16391 Release v0.10.0 2023-07-21 15:06:18 -05:00
James Westman
883a136103 Fix parsing decimals
A number literal is a float if it contains ".", not if it is divisible
by 1. For example, 1.0 should be considered a float literal.
2023-07-20 19:25:25 -05:00
James Westman
c69a12096c docs: Update bindings docs 2023-07-20 18:54:14 -05:00
James Westman
0a4b5d07a1 Remove PropertyBinding rule, just use Binding 2023-07-20 18:46:45 -05:00
James Westman
abc4e5de65 lsp: Add docs for Adw.Breakpoint 2023-07-16 16:52:51 -05:00
James Westman
cb1eb9ba44 lsp: Show better info on IdentLiteral hover
Instead of showing the documentation for the expected type, show the
signature of the referenced object.
2023-07-16 16:52:51 -05:00
James Westman
9ff76b65cc docs: Fix docs for accessibility properties 2023-07-16 16:52:51 -05:00
James Westman
c4fc4f3de8 docs: Fix bug with colliding names
Often a vfunc has the same name as a signal, and the wrong docs would be
shown.
2023-07-16 16:52:51 -05:00
James Westman
e1b7410e51 docs: Add link to online documentation 2023-07-16 16:52:51 -05:00
James Westman
4eaf735732 gir: Fix signatures for properties and signals
Add arguments to signal signatures and fix property signatures
2023-07-16 16:52:51 -05:00
James Westman
3d79f9560c
ci: Fix Dockerfile 2023-07-15 17:34:04 -05:00
gregorni
3730e2e726 Add isort to CI and run on files 2023-07-09 14:26:37 +00:00
AkshayWarrier
f526cfa4d9 lsp: Decompile empty XML docs to empty strings 2023-06-14 00:29:04 +00:00
James Westman
4e02c34a5b
Minor performance optimizations 2023-06-13 19:01:33 -05:00
seshotake
9c567fe039 lsp: Make SemanticTokenServerCapabilities match the LSP spec
SemanticTokenSeverCapabilities doesn't deserealize because legend requires a tokenModifiers array, which not provided.
See: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokensLegend
2023-06-05 05:15:51 +03:00
James Westman
93392e5e02
docs: Fix Extension grammar
It was missing ExtAdwBreakpoint
2023-05-23 20:27:31 -05:00
James Westman
24bfe2d225 Mention syntax highlighters in MAINTENANCE.md 2023-05-22 20:01:15 -05:00
James Westman
aa7679618e
Release v0.8.1 2023-05-17 10:47:52 -05:00
James Westman
6ac798ea6f
More errors for duplicates 2023-05-17 10:41:45 -05:00
James Westman
2ca71de061
Fix template IDs in breakpoint setters 2023-05-17 10:01:23 -05:00
James Westman
64da41b268
ExtAdwMessageDialog: Duplicate flag errors 2023-05-17 09:58:51 -05:00
James Westman
c95195197d Fix template IDs in a couple more places 2023-05-16 19:59:25 -05:00
James Westman
3ebe5c72c1 Fix templates in bind-property 2023-05-16 17:42:53 -05:00
James Westman
b5eca8b0b3 tests: Add another template test 2023-05-16 17:37:52 -05:00
James Westman
9e02051e12 docs: Fix ExtListItemFactory example & description 2023-05-16 17:37:46 -05:00
James Westman
e4bad039b0 Fix simple bindings with template soure 2023-05-16 17:14:34 -05:00
James Westman
8f3682135b Release v0.8.0 2023-05-13 21:22:47 -05:00
James Westman
46e467bbfb Fix 'template' keyword in list item factories 2023-05-13 20:24:31 -05:00
James Westman
5a782c653b Add Gtk.Scale mark syntax 2023-05-13 20:19:29 -05:00
James Westman
83d11ccb8c tests: Auto-discover test files 2023-05-13 20:16:58 -05:00
James Westman
9346ee039e ci: Fuzzer is no longer allowed to fail 2023-05-13 19:57:45 -05:00
James Westman
60f9173421 Add type to BuilderListItemFactory extension
Makes it a little clearer how it works.
2023-05-13 16:49:48 -05:00
James Westman
7008924afe docs: Document the new template type syntax 2023-05-13 16:49:48 -05:00
James Westman
04509e4b2e Change template syntax
Templates now use a TypeName instead of an identifier, which makes it
clearer that it's an extern symbol (or that it's a Gtk.ListItem).
2023-05-13 16:49:48 -05:00
James Westman
aebf8be278 Fix a bug found by the fuzzer 2023-05-13 14:56:38 -05:00
James Westman
26072500c8 Fix Gio.File properties 2023-05-11 13:18:24 -05:00
James Westman
43fbf8cf8e Add warning for confusing object IDs 2023-05-08 15:23:46 +00:00
James Westman
77dc9350e9 docs: Fix some warnings 2023-05-08 15:20:20 +00:00
James Westman
8fcd08c835 Add Adw.Breakpoint custom syntax 2023-05-08 10:11:40 -05:00
James Westman
aafebf0dfb ci: Use libadwaita from git 2023-05-06 21:38:16 -05:00
Sonny Piers
fc0358ef01 cli: Ignore hidden folders in interactive port
Resolves #112 and resolves #57
2023-05-07 03:01:25 +02:00
James Westman
d4c2bb34eb Remove trailing commas in Translated 2023-05-06 15:30:18 -05:00
James Westman
b08a0c0665 Remove .vscode 2023-05-06 15:30:18 -05:00
James Westman
5b50090b65 Minor code cleanup 2023-05-06 15:30:18 -05:00
James Westman
4d62df0068 docs: Remove the examples page
It has been replaced with the new syntax reference, and it was out of
date anyway.
2023-05-06 15:30:18 -05:00
James Westman
a9f6bf8d89 Remove unused imports and code 2023-05-06 15:30:18 -05:00
James Westman
10806bce1e language: Rename extension classes
Rename extension classes to match the syntax reference.
2023-05-06 15:30:18 -05:00
James Westman
9e82a2fb2a language: Rename expression classes
Rename the expression classes to match the documentation.
2023-05-06 15:30:18 -05:00
James Westman
ef39b5d7db docs: Add syntax specification
Formally document the language syntax and provide examples and detailed
descriptions of how things work.
2023-05-06 15:30:18 -05:00
James Westman
3c1941a17e Simplify Translated
Remove the TranslatedWithContext and TranslatedWithoutContext rules and
just use Translated.
2023-04-29 21:57:33 -05:00
James Westman
779e27d7ac menus: Simplify grammar a bit
Again no syntax changes, just refactoring the rules.
2023-04-29 21:52:20 -05:00
James Westman
71f52d350a Refactor child types
Didn't change the actual syntax, but changed the rules around to be less
confusing.
2023-04-29 21:52:20 -05:00
James Westman
9dcd06de51 Make builder template factories use a subscope 2023-04-28 20:49:22 -05:00
James Westman
a2eaaa26fe Rename property to avoid conflict
TranslatedWithContext.context conflicted with AstNode.context
2023-04-28 20:49:22 -05:00
James Westman
ec844b10ca Add ScopeCtx instead of root.objects_by_id
This allows us to introduce new scopes, such as in
GtkBuilderListItemFactory templates.
2023-04-28 20:49:22 -05:00
James Westman
ff5fff7f4b Fix crash 2023-04-13 17:43:44 -05:00
James Westman
dd3c75d2c7 Update menu syntax
Sections and submenus can have IDs. Also, change the code to better
reflect the documented grammar.
2023-04-12 21:44:07 -05:00
James Westman
75055ac967 Move bindings out of the Value syntax
They're only valid in properties, so they should just be there. Same
with object values.
2023-04-12 21:44:07 -05:00
James Westman
ac2a7d9282 Add StringValue
Makes the grammar more specific in a few places that take only a string
literal or translated string.
2023-04-12 21:44:07 -05:00
James Westman
5bfed72674 Update regression tests 2023-04-12 21:24:18 -05:00
James Westman
02796fd830 Use <> instead of () for casts & typeof
This makes it clearer that they aren't functions, and it eliminates
syntactic ambiguity with closure expressions.
2023-04-10 09:39:34 -05:00
James Westman
d6bd282e58 errors: Report version in compiler bug message 2023-04-09 16:51:14 -05:00
James Westman
88f5b4f1c7
Fix template types 2023-04-08 20:10:16 -05:00
James Westman
64879491a1 Fix mypy error 2023-04-07 20:35:14 -05:00
Cameron Dehning
a2fb86bc31 Builder list factory 2023-04-08 01:34:47 +00:00
James Westman
0cf9a8e4fc
Add Adw.MessageDialog responses extension 2023-03-28 12:43:53 -05:00
James Westman
749ee03e86 Fix misleading error message for missing semicolon
Fixes #105.
2023-03-28 10:10:37 -05:00
Cameron Dehning
7e20983b44 Lsp hotfix 2023-03-24 16:27:22 +00:00
James Westman
bc605c5df8
Reduce errors when a namespace is not found
When the typelib for a namespace is not found, don't emit "namespace not
imported" errors. Just emit the one error on the import statement.
2023-03-21 11:31:02 -05:00
James Westman
402677f687
performance: Cache some properties 2023-03-20 13:34:17 -05:00
James Westman
3f27e92eb0
Remove unnecessary list() call 2023-03-20 13:27:21 -05:00
Sonny Piers
6f4806bfb3 lsp: Add compile an decompile commands 2023-03-19 22:14:42 +00:00
James Westman
8c3c43a34a
Add --typelib-path command line argument
Allows adding directories to search for typelib files.
2023-03-16 18:21:56 -05:00
James Westman
90001bd885 Fix mypy errors & other bugs 2023-03-12 21:49:36 -05:00
James Westman
98ba7d467a Improve expression type checking 2023-03-12 16:12:16 -05:00
James Westman
b636d9ed71 Fix bugs in number literals 2023-03-12 14:58:35 -05:00
James Westman
fad3b35531 types: Remove g* type names
They aren't used in GIR parsing anymore since we use typelibs, and
blueprint files should use the non-prefixed names.
2023-03-11 21:37:26 -06:00
James Westman
0f5f08ade9 Fix flag syntax
Unlike commas, no trailing "|" is allowed.
2023-03-11 21:24:52 -06:00
James Westman
8874cf60b3 parse_tree: Remove Pratt parser
It isn't actually needed; the way we parse expressions as a prefix
followed by zero or more suffixes is enough.
2023-03-11 21:05:27 -06:00
James Westman
9fcb63a013
typelib: Fix crash when handling array types 2023-02-16 20:43:17 -06:00
James Westman
1df46b5a06
Change the way values work
Change the parsing for values to make them more reusable, in particular
for when I implement extensions.
2023-01-12 15:49:19 -06:00
James Westman
6938267952
Add properties to AST types
I want to have a cleaner API that relies less on the specifics of the
grammar and parser.
2023-01-12 15:49:19 -06:00
James Westman
0b7dbaf90d
Add some type hints 2023-01-12 15:49:19 -06:00
James Westman
b6ee649458
Simplify error & warning handling 2023-01-12 15:49:19 -06:00
James Westman
122b049ce9
language: Use new extern syntax in signal handlers 2023-01-12 15:49:19 -06:00
James Westman
0b402db4d5
language: Change extern type syntax
Use a '$' instead of a '.' to indicate a type provided in application
code.

The reason for the change is to have a consistent "extern" symbol that
isn't widely used elsewhere and isn't ambiguous in expressions.
2023-01-12 15:49:19 -06:00
James Westman
be284de879
parse_tree: Fix Warning node 2023-01-12 15:49:18 -06:00
James Westman
7ef314ff94
Fix diagnostic location reporting
Text positions at the beginning of a line were being shown on the
previous line.
2023-01-12 15:49:15 -06:00
Sonny Piers
40f493b378 cli: Print compile errors to stderr 2023-01-05 12:30:26 +01:00
James Westman
59aa054c4c
language: Add closure expressions 2022-12-25 14:04:41 -06:00
James Westman
5cf9b63547
language: Add cast expressions 2022-12-25 14:04:40 -06:00
James Westman
2033bd9e16
types: Add UncheckedType
This allows us to remember information about an external type, such as
its name, while still marking it as unchecked.
2022-12-25 14:04:36 -06:00
Sonny Piers
f7aa7d0be2 lsp: Support change events with no range
range is optional

https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent
2022-12-25 18:09:57 +01:00
James Westman
039d88ab45
Fix CI 2022-12-19 15:41:29 -06:00
James Westman
6c67e1fc5a
xml: Fix flags and enums
GtkBuilder XML uses enum nicknames, full names, or integer values, but
we accept GIR names, so passing those through doesn't work if the name
has an underscore (which traditionally turns into a dash in the
nickname). Avoid the problem by always writing the integer value of the
enum member.
2022-12-19 15:15:25 -06:00
James Westman
51d8969ced
Fix menus
- Menus require an ID
- The top level menu block can't have attributes
2022-12-19 15:15:25 -06:00
James Westman
8a1dba662a
ci: Run tests with G_DEBUG=fatal-warnings 2022-12-19 15:15:24 -06:00
James Westman
8758bac40a
tests: Test XML outputs
Load the outputs of the tests in Gtk.Builder and make sure they work.
Some of them don't and need to be fixed. Others will require a bit more
work to set up callbacks, templates, etc.
2022-12-19 13:53:52 -06:00
James Westman
219891584c
ci: Fix Dockerfile 2022-12-19 12:22:37 -06:00
James Westman
83a7503e3a
ci: Check formatting 2022-12-19 12:06:48 -06:00
James Westman
8fee46ec68
Format using black 2022-12-19 11:52:59 -06:00
James Westman
6a36d92380
ci: Update regression tests 2022-11-29 11:17:36 -06:00
James Westman
00a31d87bb
Post-release version bump 2022-11-26 17:20:04 -06:00
James Westman
9adcab2d22
Release v0.6.0 2022-11-26 17:14:49 -06:00
James Westman
97f0feaf7e
Update MAINTENANCE.md 2022-11-26 17:14:12 -06:00
James Westman
b915c227f8
Fix type declaration 2022-11-26 17:10:53 -06:00
James Westman
d8f1b41ef0
docs: Document the new typeof() operator 2022-11-26 16:44:43 -06:00
James Westman
46a06bb7b6
docs: Fix version in flatpak docs 2022-11-26 16:25:22 -06:00
James Westman
86b07ef0ae
Fix new mypy errors 2022-11-26 16:22:34 -06:00
James Westman
bc15ac9efb
docs: Add warning that blueprint is experimental 2022-11-02 10:44:20 -05:00
Sonny Piers
8efc290e47 doc: Mention history and Workbench 2022-11-02 15:43:41 +00:00
James Westman
0b8012b8e1
Support Python 3.9
Remove the '|' syntax for type unions, since it's a 3.10 feature, and
set mypy to check with version 3.9.
2022-10-27 13:16:18 -05:00
FeRD (Frank Dana)
d6c6a66c15 port: Fix directory recursion 2022-10-18 20:49:16 +00:00
James Westman
a8a209550b
typelib: Fix big-endian architectures 2022-10-18 15:04:55 -05:00
James Westman
a24f16109f
Separate output into its own module 2022-10-15 19:40:47 -05:00
James Westman
8cf793023d
Fix crash in language server 2022-10-15 11:47:42 -05:00
James Westman
b3783b9c6a
lsp: Log to stderr rather than a file 2022-10-15 11:26:18 -05:00
James Westman
447785ec8c language: Remove inline menus
Inline menus didn't work anyway--menus have to be referenced by ID
(though, curiously, you *can* put the <menu> within the <property> tag
and immediately reference it--but that's a hack, and not what
blueprint-compiler was doing).
2022-10-14 22:12:56 -05:00
William Roy
f1c3413dc1 Escape backlash on Windows 2022-10-07 18:02:39 +00:00
James Westman
c0c40b1577
language: Support boxed types and GType
- Add support for type checking boxed types
- Remove support for converting string and number literals
- Add the `typeof()` operator for GType literals
2022-10-06 13:03:33 -05:00
James Westman
ee2b9b2950 Update MAINTENANCE.md 2022-10-06 04:13:38 +00:00
James Westman
15496353de
ci: Update regression tests 2022-10-04 16:12:31 -05:00
Sonny Piers
302903b89c doc: Add documentation to contribute 2022-10-04 03:45:59 +02:00
Sonny Piers
82980a466b doc: Add documentation for Gtk.Label attributes 2022-10-04 03:45:54 +02:00
James Westman
12c1c7b8d6
docs: Fix build setting
The docs need to be set to build_always_stale so you don't need to
delete the directory to rebuild them. This also means we want to disable
build_by_default.
2022-10-03 20:27:07 -05:00
Sonny Piers
c998655af6 doc: Add more apps built with Blueprint 2022-10-04 01:26:38 +00:00
James Westman
6ad1433587
Post-release version bump 2022-09-04 16:54:21 -05:00
James Westman
75a6d95988
Release v0.4.0 2022-09-04 14:04:03 -05:00
James Westman
ad71324ccb
Add MAINTENANCE.md 2022-09-04 14:02:05 -05:00
Alan Beveridge
59283a76ad Update docs/index.rst 2022-08-09 15:41:09 +00:00
Sonny Piers
50db59f2d2 lsp: Report error hints 2022-07-25 00:52:05 +02:00
James Westman
30f0deea34
Exit with error code when a bug is reported 2022-07-23 15:06:38 -05:00
James Westman
08da6f79c7
Fix referencing template by ID 2022-07-16 21:16:45 -05:00
James Westman
664fa2250b
Validate that an object can have children
Fixes #32.
2022-07-09 16:40:02 -05:00
James Westman
0a0389b1f8
grammar: Create an AST node for type names 2022-07-09 16:05:10 -05:00
Sonny Piers
012fc61926 Update documentation 2022-07-09 20:07:57 +00:00
Sonny Piers
2da6be7618 lsp: Fix crash when import version missing
The issue is specific to the language server, since it's trying to use
an AST that contains errors. The test would not fail but was added
anyway.
2022-07-09 20:00:10 +00:00
James Westman
b9fdc5a5f1
Fix action widgets in templates
Fixes #69.
2022-07-09 14:48:33 -05:00
Alexander Bisono
f6eacaa3d9 Add emacs major mode to 'Editor Plugins' 2022-07-08 15:45:12 +00:00
James Westman
9542f72ce2
Ci: Update coverage configuration 2022-07-08 10:30:03 -05:00
James Westman
12c9a434f1
ci: Install pygobject in CI image 2022-06-29 02:24:06 -05:00
James Westman
68610a7dba
typelib: Use GIRepository to find typelib path 2022-06-28 23:58:48 -05:00
James Westman
cfae48a65a
docs: Add notes for distro packagers 2022-06-25 00:15:20 -05:00
James Westman
c5f2e4ed4b
gir: Gracefully handle missing .gir files
They aren't required for compilation anymore, and not being able to show
documentation on hover is probably not worth crashing the language
server over.
2022-06-25 00:15:20 -05:00
James Westman
06f54c8ff8
Use typelib instead of XML
For normal compilation, use .typelib files rather than .gir XML files.
This is much faster.

Rather than using libgirepository, which would try to actually load the
libraries, we use a custom parser.

The language server will still read XML because it needs to access
documentation, which is not in the typelib, but that's generally fine
because it's a long lived process and only has to do that once.
2022-06-25 00:15:20 -05:00
James Westman
e78fae4f12
build: Set the module path in the build
Instead of trying to find the module by traversing from the executable,
have meson hardcode the path. I *think* this is a little less fragile.
2022-06-25 00:15:20 -05:00
James Westman
75475d1a45
tokenizer: Fix number parsing (again) 2022-06-25 00:08:24 -05:00
James Westman
4fefa0bd73
Add lookup expressions 2022-06-24 23:16:15 -05:00
James Westman
c094743e84
Fix compiling empty file 2022-06-17 11:12:21 -05:00
James Westman
7eb0c1ae0d
decompiler: Fix Adwaita version 2022-06-11 02:15:56 -05:00
James Westman
fac311d3c3
Update regression tests 2022-06-09 15:06:38 -05:00
James Westman
180b529650
Post-release version bump 2022-06-09 14:51:35 -05:00
362 changed files with 11857 additions and 2630 deletions

2
.gitignore vendored
View file

@ -12,3 +12,5 @@ coverage.xml
/corpus
/crashes
.vscode

View file

@ -3,21 +3,23 @@ stages:
- pages
build:
image: registry.gitlab.gnome.org/jwestman/blueprint-compiler
image: registry.gitlab.gnome.org/gnome/blueprint-compiler
stage: build
script:
- mypy blueprintcompiler
- coverage run -m unittest
- black --check --diff ./ tests
- isort --check --diff --profile black ./ tests
- mypy --python-version=3.9 blueprintcompiler/
- G_DEBUG=fatal-warnings xvfb-run coverage run -m unittest
- coverage report
- coverage html
- coverage xml
- meson _build -Ddocs=true
- meson _build -Ddocs=true --prefix=/usr
- ninja -C _build
- ninja -C _build test
- ninja -C _build install
- ninja -C _build docs/en
- git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git
- cd blueprint-regression-tests
- git checkout dba20ee77b0ba711893726208a0523073fc697e3
- git checkout 5f9e155c1333e84e6f683cdb26b02a5925fd8db3
- ./test.sh
- cd ..
coverage: '/TOTAL.*\s([.\d]+)%/'
@ -26,12 +28,13 @@ build:
- _build
- htmlcov
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
fuzz:
image: registry.gitlab.gnome.org/jwestman/blueprint-compiler
image: registry.gitlab.gnome.org/gnome/blueprint-compiler
stage: build
allow_failure: true
script:
- meson _build
- ninja -C _build install

28
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,28 @@
First of all, thank you for contributing to Blueprint.
If you learn something useful, please add it to this file.
# Run the test suite
```sh
python -m unittest
```
# Formatting
Blueprint uses [Black](https://github.com/psf/black) for code formatting.
# Build the docs
```sh
pip install -U sphinx furo
meson -Ddocs=true build
# or
meson --reconfigure -Ddocs=true build
ninja -C build docs/en
python -m http.server 2310 --bind 127.0.0.1 --directory build/docs/en/
xdg-open http://127.0.0.1:2310/
```

18
MAINTENANCE.md Normal file
View file

@ -0,0 +1,18 @@
## Releasing a new version
1. Look at the git log since the previous release. Note every significant change
in the NEWS file.
2. Update the version number, according to semver:
- At the top of meson.build
- In docs/flatpak.rst
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 (Mastodon, TWIG, etc.)
## Related projects
Blueprint is supported by the following syntax highlighters. If changes are made to the syntax, remember to update these projects as well.
- Pygments (https://github.com/pygments/pygments/blob/master/pygments/lexers/blueprint.py)
- GtkSourceView (https://gitlab.gnome.org/GNOME/gtksourceview/-/blob/master/data/language-specs/blueprint.lang)

244
NEWS.md Normal file
View file

@ -0,0 +1,244 @@
# 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
- Add support for Adw.AlertDialog (Sonny Piers)
- Emit warnings for deprecated APIs - lsp and compiler
- lsp: Document symbols
- lsp: "Go to definition" (ctrl+click)
- lsp: Code action for "namespace not imported" diagnostics, that adds the missing import
- Add a formatter - cli and lsp (Gregor Niehl)
- Support for translation domain - see documentation
- cli: Print code actions in error messages
## Changed
- compiler: Add a header notice mentionning the file is generated (Urtsi Santsi)
- decompiler: Use single quotes for output
## Fixed
- Fixed multine strings support with the escape newline character
- lsp: Fixed the signal completion, which was missing the "$"
- lsp: Fixed property value completion (Ivan Kalinin)
- lsp: Added a missing semantic highlight (for the enum in Gtk.Scale marks)
- Handle big endian bitfields correctly (Jerry James)
- batch-compile: Fix mixing relative and absolute paths (Marco Köpcke )
## Documentation
- Fix grammar for bindings
- Add section on referencing templates
# v0.10.0
## Added
- The hover documentation now includes a link to the online documentation for the symbol, if available.
- Added hover documentation for the Adw.Breakpoint extensions, `condition` and `setters`.
## Changed
- Decompiling an empty file now produces an empty file rather than an error. (AkshayWarrier)
- More relevant documentation is shown when hovering over an identifier literal (such as an enum value or an object ID).
## Fixed
- Fixed an issue with the language server not conforming the spec. (seshotake)
- Fixed the signature section of the hover documentation for properties and signals.
- Fixed a bug where documentation was sometimes shown for a different symbol with the same name.
- Fixed a bug where documentation was not shown for accessibility properties that contain `-`.
- Number literals are now correctly parsed as floats if they contain a `.`, even if they are divisible by 1.
## Removed
- The `bind-property` keyword has been removed. Use `bind` instead. The old syntax is still accepted with a warning.
## Documentation
- Fixed the grammar for Extension, which was missing ExtAdwBreakpoint.
# v0.8.1
## Breaking Changes
- Duplicates in a number of places are now considered errors. For example, duplicate flags in several places, duplicate
strings in Gtk.FileFilters, etc.
## Fixed
- Fixed a number of bugs in the XML output when using `template` to refer to the template object.
## Documentation
- Fixed the example for ExtListItemFactory
# v0.8.0
## Breaking Changes
- A trailing `|` is no longer allowed in flags.
- The primitive type names `gboolean`, `gchararray`, `gint`, `gint64`, `guint`, `guint64`, `gfloat`, `gdouble`, `utf8`, and `gtype` are no longer permitted. Use the non-`g`-prefixed versions instead.
- Translated strings may no longer have trailing commas.
## Added
- Added cast expressions, which are sometimes needed to specify type information in expressions.
- Added support for closure expressions.
- Added the `--typelib-path` command line argument, which allows adding directories to the search path for typelib files.
- Added custom compile and decompile commands to the language server. (Sonny Piers)
- Added support for [Adw.MessageDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.MessageDialog.html#adwmessagedialog-as-gtkbuildable) custom syntax.
- Added support for inline sub-templates for [Gtk.BuilderListItemFactory](https://docs.gtk.org/gtk4/class.BuilderListItemFactory.html). (Cameron Dehning)
- Added support for [Adw.Breakpoint](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Breakpoint.html) custom syntax.
- Added a warning when an object ID might be confusing.
- Added support for [Gtk.Scale](https://docs.gtk.org/gtk4/class.Scale.html#gtkscale-as-gtkbuildable) custom syntax.
## Changed
Some of these changes affect syntax, but the old syntax is still accepted with a purple "upgrade" warning, so they are not breaking changes yet. In editors that support code actions, such as Visual Studio Code, the blueprint language server can automatically fix these warnings.
- The XML output uses the integer value rather than GIR name for enum values.
- Compiler errors are now printed to stderr rather than stdout. (Sonny Piers)
- Introduced `$` to indicate types or callbacks that are provided in application code.
- Types that are provided by application code are now begin with a `$` rather than a leading `.`.
- The handler name in a signal is now prefixed with `$`.
- Closure expressions, which were added in this version, are also prefixed with `$`.
- When a namespace is not found, errors are supressed when the namespace is used.
- The compiler bug message now reports the version of blueprint-compiler.
- The `typeof` syntax now uses `<>` instead of `()` to match cast expressions.
- Menu sections and subsections can now have an ID.
- The interactive porting tool now ignores hidden folders. (Sonny Piers)
- Templates now use the typename syntax rather than an ID to specify the template's class. In most cases, this just means adding a `$` prefix to the ID, but for GtkListItem templates it should be shortened to ListItem (since the Gtk namespace is implied). The template object is now referenced with the `template` keyword rather than with the ID.
## Fixed
- Fixed a bug in the language server's acceptance of text change commands. (Sonny Piers)
- Fixed a bug in the display of diagnostics when the diagnostic is at the beginning of a line.
- Fixed a crash that occurred when dealing with array types.
- Fixed a bug that prevented Gio.File properties from being settable.
## Documentation
- Added a reference section to the documentation. This replaces the Examples page with a detailed description of each syntax feature, including a formal specification of the grammar.
# v0.6.0
## Breaking Changes
- Quoted and numeric literals are no longer interchangeable (e.g. `"800"` is no longer an accepted value for an
integer type).
- Boxed types are now type checked.
## Added
- There is now syntax for `GType` literals: the `typeof()` pseudo-function. For example, list stores have an `item-type`
property which is now specifiable like this: `item-type: typeof(.MyDataModel)`. See the documentation for more details.
## Changed
- The language server now logs to stderr.
## Fixed
- Fix the build on Windows, where backslashes in paths were not escaped. (William Roy)
- Remove the syntax for specifying menu objects inline, since it does not work.
- Fix a crash in the language server that was triggered in files with incomplete `using Gtk 4.0;` statements.
- Fixed compilation on big-endian systems.
- Fix an issue in the interactive port tool that would lead to missed files. (Frank Dana)
## Documentation
- Fix an issue for documentation contributors where changing the documentation files would not trigger a rebuild.
- Document the missing support for Gtk.Label `<attributes>`, which is intentional, and recommend alternatives. (Sonny
Piers)
- Add a prominent warning that Blueprint is still experimental
# v0.4.0
## Added
- Lookup expressions
- With the language server, hovering over a diagnostic message now shows any
associated hints.
## Changed
- The compiler now uses .typelib files rather than XML .gir files, which reduces
dependencies and should reduce compile times by about half a second.
## Fixed
- Fix the decompiler/porting tool not importing the Adw namespace when needed
- Fix a crash when trying to compile an empty file
- Fix parsing of number tokens
- Fix a bug where action widgets did not work in templates
- Fix a crash in the language server that occurred when a `using` statement had
no version
- If a compiler bug is reported, the process now exits with a non-zero code

View file

@ -65,13 +65,31 @@ template ShumateDemoWindow : Gtk.ApplicationWindow {
}
```
## Editor plugins
## Editors
### Vim
[Workbench](https://github.com/sonnyp/Workbench) and [GNOME Builder](https://apps.gnome.org/app/org.gnome.Builder/) have builtin support for Blueprint.
Vim
- [Syntax highlighting by thetek42](https://github.com/thetek42/vim-blueprint-syntax)
- [Syntax highlighting by gabmus](https://gitlab.com/gabmus/vim-blueprint)
GNU Emacs
- [Major mode by DrBluefall](https://github.com/DrBluefall/blueprint-mode)
Visual Studio Code
- [Blueprint Language Plugin by bodil](https://github.com/bodil/vscode-blueprint)
## Donate
You can support my work on GitHub Sponsors! <https://github.com/sponsors/jameswestman>
## Getting in Touch
Matrix room: [#blueprint-language:matrix.org](https://matrix.to/#/#blueprint-language:matrix.org)
## License
Copyright (C) 2021 James Westman <james@jwestman.net>
@ -88,11 +106,3 @@ 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 <https://www.gnu.org/licenses/>.
## Donate
You can support my work on GitHub Sponsors! <https://github.com/sponsors/jameswestman>
## Getting in Touch
Matrix room: [#blueprint-language:matrix.org](https://matrix.to/#/#blueprint-language:matrix.org)

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

@ -19,20 +19,23 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import os, sys
import os
import sys
# Try to find the python module, assuming the current file is installed to (prefix)/bin
dirname = os.path.join(os.path.dirname(os.path.dirname(__file__)), "share", "blueprint-compiler")
if os.path.isdir(os.path.join(dirname, "blueprintcompiler")):
sys.path.insert(0, dirname)
# Get the configured (or, if running from source, not configured) version number
# These variables should be set by meson. If they aren't, we're running
# uninstalled, and we might have to guess some values.
version = "@VERSION@"
module_path = r"@MODULE_PATH@"
libdir = r"@LIBDIR@"
def literal(key):
return "@" + key + "@"
if version == "\u0040VERSION@":
version = "uninstalled"
libdir = None
else:
# If Meson set the configuration values, insert the module path it set
sys.path.insert(0, module_path)
from blueprintcompiler import main
if __name__ == "__main__":
main.main("uninstalled" if version == literal("VERSION") else version)
main.main(version, libdir)

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

@ -17,29 +17,80 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from collections import ChainMap, defaultdict
from functools import cached_property
import typing as T
from .errors import *
from .lsp_utils import SemanticToken
from .xml_emitter import XmlEmitter
from .lsp_utils import DocumentSymbol, LocationLink, SemanticToken
from .tokenizer import Range
TType = T.TypeVar("TType")
class Children:
"""Allows accessing children by type using array syntax."""
def __init__(self, children):
self._children = children
def __iter__(self):
def __iter__(self) -> T.Iterator["AstNode"]:
return iter(self._children)
@T.overload
def __getitem__(self, key: T.Type[TType]) -> T.List[TType]: ...
@T.overload
def __getitem__(self, key: int) -> "AstNode": ...
def __getitem__(self, key):
if isinstance(key, int):
if key >= len(self._children):
return None
else:
return self._children[key]
else:
return [child for child in self._children if isinstance(child, key)]
class Ranges:
def __init__(self, ranges: T.Dict[str, Range]):
self._ranges = ranges
def __getitem__(self, key: T.Union[str, tuple[str, str]]) -> T.Optional[Range]:
if isinstance(key, str):
return self._ranges.get(key)
elif isinstance(key, tuple):
start, end = key
return Range.join(self._ranges.get(start), self._ranges.get(end))
TCtx = T.TypeVar("TCtx")
TAttr = T.TypeVar("TAttr")
class Ctx:
"""Allows accessing values from higher in the syntax tree."""
def __init__(self, node: "AstNode") -> None:
self.node = node
def __getitem__(self, key: T.Type[TCtx]) -> T.Optional[TCtx]:
attrs = self.node._attrs_by_type(Context)
for name, attr in attrs:
if attr.type == key:
return getattr(self.node, name)
if self.node.parent is not None:
return self.node.parent.context[key]
else:
return None
class AstNode:
"""Base class for nodes in the abstract syntax tree."""
completers: T.List = []
attrs_by_type: T.Dict[T.Type, T.List] = {}
def __init__(self, group, children, tokens, incomplete=False):
self.group = group
@ -53,19 +104,33 @@ class AstNode:
def __init_subclass__(cls):
cls.completers = []
cls.validators = [getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")]
cls.validators = [
getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")
]
cls.attrs_by_type = {}
@cached_property
def context(self):
return Ctx(self)
@property
@cached_property
def ranges(self):
return Ranges(self.group.ranges)
@cached_property
def root(self):
if self.parent is None:
return self
else:
return self.parent.root
def parent_by_type(self, type):
@property
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:
if self.parent is None:
return None
raise CompilerBugError()
elif isinstance(self.parent, type):
return self.parent
else:
@ -73,7 +138,19 @@ class AstNode:
@cached_property
def errors(self):
return list(self._get_errors())
return list(
error
for error in self._get_errors()
if not isinstance(error, CompileWarning)
)
@cached_property
def warnings(self):
return list(
warning
for warning in self._get_errors()
if isinstance(warning, CompileWarning)
)
def _get_errors(self):
for validator in self.validators:
@ -83,25 +160,23 @@ 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()
def _attrs_by_type(self, attr_type):
def _attrs_by_type(self, attr_type: T.Type[TAttr]) -> T.List[T.Tuple[str, TAttr]]:
if attr_type not in self.attrs_by_type:
self.attrs_by_type[attr_type] = []
for name in dir(type(self)):
item = getattr(type(self), name)
if isinstance(item, attr_type):
yield name, item
def generate(self) -> str:
""" Generates an XML string from the node. """
xml = XmlEmitter()
self.emit_xml(xml)
return xml.result
def emit_xml(self, xml: XmlEmitter):
""" Emits the XML representation of this AST node to the XmlEmitter. """
raise NotImplementedError()
self.attrs_by_type[attr_type].append((name, item))
return self.attrs_by_type[attr_type]
def get_docs(self, idx: int) -> T.Optional[str]:
for name, attr in self._attrs_by_type(Docs):
@ -109,29 +184,46 @@ 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 child.group.start <= idx < child.group.end:
docs = child.get_docs(idx)
if docs is not None:
if idx in child.range:
if docs := child.get_docs(idx):
return docs
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]:
for child in self.children:
yield from child.get_semantic_tokens()
def iterate_children_recursive(self) -> T.Iterator["AstNode"]:
yield self
def get_reference(self, idx: int) -> T.Optional[LocationLink]:
for child in self.children:
yield from child.iterate_children_recursive()
if idx in child.range:
if ref := child.get_reference(idx):
return ref
return None
@property
def document_symbol(self) -> T.Optional[DocumentSymbol]:
return None
def validate_unique_in_parent(self, error, check=None):
def get_document_symbols(self) -> T.List[DocumentSymbol]:
result = []
for child in self.children:
if s := child.document_symbol:
s.children = child.get_document_symbols()
result.append(s)
else:
result.extend(child.get_document_symbols())
return result
def validate_unique_in_parent(
self, error: str, check: T.Optional[T.Callable[["AstNode"], bool]] = None
):
for child in self.parent.children:
if child is self:
break
@ -140,19 +232,38 @@ class AstNode:
if check is None or check(child):
raise CompileError(
error,
references=[ErrorReference(child.group.start, child.group.end, "previous declaration was here")]
references=[
ErrorReference(
child.range,
"previous declaration was here",
)
],
)
def validate(token_name=None, end_token_name=None, skip_incomplete=False):
def validate(
token_name: T.Optional[str] = None,
end_token_name: T.Optional[str] = None,
skip_incomplete: bool = False,
):
"""Decorator for functions that validate an AST node. Exceptions raised
during validation are marked with range information from the tokens."""
def decorator(func):
def inner(self):
def inner(self: AstNode):
if skip_incomplete and self.incomplete:
return
def fill_error(e: CompileError):
if e.range is None:
e.range = (
Range.join(
self.ranges[token_name],
self.ranges[end_token_name],
)
or self.range
)
try:
func(self)
except CompileError as e:
@ -161,25 +272,18 @@ def validate(token_name=None, end_token_name=None, skip_incomplete=False):
if self.incomplete:
return
# This mess of code sets the error's start and end positions
# from the tokens passed to the decorator, if they have not
# already been set
if e.start is None:
if token := self.group.tokens.get(token_name):
e.start = token.start
else:
e.start = self.group.start
if e.end is None:
if token := self.group.tokens.get(end_token_name):
e.end = token.end
elif token := self.group.tokens.get(token_name):
e.end = token.end
else:
e.end = self.group.end
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
@ -205,3 +309,28 @@ def docs(*args, **kwargs):
return Docs(func, *args, **kwargs)
return decorator
class Context:
def __init__(self, type: T.Type[TCtx], func: T.Callable[[AstNode], TCtx]) -> None:
self.type = type
self.func = func
def __get__(self, instance, owner):
if instance is None:
return self
if ctx := getattr(instance, "_context_" + self.type.__name__, None):
return ctx
else:
ctx = self.func(instance)
setattr(instance, "_context_" + self.type.__name__, ctx)
return ctx
def context(type: T.Type[TCtx]):
"""Decorator for functions that return a context object, which is passed down to ."""
def decorator(func: T.Callable[[AstNode], TCtx]) -> Context:
return Context(type, func)
return decorator

View file

@ -19,20 +19,25 @@
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
from .lsp_utils import Completion, CompletionItemKind
from .parser import SKIP_TOKENS
from .tokenizer import TokenType, Token
from .tokenizer import Token, TokenType
Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]]
def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int) -> T.Iterator[Completion]:
def _complete(
lsp, ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int
) -> T.Iterator[Completion]:
for child in ast_node.children:
if child.group.start <= idx and (idx < child.group.end or (idx == child.group.end and child.incomplete)):
yield from _complete(child, tokens, idx, token_idx)
if child.group.start <= idx and (
idx < child.group.end or (idx == child.group.end and child.incomplete)
):
yield from _complete(lsp, child, tokens, idx, token_idx)
return
prev_tokens: T.List[Token] = []
@ -45,10 +50,12 @@ def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int
token_idx -= 1
for completer in ast_node.completers:
yield from completer(prev_tokens, ast_node)
yield from completer(prev_tokens, ast_node, lsp)
def complete(ast_node: AstNode, tokens: T.List[Token], idx: int) -> T.Iterator[Completion]:
def complete(
lsp, ast_node: AstNode, tokens: T.List[Token], idx: int
) -> T.Iterator[Completion]:
token_idx = 0
# find the current token
for i, token in enumerate(tokens):
@ -60,23 +67,29 @@ def complete(ast_node: AstNode, tokens: T.List[Token], idx: int) -> T.Iterator[C
idx = tokens[token_idx].start
token_idx -= 1
yield from _complete(ast_node, tokens, idx, token_idx)
yield from _complete(lsp, ast_node, tokens, idx, token_idx)
@completer([language.GtkDirective])
def using_gtk(ast_node, match_variables):
yield Completion("using Gtk 4.0;", CompletionItemKind.Keyword)
def using_gtk(lsp, ast_node, match_variables):
yield Completion(
"using Gtk 4.0", CompletionItemKind.Keyword, snippet="using Gtk 4.0;\n"
)
@completer(
applies_in=[language.UI, language.ObjectContent, language.Template],
matches=new_statement_patterns
matches=new_statement_patterns,
)
def namespace(ast_node, match_variables):
def namespace(lsp, ast_node, match_variables):
yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.")
for ns in ast_node.root.children[language.Import]:
if ns.gir_namespace is not None:
yield Completion(ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".")
yield Completion(
ns.gir_namespace.name,
CompletionItemKind.Module,
text=ns.gir_namespace.name + ".",
)
@completer(
@ -84,48 +97,122 @@ def namespace(ast_node, match_variables):
matches=[
[(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)],
[(TokenType.IDENT, None), (TokenType.OP, ".")],
]
],
)
def object_completer(ast_node, match_variables):
def object_completer(lsp, ast_node, match_variables):
ns = ast_node.root.gir.namespaces.get(match_variables[0])
if ns is not None:
for c in ns.classes.values():
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(
applies_in=[language.UI, language.ObjectContent, language.Template],
matches=new_statement_patterns,
)
def gtk_object_completer(ast_node, match_variables):
def gtk_object_completer(lsp, ast_node, match_variables):
ns = ast_node.root.gir.namespaces.get("Gtk")
if ns is not None:
for c in ns.classes.values():
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(
applies_in=[language.ObjectContent],
matches=new_statement_patterns,
)
def property_completer(ast_node, match_variables):
if ast_node.gir_class:
for prop in ast_node.gir_class.properties:
yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;")
def property_completer(lsp, ast_node, match_variables):
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)
and lsp.client_supports_completion_choice
):
yield Completion(
prop_name,
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=f"{prop_name}: ${{1|true,false|}};",
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=snippet,
docs=prop.doc,
detail=prop.detail,
)
elif (
isinstance(prop.type, gir.Enumeration)
and len(prop.type.members) <= 10
and lsp.client_supports_completion_choice
):
choices = ",".join(prop.type.members.keys())
yield Completion(
prop_name,
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=f"{prop_name}: ${{1|{choices}|}};",
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(
prop_name,
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.BaseTypedAttribute],
matches=[
[(TokenType.IDENT, None), (TokenType.OP, ":")]
],
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,
detail=member.detail,
)
def prop_value_completer(ast_node, match_variables):
if isinstance(ast_node.value_type, gir.Enumeration):
for name, member in ast_node.value_type.members.items():
yield Completion(name, CompletionItemKind.EnumMember, docs=member.doc)
elif isinstance(ast_node.value_type, gir.BoolType):
elif isinstance(vt.value_type, gir.BoolType):
yield Completion("true", CompletionItemKind.Constant)
yield Completion("false", CompletionItemKind.Constant)
@ -134,22 +221,32 @@ def prop_value_completer(ast_node, match_variables):
applies_in=[language.ObjectContent],
matches=new_statement_patterns,
)
def signal_completer(ast_node, match_variables):
if ast_node.gir_class:
for signal in ast_node.gir_class.signals:
def signal_completer(lsp, ast_node, match_variables):
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:
name = "on_" + (ast_node.parent.tokens["id"] or ast_node.parent.tokens["class_name"].lower())
yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;")
@completer(
applies_in=[language.UI],
matches=new_statement_patterns
name = "on_" + (
ast_node.parent.children[ClassName][0].tokens["id"]
or ast_node.parent.children[ClassName][0]
.tokens["class_name"]
.lower()
)
def template_completer(ast_node, match_variables):
yield Completion(
"template", CompletionItemKind.Snippet,
snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}"
signal_name,
CompletionItemKind.Event,
sort_text=f"1 {signal_name}",
snippet=f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;",
docs=signal.doc,
detail=signal.detail,
)
@completer(applies_in=[language.UI], matches=new_statement_patterns)
def template_completer(lsp, ast_node, match_variables):
yield Completion(
"template",
CompletionItemKind.Snippet,
snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}",
)

View file

@ -20,33 +20,27 @@
import typing as T
from .tokenizer import Token, TokenType
from .lsp_utils import Completion
from .tokenizer import Token, TokenType
new_statement_patterns = [
[(TokenType.PUNCTUATION, "{")],
[(TokenType.PUNCTUATION, "}")],
[(TokenType.PUNCTUATION, "]")],
[(TokenType.PUNCTUATION, ";")],
]
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):
def inner(prev_tokens: T.List[Token], ast_node, lsp):
# For completers that apply in ObjectContent nodes, we can further
# check that the object is the right class
if applies_in_subclass is not None:
type = ast_node.root.gir.get_type(applies_in_subclass[1], applies_in_subclass[0])
if ast_node.gir_class and not ast_node.gir_class.assignable_to(type):
type = ast_node.root.gir.get_type(
applies_in_subclass[1], applies_in_subclass[0]
)
if not ast_node.gir_class or not ast_node.gir_class.assignable_to(type):
return
any_match = len(matches) == 0
@ -59,7 +53,9 @@ def completer(applies_in: T.List, matches: T.List=[], applies_in_subclass=None):
for i in range(0, len(pattern)):
type, value = pattern[i]
token = prev_tokens[i - len(pattern)]
if token.type != type or (value is not None and str(token) != value):
if token.type != type or (
value is not None and str(token) != value
):
break
if value is None:
match_variables.append(str(token))
@ -70,7 +66,7 @@ def completer(applies_in: T.List, matches: T.List=[], applies_in_subclass=None):
if not any_match:
return
yield from func(ast_node, match_variables)
yield from func(lsp, ast_node, match_variables)
for c in applies_in:
c.completers.append(inner)

View file

@ -17,20 +17,20 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import re
from enum import Enum
import typing as T
from collections import defaultdict
from dataclasses import dataclass
from enum import Enum
from .xml_reader import Element, parse
from . import formatter
from .gir import *
from .utils import Colors
from .utils import Colors, escape_quote
from .xml_reader import Element, parse, parse_string
__all__ = ["decompile"]
_DECOMPILERS: T.Dict = {}
_DECOMPILERS: dict[str, list] = defaultdict(list)
_CLOSING = {
"{": "}",
"[": "]",
@ -39,7 +39,7 @@ _NAMESPACES = [
("GLib", "2.0"),
("GObject", "2.0"),
("Gio", "2.0"),
("Adw", "1.0"),
("Adw", "1"),
]
@ -51,26 +51,34 @@ class LineType(Enum):
class DecompileCtx:
def __init__(self):
self._result = ""
self.gir = GirContext()
self._indent = 0
self._blocks_need_end = []
self._last_line_type = LineType.NONE
def __init__(self, parent_gir: T.Optional[GirContext] = None) -> None:
self.sub_decompiler = parent_gir is not None
self._result: str = ""
self.gir = parent_gir or GirContext()
self._blocks_need_end: T.List[str] = []
self._last_line_type: LineType = LineType.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):
imports = "\n".join([
def result(self) -> str:
imports = ""
if not self.sub_decompiler:
import_lines = sorted(
[
f"using {ns} {namespace.version};"
for ns, namespace in self.gir.namespaces.items()
])
return imports + "\n" + self._result
if ns != "Gtk"
]
)
imports += "\n".join(["using Gtk 4.0;", *import_lines])
return formatter.format(imports + self._result)
def type_by_cname(self, cname):
def type_by_cname(self, cname: str) -> T.Optional[GirType]:
if type := self.gir.get_type_by_cname(cname):
return type
@ -83,115 +91,204 @@ class DecompileCtx:
except:
pass
return None
def start_block(self):
self._blocks_need_end.append(None)
def start_block(self) -> None:
self._blocks_need_end.append("")
self._obj_type_stack.append(None)
def end_block(self):
def end_block(self) -> None:
if close := self._blocks_need_end.pop():
self.print(close)
self._obj_type_stack.pop()
def end_block_with(self, text):
@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
def print(self, line, newline=True):
if line == "}" or line == "]":
self._indent -= 1
# Add blank lines between different types of lines, for neatness
if newline:
if line == "}" or line == "]":
line_type = LineType.BLOCK_END
elif line.endswith("{") or line.endswith("]"):
line_type = LineType.BLOCK_START
elif line.endswith(";"):
line_type = LineType.STMT
else:
line_type = LineType.NONE
if line_type != self._last_line_type and self._last_line_type != LineType.BLOCK_START and line_type != LineType.BLOCK_END:
self._result += "\n"
self._last_line_type = line_type
self._result += (" " * self._indent) + line
if newline:
self._result += "\n"
def print(self, line: str, newline: bool = True) -> None:
self._result += line
if line.endswith("{") or line.endswith("["):
if len(self._blocks_need_end):
self._blocks_need_end[-1] = _CLOSING[line[-1]]
self._indent += 1
# 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 (
member.nick == value
or member.c_ident == value
or str(member.value) == value
):
return member.name
return value.replace("-", "_")
def print_attribute(self, name, value, type):
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"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger"))
or type.assignable_to(
self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable")
)
or type.assignable_to(
self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction")
)
or type.assignable_to(
self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger")
)
):
self.print(f"{name}: \"{escape_quote(value)}\";")
elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")):
self.print(f"{name}: {value};")
elif isinstance(type, Enumeration):
for member in type.members.values():
if member.nick == value or member.c_ident == value:
self.print(f"{name}: {member.name};")
break
else:
self.print(f"{name}: {value.replace('-', '_')};")
return "", escape_quote(value)
elif value == self.template_class:
return "", "template"
elif type.assignable_to(
self.gir.namespaces["Gtk"].lookup_type("GObject.Object")
) or isinstance(type, Interface):
return "", ("null" if value == "" else value)
elif isinstance(type, Bitfield):
flags = re.sub(r"\s*\|\s*", " | ", value).replace("-", "_")
self.print(f"{name}: {flags};")
flags = [get_enum_name(flag) for flag in value.split("|")]
return "", " | ".join(flags)
elif isinstance(type, Enumeration):
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(ctx: DecompileCtx, gir, xml):
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 = {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)
for child_type in xml.children.values():
for child in child_type:
_decompile_element(ctx, gir, child)
ctx.end_block()
except UnsupportedError as e:
raise e
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)
def decompile(data):
ctx.end_block()
ctx._node_stack.pop()
except UnsupportedError as e:
raise e
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: str) -> str:
ctx = DecompileCtx()
xml = parse_string(data)
decompile_element(ctx, None, xml)
return ctx.result
def canon(string: str) -> str:
if string == "class":
@ -199,37 +296,61 @@ def canon(string: str) -> str:
else:
return string.replace("-", "_").lower()
def truthy(string: str) -> bool:
return string.lower() in ["yes", "true", "t", "y", "1"]
def full_name(gir):
def truthy(string: str) -> bool:
return string is not None and string.lower() in ["yes", "true", "t", "y", "1"]
def full_name(gir: GirType) -> str:
return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name
def lookup_by_cname(gir, cname: str):
def lookup_by_cname(gir, cname: str) -> T.Optional[GirType]:
if isinstance(gir, GirContext):
return gir.get_type_by_cname(cname)
else:
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
def escape_quote(string: str) -> str:
return (string
.replace("\\", "\\\\")
.replace("\'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n"))
@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
@ -243,14 +364,43 @@ def decompile_placeholder(ctx, gir):
pass
@decompiler("property", cdata=True)
def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=None, bind_flags=None, translatable="false", comments=None, context=None):
name = name.replace("_", "-")
if comments is not None:
ctx.print(f"/* Translators: {comments} */")
def decompile_translatable(
string: str,
translatable: T.Optional[str],
context: T.Optional[str],
comments: T.Optional[str],
) -> T.Tuple[str, str]:
if translatable is not None and truthy(translatable):
if comments is None:
comments = ""
else:
comments = comments.replace("/*", " ").replace("*/", " ")
comments = f"/* Translators: {comments} */"
if context is not None:
return comments, f"C_({escape_quote(context)}, {escape_quote(string)})"
else:
return comments, f"_({escape_quote(string)})"
else:
return "", f"{escape_quote(string)}"
@decompiler("property", cdata=True)
def decompile_property(
ctx: DecompileCtx,
gir,
name,
cdata,
bind_source=None,
bind_property=None,
bind_flags=None,
translatable="false",
comments=None,
context=None,
):
name = name.replace("_", "-")
if cdata is None:
ctx.print(f"{name}: ", False)
ctx.print(f"{name}: ")
ctx.end_block_with(";")
elif bind_source:
flags = ""
@ -261,21 +411,50 @@ def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=No
flags += " inverted"
if "bidirectional" in bind_flags:
flags += " bidirectional"
if bind_source == ctx.template_class:
bind_source = "template"
ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
elif truthy(translatable):
if context is not None:
ctx.print(f"{name}: C_(\"{escape_quote(context)}\", \"{escape_quote(cdata)}\");")
else:
ctx.print(f"{name}: _(\"{escape_quote(cdata)}\");")
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
)
if comments is not None:
ctx.print(comments)
ctx.print(f"{name}: {translatable};")
elif gir is None or gir.properties.get(name) is None:
ctx.print(f"{name}: \"{escape_quote(cdata)}\";")
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
@decompiler("attribute", cdata=True)
def decompile_attribute(ctx, gir, name, cdata, translatable="false", comments=None, context=None):
decompile_property(ctx, gir, name, cdata, translatable=translatable, comments=comments, context=context)
def decompile_attribute(
ctx, gir, name, cdata, translatable="false", comments=None, context=None
):
decompile_property(
ctx,
gir,
name,
cdata,
translatable=translatable,
comments=comments,
context=context,
)
@decompiler("attributes")
def decompile_attributes(ctx, gir):
@ -292,5 +471,7 @@ class UnsupportedError(Exception):
print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}")
if self.tag:
print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}")
print(f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You
probably need to port this file manually.{Colors.CLEAR}\n""")
print(
f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You
probably need to port this file manually.{Colors.CLEAR}\n"""
)

View file

@ -17,24 +17,27 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from dataclasses import dataclass
import sys
import traceback
import typing as T
import sys, traceback
from dataclasses import dataclass
from . import utils
from .tokenizer import Range
from .utils import Colors
class PrintableError(Exception):
"""Parent class for errors that can be pretty-printed for the user, e.g.
compilation warnings and errors."""
def pretty_print(self, filename, code):
def pretty_print(self, filename, code, stream=sys.stdout):
raise NotImplementedError()
@dataclass
class ErrorReference:
start: int
end: int
range: Range
message: str
@ -44,12 +47,20 @@ class CompileError(PrintableError):
category = "error"
color = Colors.RED
def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None, actions=None, fatal=False, references=None):
def __init__(
self,
message: str,
range: T.Optional[Range] = None,
did_you_mean: T.Optional[T.Tuple[str, T.List[str]]] = None,
hints: T.Optional[T.List[str]] = None,
actions: T.Optional[T.List["CodeAction"]] = None,
fatal: bool = False,
references: T.Optional[T.List[ErrorReference]] = None,
) -> None:
super().__init__(message)
self.message = message
self.start = start
self.end = end
self.range = range
self.hints = hints or []
self.actions = actions or []
self.references = references or []
@ -58,12 +69,11 @@ class CompileError(PrintableError):
if did_you_mean is not None:
self._did_you_mean(*did_you_mean)
def hint(self, hint: str):
def hint(self, hint: str) -> "CompileError":
self.hints.append(hint)
return self
def _did_you_mean(self, word: str, options: T.List[str]):
def _did_you_mean(self, word: str, options: T.List[str]) -> None:
if word.replace("_", "-") in options:
self.hint(f"use '-', not '_': `{word.replace('_', '-')}`")
return
@ -79,28 +89,65 @@ class CompileError(PrintableError):
self.hint("Did you check your spelling?")
self.hint("Are your dependencies up to date?")
def pretty_print(self, filename, code, stream=sys.stdout):
line_num, col_num = utils.idx_to_pos(self.start + 1, code)
line = code.splitlines(True)[line_num]
def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None:
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
stream.write(f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
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:
stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n")
for i, action in enumerate(self.actions):
old = (
action.edit_range.text
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"
)
for ref in self.references:
line_num, col_num = utils.idx_to_pos(ref.start + 1, code)
line_num, col_num = utils.idx_to_pos(ref.range.start + 1, code)
line = code.splitlines(True)[line_num]
line_num += 1
stream.write(f"""{Colors.FAINT}note: {ref.message}:
stream.write(
f"""{Colors.FAINT}note: {ref.message}:
at {filename} line {line_num} column {col_num}:
{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""")
{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n"""
)
stream.write("\n")
@ -110,15 +157,29 @@ class CompileWarning(CompileError):
color = Colors.YELLOW
class DeprecatedWarning(CompileWarning):
pass
class UnusedWarning(CompileWarning):
pass
class UpgradeWarning(CompileWarning):
category = "upgrade"
color = Colors.PURPLE
class UnexpectedTokenError(CompileError):
def __init__(self, start, end):
super().__init__("Unexpected tokens", start, end)
def __init__(self, range: Range) -> None:
super().__init__("Unexpected tokens", range)
@dataclass
class CodeAction:
title: str
replace_with: str
edit_range: T.Optional[Range] = None
class MultipleErrors(PrintableError):
@ -126,13 +187,13 @@ class MultipleErrors(PrintableError):
a list and re-thrown using the MultipleErrors exception. It will
pretty-print all of the errors and a count of how many errors there are."""
def __init__(self, errors: T.List[CompileError]):
def __init__(self, errors: T.List[CompileError]) -> None:
super().__init__()
self.errors = errors
def pretty_print(self, filename, code) -> None:
def pretty_print(self, filename, code, stream=sys.stdout) -> None:
for error in self.errors:
error.pretty_print(filename, code)
error.pretty_print(filename, code, stream)
if len(self.errors) != 1:
print(f"{len(self.errors)} errors")
@ -141,7 +202,7 @@ class CompilerBugError(Exception):
"""Emitted on assertion errors"""
def assert_true(truth: bool, message:str=None):
def assert_true(truth: bool, message: T.Optional[str] = None):
if not truth:
raise CompilerBugError(message)
@ -149,11 +210,17 @@ def assert_true(truth: bool, message:str=None):
def report_bug(): # pragma: no cover
"""Report an error and ask people to report it."""
from . import main
print(traceback.format_exc())
print(f"Arguments: {sys.argv}\n")
print(f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG *****
print(f"Arguments: {sys.argv}")
print(f"Version: {main.VERSION}\n")
print(
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.CLEAR}""")
{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue
{Colors.CLEAR}"""
)
sys.exit(1)

View file

@ -0,0 +1,232 @@
# formatter.py
#
# Copyright 2023 Gregor Niehl <gregorniehl@web.de>
#
# 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
import re
from enum import Enum
from . import tokenizer
from .errors import CompilerBugError
from .tokenizer import TokenType
OPENING_TOKENS = ("{", "[")
CLOSING_TOKENS = ("}", "]")
NEWLINE_AFTER = tuple(";") + OPENING_TOKENS + CLOSING_TOKENS
NO_WHITESPACE_BEFORE = (",", ":", "::", ";", ")", ".", ">", "]", "=")
NO_WHITESPACE_AFTER = ("C_", "_", "(", ".", "$", "<", "::", "[", "=")
# NO_WHITESPACE_BEFORE takes precedence over WHITESPACE_AFTER
WHITESPACE_AFTER = (":", ",", ">", ")", "|", "=>")
WHITESPACE_BEFORE = ("{", "|")
class LineType(Enum):
STATEMENT = 0
BLOCK_OPEN = 1
BLOCK_CLOSE = 2
CHILD_TYPE = 3
COMMENT = 4
def format(data, tab_size=2, insert_space=True):
indent_levels = 0
tokens = tokenizer.tokenize(data)
end_str = ""
last_not_whitespace = tokens[0]
current_line = ""
prev_line_type = None
is_child_type = False
indent_item = " " * tab_size if insert_space else "\t"
watch_parentheses = False
parentheses_balance = 0
bracket_tracker = [None]
last_whitespace_contains_newline = False
def commit_current_line(
line_type=prev_line_type, redo_whitespace=False, newlines_before=1
):
nonlocal end_str, current_line, prev_line_type
indent_whitespace = indent_levels * indent_item
whitespace_to_add = "\n" + indent_whitespace
if redo_whitespace or newlines_before != 1:
end_str = end_str.strip() + "\n" * newlines_before
if newlines_before > 0:
end_str += indent_whitespace
end_str += current_line + whitespace_to_add
current_line = ""
prev_line_type = line_type
for item in tokens:
str_item = str(item)
if item.type == TokenType.WHITESPACE:
last_whitespace_contains_newline = "\n" in str_item
continue
whitespace_required = (
str_item in WHITESPACE_BEFORE
or str(last_not_whitespace) in WHITESPACE_AFTER
or (str_item == "(" and end_str.endswith(": bind"))
)
whitespace_blockers = (
str_item in NO_WHITESPACE_BEFORE
or str(last_not_whitespace) in NO_WHITESPACE_AFTER
or (str_item == "<" and str(last_not_whitespace) == "typeof")
)
this_or_last_is_ident = TokenType.IDENT in (item.type, last_not_whitespace.type)
current_line_is_empty = len(current_line) == 0
is_function = str_item == "(" and not re.match(
r"^([A-Za-z_\-])+(: bind)?$", current_line
)
any_blockers = whitespace_blockers or current_line_is_empty or is_function
if (whitespace_required or this_or_last_is_ident) and not any_blockers:
current_line += " "
current_line += str_item
if str_item in ("[", "("):
bracket_tracker.append(str_item)
elif str_item in ("]", ")"):
bracket_tracker.pop()
needs_newline_treatment = (
str_item in NEWLINE_AFTER or item.type == TokenType.COMMENT
)
if needs_newline_treatment:
if str_item in OPENING_TOKENS:
list_or_child_type = str_item == "["
if list_or_child_type:
is_child_type = current_line.startswith("[")
if is_child_type:
if str(last_not_whitespace) not in OPENING_TOKENS:
end_str = (
end_str.strip() + "\n\n" + (indent_item * indent_levels)
)
last_not_whitespace = item
continue
indent_levels += 1
keep_same_indent = prev_line_type not in (
LineType.CHILD_TYPE,
LineType.COMMENT,
LineType.BLOCK_OPEN,
)
if keep_same_indent:
end_str = (
end_str.strip() + "\n\n" + indent_item * (indent_levels - 1)
)
commit_current_line(LineType.BLOCK_OPEN)
elif str_item == "]" and is_child_type:
commit_current_line(LineType.CHILD_TYPE, False)
is_child_type = False
elif str_item in CLOSING_TOKENS:
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:
end_str = end_str.strip()
commit_current_line(LineType.BLOCK_CLOSE, True, 0)
indent_levels -= 1
commit_current_line(LineType.BLOCK_CLOSE, True)
elif str_item == ";":
line_type = LineType.STATEMENT
newlines = 1
if len(current_line) == 1:
newlines = 0
line_type = LineType.BLOCK_CLOSE
elif prev_line_type == LineType.BLOCK_CLOSE:
newlines = 2
commit_current_line(line_type, newlines_before=newlines)
elif item.type == TokenType.COMMENT:
require_extra_newline = (
LineType.BLOCK_CLOSE,
LineType.STATEMENT,
LineType.COMMENT,
)
single_line_comment = str_item.startswith("//")
newlines = 1
if single_line_comment:
if not str_item.startswith("// "):
current_line = f"// {current_line[2:]}"
if not last_whitespace_contains_newline:
current_line = " " + current_line
newlines = 0
elif prev_line_type == LineType.BLOCK_CLOSE:
newlines = 2
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: # pragma: no cover
raise CompilerBugError()
elif str_item == "(" and (
re.match(r"^([A-Za-z_\-])+\s*\(", current_line) or watch_parentheses
):
watch_parentheses = True
parentheses_balance += 1
elif str_item == ")" and watch_parentheses:
parentheses_balance -= 1
all_parentheses_closed = parentheses_balance == 0
if all_parentheses_closed:
commit_current_line(
newlines_before=2 if prev_line_type == LineType.BLOCK_CLOSE else 1
)
watch_parentheses = False
tracker_is_empty = len(bracket_tracker) > 0
if tracker_is_empty:
last_in_tracker = bracket_tracker[-1]
is_list_comma = last_in_tracker == "[" and str_item == ","
if is_list_comma:
last_was_list_item = end_str.strip()[-1] not in ("[", ",")
if last_was_list_item:
end_str = end_str.strip()
commit_current_line()
last_not_whitespace = item
last_whitespace_contains_newline = False
return end_str.strip() + "\n"

File diff suppressed because it is too large Load diff

View file

@ -18,25 +18,27 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import difflib
import os
import typing as T
from . import decompiler, tokenizer, parser
from .errors import MultipleErrors, PrintableError
from . import decompiler, parser, tokenizer
from .errors import CompilerBugError, MultipleErrors, PrintableError
from .outputs.xml import XmlOutput
from .utils import Colors
# A tool to interactively port projects to blueprints.
class CouldNotPort:
def __init__(self, message):
def __init__(self, message: str):
self.message = message
def change_suffix(f):
return f.removesuffix(".ui") + ".blp"
def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
if os.path.exists(out_file):
return CouldNotPort("already exists")
@ -54,19 +56,23 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
if errors:
raise errors
if len(ast.errors):
raise MultipleErrors(ast.errors)
if not ast:
raise CompilerBugError()
ast.generate()
output = XmlOutput()
output.emit(ast)
except PrintableError as e:
e.pretty_print(out_file, decompiled)
print(f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}")
print(
f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}"
)
print(f"in {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE}")
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")
@ -82,10 +88,12 @@ def listdir_recursive(subdir):
for file in files:
if file in ["_build", "build"]:
continue
if file.startswith("."):
continue
full = os.path.join(subdir, file)
if full == "./subprojects":
# skip the subprojects directory
return
continue
if os.path.isfile(full):
yield full
elif os.path.isdir(full):
@ -106,7 +114,9 @@ def enter():
def step1():
print(f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}")
print(
f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}"
)
if os.path.exists("subprojects/blueprint-compiler.wrap"):
print("subprojects/blueprint-compiler.wrap already exists, skipping\n")
@ -119,17 +129,20 @@ def step1():
pass
from .main import VERSION
VERSION = "main" if VERSION == "uninstalled" else "v" + VERSION
with open("subprojects/blueprint-compiler.wrap", "w") as wrap:
wrap.write(f"""[wrap-git]
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
[provide]
program_names = blueprint-compiler""")
program_names = blueprint-compiler"""
)
print()
@ -144,7 +157,9 @@ def step2():
if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"):
gitignore.write("\n/subprojects/blueprint-compiler\n")
else:
print("'/subprojects/blueprint-compiler' already in .gitignore, skipping")
print(
"'/subprojects/blueprint-compiler' already in .gitignore, skipping"
)
else:
if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"):
with open(".gitignore", "w") as gitignore:
@ -167,9 +182,13 @@ def step3():
if isinstance(result, CouldNotPort):
if result.message == "already exists":
print(Colors.FAINT, end="")
print(f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}")
print(
f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}"
)
else:
print(f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}")
print(
f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}"
)
success += 1
print()
@ -178,7 +197,9 @@ def step3():
elif success == len(files):
print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}")
elif success > 0:
print(f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}")
print(
f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}"
)
else:
print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}")
@ -202,22 +223,33 @@ def step3():
def step4(ported):
print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}")
print(f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}")
print(
f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}"
)
meson_files = [file for file in listdir_recursive(".") if os.path.basename(file) == "meson.build"]
meson_files = [
file
for file in listdir_recursive(".")
if os.path.basename(file) == "meson.build"
]
for meson_file in meson_files:
with open(meson_file, "r") as f:
if "gnome.compile_resources" in f.read():
parent = os.path.dirname(meson_file)
file_list = "\n ".join([
file_list = "\n ".join(
[
f"'{os.path.relpath(file, parent)}',"
for file in ported
if file.startswith(parent)
])
]
)
if len(file_list):
print(f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}")
print(f"""
print(
f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}"
)
print(
f"""
blueprints = custom_target('blueprints',
input: files(
{file_list}
@ -225,14 +257,17 @@ blueprints = custom_target('blueprints',
output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
)
""")
"""
)
enter()
print(f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()'
print(
f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()'
arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}
dependencies: blueprints,
""")
"""
)
enter()
print()
@ -242,7 +277,9 @@ def step5(in_files):
print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}")
if not os.path.exists("po/POTFILES.in"):
print(f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n")
print(
f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n"
)
return
with open("po/POTFILES.in", "r") as potfiles:
@ -255,12 +292,22 @@ def step5(in_files):
new_data = "".join(lines)
print(f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}")
print(
"".join([
(Colors.GREEN if line.startswith('+') else Colors.RED + Colors.FAINT if line.startswith('-') else '') + line + Colors.CLEAR
f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}"
)
print(
"".join(
[
(
Colors.GREEN
if line.startswith("+")
else Colors.RED + Colors.FAINT if line.startswith("-") else ""
)
+ line
+ Colors.CLEAR
for line in difflib.unified_diff(old_lines, lines)
])
]
)
)
if yesno("Is this ok?"):
@ -289,5 +336,6 @@ def run(opts):
step5(in_files)
step6(in_files)
print(f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}")
print(
f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}"
)

View file

@ -1,49 +1,78 @@
""" Contains all the syntax beyond basic objects, properties, signal, and
templates. """
from .attributes import BaseAttribute, BaseTypedAttribute
from .adw_breakpoint import (
AdwBreakpointCondition,
AdwBreakpointSetter,
AdwBreakpointSetters,
)
from .adw_response_dialog import ExtAdwResponseDialog
from .binding import Binding
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
from .expression import (
CastExpr,
ClosureArg,
ClosureExpr,
ExprBase,
Expression,
LiteralExpr,
LookupOp,
)
from .gobject_object import Object, ObjectContent
from .gobject_property import Property
from .gobject_signal import Signal
from .gtk_a11y import A11y
from .gtk_combo_box_text import Items
from .gtk_file_filter import mime_types, patterns, suffixes
from .gtk_layout import Layout
from .gtk_menu import menu
from .gtk_size_group import Widgets
from .gtk_string_list import Strings
from .gtk_styles import Styles
from .gtkbuilder_child import Child
from .gtk_a11y import A11yProperty, ExtAccessibility
from .gtk_combo_box_text import ExtComboBoxItems
from .gtk_file_filter import (
Filters,
ext_file_filter_mime_types,
ext_file_filter_patterns,
ext_file_filter_suffixes,
)
from .gtk_layout import ExtLayout
from .gtk_list_item_factory import ExtListItemFactory
from .gtk_menu import Menu, MenuAttribute, menu
from .gtk_scale import ExtScaleMarks
from .gtk_size_group import ExtSizeGroupWidgets
from .gtk_string_list import ExtStringListStrings
from .gtk_styles import ExtStyles
from .gtkbuilder_child import Child, ChildExtension, ChildInternal, ChildType
from .gtkbuilder_template import Template
from .imports import GtkDirective, Import
from .types import ClassName
from .ui import UI
from .values import IdentValue, TranslatedStringValue, FlagsValue, LiteralValue
from .common import *
OBJECT_HOOKS.children = [
menu,
Object,
]
from .values import (
ArrayValue,
ExprValue,
Flag,
Flags,
IdentLiteral,
Literal,
NumberLiteral,
ObjectValue,
QuotedLiteral,
StringValue,
Translated,
TypeLiteral,
Value,
)
OBJECT_CONTENT_HOOKS.children = [
Signal,
Property,
A11y,
Styles,
Layout,
mime_types,
patterns,
suffixes,
Widgets,
Items,
Strings,
AdwBreakpointCondition,
AdwBreakpointSetters,
ExtAccessibility,
ExtAdwResponseDialog,
ExtComboBoxItems,
ext_file_filter_mime_types,
ext_file_filter_patterns,
ext_file_filter_suffixes,
ExtLayout,
ExtListItemFactory,
ExtScaleMarks,
ExtSizeGroupWidgets,
ExtStringListStrings,
ExtStyles,
Child,
]
VALUE_HOOKS.children = [
TranslatedStringValue,
FlagsValue,
IdentValue,
LiteralValue,
]
LITERAL.children = [Literal]

View file

@ -0,0 +1,254 @@
# adw_breakpoint.py
#
# Copyright 2023 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 *
from .contexts import ScopeCtx, ValueTypeCtx
from .gobject_object import Object, validate_parent_type
from .values import Value
class AdwBreakpointCondition(AstNode):
grammar = [
UseExact("kw", "condition"),
"(",
UseQuoted("condition"),
Match(")").expected(),
]
@property
def condition(self) -> str:
return self.tokens["condition"]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"condition",
SymbolKind.Property,
self.range,
self.group.tokens["kw"].range,
self.condition,
)
@docs("kw")
def keyword_docs(self):
klass = self.root.gir.get_type("Breakpoint", "Adw")
if klass is None:
return None
prop = klass.properties.get("condition")
assert isinstance(prop, gir.Property)
return prop.doc
@validate()
def unique(self):
self.validate_unique_in_parent("Duplicate condition statement")
class AdwBreakpointSetter(AstNode):
grammar = Statement(
UseIdent("object"),
Match(".").expected(),
UseIdent("property"),
Match(":").expected(),
Value,
)
@property
def object_id(self) -> str:
return self.tokens["object"]
@property
def object(self) -> T.Optional[Object]:
return self.context[ScopeCtx].objects.get(self.object_id)
@property
def property_name(self) -> T.Optional[str]:
return self.tokens["property"]
@property
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]:
if self.object is not None:
return self.object.gir_class
else:
return None
@property
def gir_property(self) -> T.Optional[gir.Property]:
if (
self.gir_class is not None
and not isinstance(self.gir_class, ExternType)
and self.property_name is not None
):
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) -> T.Optional[DocumentSymbol]:
if self.value is None:
return None
return DocumentSymbol(
f"{self.object_id}.{self.property_name}",
SymbolKind.Property,
self.range,
self.group.tokens["object"].range,
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:
type = self.gir_property.type
else:
type = None
return ValueTypeCtx(type, allow_null=True)
@docs("object")
def object_docs(self):
if self.object is not None:
return f"```\n{self.object.signature}\n```"
else:
return None
@docs("property")
def property_docs(self):
if self.gir_property is not None:
return self.gir_property.doc
else:
return None
@validate("object")
def object_exists(self):
if self.object is None:
raise CompileError(
f"Could not find object with ID {self.object_id}",
did_you_mean=(self.object_id, self.context[ScopeCtx].objects.keys()),
)
@validate("property")
def property_exists(self):
if self.gir_class is None or self.gir_class.incomplete:
# Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself
return
if self.gir_property is None and self.property_name is not None:
raise CompileError(
f"Class {self.gir_class.full_name} does not have a property called {self.property_name}",
did_you_mean=(self.property_name, self.gir_class.properties.keys()),
)
@validate()
def unique(self):
self.validate_unique_in_parent(
f"Duplicate setter for {self.object_id}.{self.property_name}",
lambda x: x.object_id == self.object_id
and x.property_name == self.property_name,
)
class AdwBreakpointSetters(AstNode):
grammar = [
Keyword("setters"),
Match("{").expected(),
Until(AdwBreakpointSetter, "}"),
]
@property
def setters(self) -> T.List[AdwBreakpointSetter]:
return self.children[AdwBreakpointSetter]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"setters",
SymbolKind.Struct,
self.range,
self.group.tokens["setters"].range,
)
@validate()
def container_is_breakpoint(self):
validate_parent_type(self, "Adw", "Breakpoint", "breakpoint setters")
@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

@ -0,0 +1,192 @@
# adw_response_dialog.py
#
# Copyright 2023 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 ..decompiler import decompile_translatable, truthy
from .common import *
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue
class ExtAdwResponseDialogFlag(AstNode):
grammar = AnyOf(
UseExact("flag", "destructive"),
UseExact("flag", "suggested"),
UseExact("flag", "disabled"),
)
@property
def flag(self) -> str:
return self.tokens["flag"]
@validate()
def unique(self):
self.validate_unique_in_parent(
f"Duplicate '{self.flag}' flag", check=lambda child: child.flag == self.flag
)
@validate()
def exclusive(self):
if self.flag in ["destructive", "suggested"]:
self.validate_unique_in_parent(
"'suggested' and 'destructive' are exclusive",
check=lambda child: child.flag in ["destructive", "suggested"],
)
class ExtAdwResponseDialogResponse(AstNode):
grammar = [
UseIdent("id"),
Match(":").expected(),
to_parse_node(StringValue).expected("a string or translatable string"),
ZeroOrMore(ExtAdwResponseDialogFlag),
]
@property
def id(self) -> str:
return self.tokens["id"]
@property
def flags(self) -> T.List[ExtAdwResponseDialogFlag]:
return self.children[ExtAdwResponseDialogFlag]
@property
def appearance(self) -> T.Optional[str]:
if any(flag.flag == "destructive" for flag in self.flags):
return "destructive"
elif any(flag.flag == "suggested" for flag in self.flags):
return "suggested"
else:
return None
@property
def enabled(self) -> bool:
return not any(flag.flag == "disabled" for flag in self.flags)
@property
def value(self) -> StringValue:
return self.children[0]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.id,
SymbolKind.Field,
self.range,
self.group.tokens["id"].range,
self.value.range.text,
)
@validate("id")
def unique_in_parent(self):
self.validate_unique_in_parent(
f"Duplicate response ID '{self.id}'",
check=lambda child: child.id == self.id,
)
class ExtAdwResponseDialog(AstNode):
grammar = [
Keyword("responses"),
Match("[").expected(),
Delimited(ExtAdwResponseDialogResponse, ","),
"]",
]
@property
def responses(self) -> T.List[ExtAdwResponseDialogResponse]:
return self.children
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"responses",
SymbolKind.Array,
self.range,
self.group.tokens["responses"].range,
)
@validate("responses")
def container_is_message_dialog_or_alert_dialog(self):
try:
validate_parent_type(self, "Adw", "MessageDialog", "responses")
except:
validate_parent_type(self, "Adw", "AlertDialog", "responses")
@validate("responses")
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],
applies_in_subclass=("Adw", "MessageDialog"),
matches=new_statement_patterns,
)
def complete_adw_message_dialog(lsp, ast_node, match_variables):
yield Completion(
"responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]"
)
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Adw", "AlertDialog"),
matches=new_statement_patterns,
)
def complete_adw_alert_dialog(lsp, ast_node, match_variables):
yield Completion(
"responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]"
)
@decompiler("responses")
def decompile_responses(ctx, gir):
ctx.print(f"responses [")
@decompiler("response", cdata=True)
def decompile_response(
ctx,
gir,
cdata,
id,
appearance=None,
enabled=None,
translatable=None,
context=None,
comments=None,
):
comments, translated = decompile_translatable(
cdata, translatable, context, comments
)
if comments is not None:
ctx.print(comments)
flags = ""
if appearance is not None:
flags += f" {appearance}"
if enabled is not None and not truthy(enabled):
flags += " disabled"
ctx.print(f"{id}: {translated}{flags},")

View file

@ -1,49 +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 .values import Value, TranslatedStringValue
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"]
def emit_xml(self, xml: XmlEmitter):
value = self.children[Value][0]
attrs = { self.attr_name: self.name }
if isinstance(value, TranslatedStringValue):
attrs = { **attrs, **value.attrs }
xml.start_tag(self.tag_name, **attrs)
value.emit_xml(xml)
xml.end_tag()
class BaseTypedAttribute(BaseAttribute):
""" A BaseAttribute whose parent has a value_type property that can assist
in validation. """

View file

@ -0,0 +1,123 @@
# binding.py
#
# Copyright 2023 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 dataclasses import dataclass
from .common import *
from .expression import Expression, LiteralExpr, LookupOp
class BindingFlag(AstNode):
grammar = [
AnyOf(
UseExact("flag", "inverted"),
UseExact("flag", "bidirectional"),
UseExact("flag", "no-sync-create"),
UseExact("flag", "sync-create"),
)
]
@property
def flag(self) -> str:
return self.tokens["flag"]
@validate()
def sync_create(self):
if self.flag == "sync-create":
raise UpgradeWarning(
"'sync-create' is now the default. Use 'no-sync-create' if this is not wanted.",
actions=[CodeAction("remove 'sync-create'", "")],
)
@validate()
def unique(self):
self.validate_unique_in_parent(
f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag
)
@validate()
def flags_only_if_simple(self):
if self.parent.simple_binding is None:
raise CompileError(
"Only bindings with a single lookup can have flags",
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax Binding")
class Binding(AstNode):
grammar = [
AnyOf(Keyword("bind"), UseExact("bind", "bind-property")),
Expression,
ZeroOrMore(BindingFlag),
]
@property
def expression(self) -> Expression:
return self.children[Expression][0]
@property
def flags(self) -> T.List[BindingFlag]:
return self.children[BindingFlag]
@property
def simple_binding(self) -> T.Optional["SimpleBinding"]:
if isinstance(self.expression.last, LookupOp):
if isinstance(self.expression.last.lhs, LiteralExpr):
from .values import IdentLiteral
if isinstance(self.expression.last.lhs.literal.value, IdentLiteral):
flags = [x.flag for x in self.flags]
return SimpleBinding(
self.expression.last.lhs.literal.value.ident,
self.expression.last.property_name,
no_sync_create="no-sync-create" in flags,
bidirectional="bidirectional" in flags,
inverted="inverted" in flags,
)
return None
@validate("bind")
def bind_property(self):
if self.tokens["bind"] == "bind-property":
raise UpgradeWarning(
"'bind-property' is no longer needed. Use 'bind' instead. (blueprint 0.8.2)",
actions=[CodeAction("use 'bind'", "bind")],
)
@docs("bind")
def ref_docs(self):
return get_docs_section("Syntax Binding")
@dataclass
class SimpleBinding:
source: str
property_name: str
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

@ -18,19 +18,46 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .. import gir
from ..ast_utils import AstNode, validate, docs
from ..errors import CompileError, MultipleErrors
from ..completions_utils import *
from .. import decompiler as decompile
from ..decompiler import DecompileCtx, decompiler
from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration
from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType
from .. import gir
from ..ast_utils import AstNode, context, docs, validate
from ..completions_utils import *
from ..decompiler import (
DecompileCtx,
decompile_translatable,
decompiler,
escape_quote,
truthy,
)
from ..errors import (
CodeAction,
CompileError,
CompileWarning,
DeprecatedWarning,
MultipleErrors,
UnusedWarning,
UpgradeWarning,
)
from ..gir import (
BoolType,
Enumeration,
ExternType,
FloatType,
GirType,
IntType,
StringType,
)
from ..lsp_utils import (
Completion,
CompletionItemKind,
DocumentSymbol,
LocationLink,
SemanticToken,
SemanticTokenType,
SymbolKind,
get_docs_section,
)
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
OBJECT_HOOKS = AnyOf()
OBJECT_CONTENT_HOOKS = AnyOf()
VALUE_HOOKS = AnyOf()
LITERAL = AnyOf()

View file

@ -0,0 +1,87 @@
# contexts.py
#
# Copyright 2023 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
import typing as T
from dataclasses import dataclass
from functools import cached_property
from .common import *
from .gobject_object import Object
from .gtkbuilder_template import Template
@dataclass
class ValueTypeCtx:
value_type: T.Optional[GirType]
allow_null: bool = False
must_infer_type: bool = False
@dataclass
class ScopeCtx:
node: AstNode
@cached_property
def template(self):
from .gtk_list_item_factory import ExtListItemFactory
from .ui import UI
if isinstance(self.node, UI):
return self.node.template
elif isinstance(self.node, ExtListItemFactory):
return self.node
@cached_property
def objects(self) -> T.Dict[str, Object]:
return {
obj.tokens["id"]: obj
for obj in self._iter_recursive(self.node)
if obj.tokens["id"] is not None
}
def validate_unique_ids(self) -> None:
from .gtk_list_item_factory import ExtListItemFactory
passed = {}
for obj in self._iter_recursive(self.node):
if obj.tokens["id"] is None:
continue
if obj.tokens["id"] in passed:
token = obj.group.tokens["id"]
if not isinstance(obj, Template) and not isinstance(
obj, ExtListItemFactory
):
raise CompileError(
f"Duplicate object ID '{obj.tokens['id']}'",
token.range,
)
passed[obj.tokens["id"]] = obj
def _iter_recursive(self, node: AstNode):
yield node
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

@ -0,0 +1,384 @@
# expressions.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 ..decompiler import decompile_element
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
from .types import TypeName
expr = Sequence()
class ExprBase(AstNode):
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
if rhs := self.rhs:
return rhs.context[ValueTypeCtx]
else:
return self.parent.context[ValueTypeCtx]
@property
def type(self) -> T.Optional[GirType]:
raise NotImplementedError()
@property
def rhs(self) -> T.Optional["ExprBase"]:
if isinstance(self.parent, Expression):
children = list(self.parent.children)
if children.index(self) + 1 < len(children):
return children[children.index(self) + 1]
else:
return self.parent.rhs
else:
return None
class Expression(ExprBase):
grammar = expr
@property
def last(self) -> ExprBase:
return self.children[-1]
@property
def type(self) -> T.Optional[GirType]:
return self.last.type
class InfixExpr(ExprBase):
@property
def lhs(self):
children = list(self.parent_by_type(Expression).children)
return children[children.index(self) - 1]
class LiteralExpr(ExprBase):
grammar = LITERAL
@property
def is_object(self) -> bool:
from .values import IdentLiteral
return isinstance(self.literal.value, IdentLiteral) and (
self.literal.value.ident in self.context[ScopeCtx].objects
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
return self.children[Literal][0]
@property
def type(self) -> T.Optional[GirType]:
return self.literal.value.type
@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 not isinstance(self.rhs.rhs, LookupOp):
raise CompileError('"item" can only be used for looking up properties')
class LookupOp(InfixExpr):
grammar = [".", UseIdent("property")]
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(None, must_infer_type=True)
@property
def property_name(self) -> str:
return self.tokens["property"]
@property
def type(self) -> T.Optional[GirType]:
if isinstance(self.lhs.type, gir.Class) or isinstance(
self.lhs.type, gir.Interface
):
if property := self.lhs.type.properties.get(self.property_name):
return property.type
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:
# Literal values throw their own errors if the type isn't known
if isinstance(self.lhs, LiteralExpr):
return
raise CompileError(
f"Could not determine the type of the preceding expression",
hints=[
f"add a type cast so blueprint knows which type the property {self.property_name} belongs to"
],
)
if self.lhs.type.incomplete:
return
elif not isinstance(self.lhs.type, gir.Class) and not isinstance(
self.lhs.type, gir.Interface
):
raise CompileError(
f"Type {self.lhs.type.full_name} does not have properties"
)
elif self.lhs.type.properties.get(self.property_name) is None:
raise CompileError(
f"{self.lhs.type.full_name} does not have a property called {self.property_name}",
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 = [
Keyword("as"),
AnyOf(
["<", TypeName, Match(">").expected()],
[
UseExact("lparen", "("),
TypeName,
UseExact("rparen", ")").expected("')'"),
],
),
]
@context(ValueTypeCtx)
def value_type(self):
return ValueTypeCtx(self.type)
@property
def type(self) -> T.Optional[GirType]:
return self.children[TypeName][0].gir_type
@validate()
def cast_makes_sense(self):
if self.type is None or self.lhs.type is None:
return
if not self.type.assignable_to(self.lhs.type):
raise CompileError(
f"Invalid cast. No instance of {self.lhs.type.full_name} can be an instance of {self.type.full_name}."
)
@validate("lparen", "rparen")
def upgrade_to_angle_brackets(self):
if self.tokens["lparen"]:
raise UpgradeWarning(
"Use angle bracket syntax introduced in blueprint 0.8.0",
actions=[
CodeAction(
"Use <> instead of ()",
f"<{self.children[TypeName][0].as_string}>",
)
],
)
@docs("as")
def ref_docs(self):
return get_docs_section("Syntax CastExpression")
class ClosureArg(AstNode):
grammar = Expression
@property
def expr(self) -> Expression:
return self.children[Expression][0]
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(None)
class ClosureExpr(ExprBase):
grammar = [
Optional(["$", UseLiteral("extern", True)]),
UseIdent("name"),
"(",
Delimited(ClosureArg, ","),
")",
]
@property
def type(self) -> T.Optional[GirType]:
if isinstance(self.rhs, CastExpr):
return self.rhs.type
else:
return None
@property
def closure_name(self) -> str:
return self.tokens["name"]
@property
def args(self) -> T.List[ClosureArg]:
return self.children[ClosureArg]
@validate()
def cast_to_return_type(self):
if not isinstance(self.rhs, CastExpr):
raise CompileError(
"Closure expression must be cast to the closure's return type"
)
@validate()
def builtin_exists(self):
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

@ -21,8 +21,25 @@
import typing as T
from functools import cached_property
from blueprintcompiler.errors import T
from blueprintcompiler.lsp_utils import DocumentSymbol
from .common import *
from .response_id import ResponseId
from .response_id import ExtResponse
from .types import ClassName, ConcreteClassName
RESERVED_IDS = {
"this",
"self",
"template",
"true",
"false",
"null",
"none",
"item",
"expr",
"typeof",
}
class ObjectContent(AstNode):
@ -32,59 +49,53 @@ class ObjectContent(AstNode):
def gir_class(self):
return self.parent.gir_class
def emit_xml(self, xml: XmlEmitter):
for x in self.children:
x.emit_xml(xml)
class Object(AstNode):
grammar: T.Any = [
class_name,
ConcreteClassName,
Optional(UseIdent("id")),
ObjectContent,
]
@validate("namespace")
def gir_ns_exists(self):
if not self.tokens["ignore_gir"]:
self.root.gir.validate_ns(self.tokens["namespace"])
@property
def id(self) -> str:
return self.tokens["id"]
@validate("class_name")
def gir_class_exists(self):
if self.tokens["class_name"] and not self.tokens["ignore_gir"] and self.gir_ns is not None:
self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"])
@property
def class_name(self) -> ClassName:
return self.children[ClassName][0]
@validate("namespace", "class_name")
def not_abstract(self):
if self.gir_class is not None and self.gir_class.abstract:
raise CompileError(
f"{self.gir_class.full_name} can't be instantiated because it's abstract",
hints=[f"did you mean to use a subclass of {self.gir_class.full_name}?"]
@property
def content(self) -> ObjectContent:
return self.children[ObjectContent][0]
@property
def signature(self) -> str:
if self.id:
return f"{self.class_name.gir_type.full_name} {self.id}"
elif t := self.class_name.gir_type:
return f"{t.full_name}"
else:
return f"{self.class_name.as_string}"
@property
def document_symbol(self) -> T.Optional[DocumentSymbol]:
return DocumentSymbol(
self.class_name.as_string,
SymbolKind.Object,
self.range,
self.children[ClassName][0].range,
self.id,
)
@property
def gir_ns(self):
if not self.tokens["ignore_gir"]:
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
@property
def gir_class(self):
if self.tokens["class_name"] and not self.tokens["ignore_gir"]:
return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"])
@docs("namespace")
def namespace_docs(self):
if ns := self.root.gir.namespaces.get(self.tokens["namespace"]):
return ns.doc
@docs("class_name")
def class_docs(self):
if self.gir_class:
return self.gir_class.doc
def gir_class(self) -> GirType:
if self.class_name is None:
raise CompilerBugError()
return self.class_name.gir_type
@cached_property
def action_widgets(self) -> T.List[ResponseId]:
def action_widgets(self) -> T.List[ExtResponse]:
"""Get list of widget's action widgets.
Empty if object doesn't have action widgets.
@ -93,44 +104,36 @@ class Object(AstNode):
return [
child.response_id
for child in self.children[ObjectContent][0].children[Child]
for child in self.content.children[Child]
if child.response_id
]
def emit_xml(self, xml: XmlEmitter):
from .gtkbuilder_child import Child
@validate("id")
def object_id_not_reserved(self):
from .gtkbuilder_template import Template
xml.start_tag("object", **{
"class": self.gir_class or self.tokens["class_name"],
"id": self.tokens["id"],
})
for child in self.children:
child.emit_xml(xml)
# List action widgets
action_widgets = self.action_widgets
if action_widgets:
xml.start_tag("action-widgets")
for action_widget in action_widgets:
action_widget.emit_action_widget(xml)
xml.end_tag()
xml.end_tag()
if not isinstance(self, Template) and self.id in RESERVED_IDS:
raise CompileWarning(f"{self.id} may be a confusing object ID")
def validate_parent_type(node, ns: str, name: str, err_msg: str):
parent = node.root.gir.get_type(name, ns)
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}")
raise CompileError(
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
klass_name = (
decompile.full_name(gir_class) if gir_class is not None else "$" + klass
)
if id is None:
ctx.print(f"{klass_name} {{")
else:
ctx.print(f"{klass_name} {id} {{")
ctx.push_obj_type(gir_class)
return gir_class

View file

@ -18,79 +18,83 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import Object
from .gtkbuilder_template import Template
from .values import Value, TranslatedStringValue
from .binding import Binding
from .common import *
from .contexts import ValueTypeCtx
from .values import ArrayValue, ExprValue, ObjectValue, Value
class Property(AstNode):
grammar = AnyOf(
Statement(
UseIdent("name"),
":",
Keyword("bind"),
UseIdent("bind_source").expected("the ID of a source object to bind from"),
".",
UseIdent("bind_property").expected("a property name to bind from"),
ZeroOrMore(AnyOf(
["no-sync-create", UseLiteral("no_sync_create", True)],
["inverted", UseLiteral("inverted", True)],
["bidirectional", UseLiteral("bidirectional", True)],
Match("sync-create").warn("sync-create is deprecated in favor of no-sync-create"),
)),
),
Statement(
UseIdent("name"),
":",
AnyOf(
OBJECT_HOOKS,
VALUE_HOOKS,
).expected("a 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, ExprValue, ObjectValue, Value, ArrayValue]:
return self.children[0]
@property
def gir_class(self):
return self.parent.parent.gir_class
@property
def gir_property(self):
if self.gir_class is not None:
def gir_property(self) -> T.Optional[gir.Property]:
if self.gir_class is not None and not isinstance(self.gir_class, ExternType):
return self.gir_class.properties.get(self.tokens["name"])
else:
return None
@property
def value_type(self):
if self.gir_property is not None:
return self.gir_property.type
def document_symbol(self) -> DocumentSymbol:
if isinstance(self.value, ObjectValue) or self.value is None:
detail = None
else:
detail = self.value.range.text
return DocumentSymbol(
self.name,
SymbolKind.Property,
self.range,
self.group.tokens["name"].range,
detail,
)
@validate()
def binding_valid(self):
if (
isinstance(self.value, Binding)
and self.gir_property is not None
and self.gir_property.construct_only
):
raise CompileError(
f"{self.gir_property.full_name} can't be bound because it is construct-only",
hints=["construct-only properties may only be set to a static value"],
)
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
if self.gir_property is not None:
type = self.gir_property.type
else:
type = None
return ValueTypeCtx(type)
@validate("name")
def property_exists(self):
if self.gir_class is None:
if self.gir_class is None or self.gir_class.incomplete:
# Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself
return
if isinstance(self.parent.parent, Template):
# If the property is part of a template, it might be defined by
# the application and thus not in gir
return
if self.gir_property is None:
raise CompileError(
f"Class {self.gir_class.full_name} does not contain a property called {self.tokens['name']}",
did_you_mean=(self.tokens["name"], self.gir_class.properties.keys())
)
@validate("bind")
def property_bindable(self):
if self.tokens["bind"] and self.gir_property is not None and self.gir_property.construct_only:
raise CompileError(
f"{self.gir_property.full_name} can't be bound because it is construct-only",
hints=["construct-only properties may only be set to a static value"]
f"Class {self.gir_class.full_name} does not have a property called {self.tokens['name']}",
did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()),
)
@validate("name")
@ -98,64 +102,25 @@ class Property(AstNode):
if self.gir_property is not None and not self.gir_property.writable:
raise CompileError(f"{self.gir_property.full_name} is not writable")
@validate()
def obj_property_type(self):
if len(self.children[Object]) == 0:
return
object = self.children[Object][0]
type = self.value_type
if object and type and object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
)
@validate("name")
def unique_in_parent(self):
self.validate_unique_in_parent(
f"Duplicate property '{self.tokens['name']}'",
check=lambda child: child.tokens["name"] == self.tokens["name"]
check=lambda child: child.tokens["name"] == self.tokens["name"],
)
@validate("name")
def deprecated(self) -> None:
if self.gir_property is not None and self.gir_property.deprecated:
hints = []
if self.gir_property.deprecated_doc:
hints.append(self.gir_property.deprecated_doc)
raise DeprecatedWarning(
f"{self.gir_property.signature} is deprecated",
hints=hints,
)
@docs("name")
def property_docs(self):
if self.gir_property is not None:
return self.gir_property.doc
def emit_xml(self, xml: XmlEmitter):
values = self.children[Value]
value = values[0] if len(values) == 1 else None
bind_flags = []
if self.tokens["bind_source"] and not self.tokens["no_sync_create"]:
bind_flags.append("sync-create")
if self.tokens["inverted"]:
bind_flags.append("invert-boolean")
if self.tokens["bidirectional"]:
bind_flags.append("bidirectional")
bind_flags_str = "|".join(bind_flags) or None
props = {
"name": self.tokens["name"],
"bind-source": self.tokens["bind_source"],
"bind-property": self.tokens["bind_property"],
"bind-flags": bind_flags_str,
}
if isinstance(value, TranslatedStringValue):
props = { **props, **value.attrs }
if len(self.children[Object]) == 1:
xml.start_tag("property", **props)
self.children[Object][0].emit_xml(xml)
xml.end_tag()
elif value is None:
xml.put_self_closing("property", **props)
else:
xml.start_tag("property", **props)
value.emit_xml(xml)
xml.end_tag()

View file

@ -17,97 +17,233 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from .gtkbuilder_template import Template
from .common import *
from .contexts import ScopeCtx
from .gtkbuilder_template import Template
class SignalFlag(AstNode):
grammar = AnyOf(
UseExact("flag", "swapped"),
UseExact("flag", "not-swapped"),
UseExact("flag", "after"),
)
@property
def flag(self) -> str:
return self.tokens["flag"]
@validate()
def unique(self):
self.validate_unique_in_parent(
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(
UseIdent("name"),
Optional([
Optional(
[
"::",
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"),
Match("(").expected("argument list"),
Optional(UseIdent("object")).expected("object identifier"),
Match(")").expected(),
ZeroOrMore(AnyOf(
[Keyword("swapped"), UseLiteral("swapped", True)],
[Keyword("after"), UseLiteral("after", True)],
)),
ZeroOrMore(SignalFlag),
Mark("detail_end"),
)
@property
def name(self) -> str:
return self.tokens["name"]
@property
def gir_signal(self):
if self.gir_class is not None:
return self.gir_class.signals.get(self.tokens["name"])
def detail_name(self) -> T.Optional[str]:
return self.tokens["detail_name"]
@property
def full_name(self) -> str:
if self.detail_name is None:
return self.name
else:
return self.name + "::" + self.detail_name
@property
def handler(self) -> str:
return self.tokens["handler"]
@property
def object_id(self) -> T.Optional[str]:
return self.tokens["object"]
@property
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) -> 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:
return any(x.flag == "after" for x in self.flags)
@property
def gir_signal(self) -> T.Optional[gir.Signal]:
if self.gir_class is not None and not isinstance(self.gir_class, ExternType):
return self.gir_class.signals.get(self.tokens["name"])
else:
return None
@property
def gir_class(self):
return self.parent.parent.gir_class
@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,
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"]:
if self.handler is not None:
raise UpgradeWarning(
"Use the '$' extern syntax introduced in blueprint 0.8.0",
actions=[CodeAction("Use '$' syntax", "$" + self.handler)],
)
@validate("name")
def signal_exists(self):
if self.gir_class is None:
if self.gir_class is None or self.gir_class.incomplete:
# Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself
return
if isinstance(self.parent.parent, Template):
# If the signal is part of a template, it might be defined by
# the application and thus not in gir
return
if self.gir_signal is None:
raise CompileError(
f"Class {self.gir_class.full_name} does not contain a signal called {self.tokens['name']}",
did_you_mean=(self.tokens["name"], self.gir_class.signals.keys())
did_you_mean=(self.tokens["name"], self.gir_class.signals.keys()),
)
@validate("object")
def object_exists(self):
object_id = self.tokens["object"]
if object_id is None:
return
if self.root.objects_by_id.get(object_id) is None:
raise CompileError(
f"Could not find object with ID '{object_id}'"
)
if self.context[ScopeCtx].objects.get(object_id) is None:
raise CompileError(f"Could not find object with ID '{object_id}'")
@validate("name")
def deprecated(self) -> None:
if self.gir_signal is not None and self.gir_signal.deprecated:
hints = []
if self.gir_signal.deprecated_doc:
hints.append(self.gir_signal.deprecated_doc)
raise DeprecatedWarning(
f"{self.gir_signal.signature} is deprecated",
hints=hints,
)
@docs("name")
def signal_docs(self):
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
def emit_xml(self, xml: XmlEmitter):
name = self.tokens["name"]
if self.tokens["detail_name"]:
name += "::" + self.tokens["detail_name"]
xml.put_self_closing(
"signal",
name=name,
handler=self.tokens["handler"],
swapped="true" if self.tokens["swapped"] else None,
object=self.tokens["object"]
)
@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,10 +17,12 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .attributes import BaseTypedAttribute
from .values import Value
import typing as T
from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type
from .values import Value
def get_property_types(gir):
@ -86,6 +88,7 @@ def get_state_types(gir):
"selected": BoolType(),
}
def get_types(gir):
return {
**get_property_types(gir),
@ -93,7 +96,19 @@ def get_types(gir):
**get_state_types(gir),
}
allow_duplicates = [
"controls",
"described-by",
"details",
"flow-to",
"labelled-by",
"owns",
]
def _get_docs(gir, name):
name = name.replace("-", "_")
if gir_type := (
gir.get_type("AccessibleProperty", "Gtk").members.get(name)
or gir.get_type("AccessibleRelation", "Gtk").members.get(name)
@ -102,11 +117,11 @@ def _get_docs(gir, name):
return gir_type.doc
class A11yProperty(BaseTypedAttribute):
class A11yProperty(AstNode):
grammar = Statement(
UseIdent("name"),
":",
VALUE_HOOKS.expected("a value"),
AnyOf(Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]),
)
@property
@ -127,8 +142,22 @@ class A11yProperty(BaseTypedAttribute):
return self.tokens["name"].replace("_", "-")
@property
def value_type(self) -> GirType:
return get_types(self.root.gir).get(self.tokens["name"])
def values(self) -> T.List[Value]:
return list(self.children)
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(get_types(self.root.gir).get(self.tokens["name"]))
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.Field,
self.range,
self.group.tokens["name"].range,
", ".join(v.range.text for v in self.values),
)
@validate("name")
def is_valid_property(self):
@ -146,19 +175,46 @@ class A11yProperty(BaseTypedAttribute):
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):
return _get_docs(self.root.gir, self.tokens["name"])
class A11y(AstNode):
class ExtAccessibility(AstNode):
grammar = [
Keyword("accessibility"),
"{",
Until(A11yProperty, "}"),
]
@property
def properties(self) -> T.List[A11yProperty]:
return self.children[A11yProperty]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"accessibility",
SymbolKind.Struct,
self.range,
self.group.tokens["accessibility"].range,
)
@validate("accessibility")
def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "accessibility properties")
@ -167,44 +223,65 @@ class A11y(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate accessibility block")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("accessibility")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@docs("accessibility")
def ref_docs(self):
return get_docs_section("Syntax ExtAccessibility")
@completer(
applies_in=[ObjectContent],
matches=new_statement_patterns,
)
def a11y_completer(ast_node, match_variables):
def a11y_completer(lsp, ast_node, match_variables):
yield Completion(
"accessibility", CompletionItemKind.Snippet,
snippet="accessibility {\n $0\n}"
"accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}"
)
@completer(
applies_in=[A11y],
applies_in=[ExtAccessibility],
matches=new_statement_patterns,
)
def a11y_name_completer(ast_node, match_variables):
def a11y_name_completer(lsp, ast_node, match_variables):
for name, type in get_types(ast_node.root.gir).items():
yield Completion(name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type))
yield Completion(
name,
CompletionItemKind.Property,
docs=_get_docs(ast_node.root.gir, type.name),
)
@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

@ -18,40 +18,64 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .attributes import BaseTypedAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue
class Item(BaseTypedAttribute):
tag_name = "item"
attr_name = "id"
class Item(AstNode):
grammar = [
Optional([UseIdent("name"), ":"]),
StringValue,
]
@property
def value_type(self):
return StringType()
def name(self) -> T.Optional[str]:
return self.tokens["name"]
@property
def value(self) -> StringValue:
return self.children[StringValue][0]
item = Group(
Item,
[
Optional([
UseIdent("name"),
":",
]),
VALUE_HOOKS,
]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.value.range.text,
SymbolKind.String,
self.range,
self.value.range,
self.name,
)
@validate("name")
def unique_in_parent(self):
if self.name is not None:
self.validate_unique_in_parent(
f"Duplicate item '{self.name}'", lambda x: x.name == self.name
)
class Items(AstNode):
@docs("name")
def ref_docs(self):
return get_docs_section("Syntax ExtComboBoxItems")
class ExtComboBoxItems(AstNode):
grammar = [
Keyword("items"),
"[",
Delimited(item, ","),
Delimited(Item, ","),
"]",
]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"items",
SymbolKind.Array,
self.range,
self.group.tokens["items"].range,
)
@validate("items")
def container_is_combo_box_text(self):
validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items")
@ -60,11 +84,9 @@ class Items(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate items block")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("items")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@docs("items")
def ref_docs(self):
return get_docs_section("Syntax ExtComboBoxItems")
@completer(
@ -72,8 +94,31 @@ class Items(AstNode):
applies_in_subclass=("Gtk", "ComboBoxText"),
matches=new_statement_patterns,
)
def items_completer(ast_node, match_variables):
yield Completion(
"items", CompletionItemKind.Snippet,
snippet="items [$0]"
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

@ -18,47 +18,63 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
from .gobject_object import ObjectContent, validate_parent_type
class Filters(AstNode):
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.tokens["tag_name"],
SymbolKind.Array,
self.range,
self.group.tokens["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)
def emit_xml(self, xml: XmlEmitter):
xml.start_tag(self.tokens["tag_name"])
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@docs("tag_name")
def ref_docs(self):
return get_docs_section("Syntax ExtFileFilter")
class FilterString(AstNode):
def emit_xml(self, xml):
xml.start_tag(self.tokens["tag_name"])
xml.put_text(self.tokens["name"])
xml.end_tag()
@property
def item(self) -> str:
return self.tokens["name"]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.item,
SymbolKind.String,
self.range,
self.group.tokens["name"].range,
)
@validate()
def unique_in_parent(self):
self.validate_unique_in_parent(
f"Duplicate {self.tokens['tag_name']} '{self.item}'",
check=lambda child: child.item == self.item,
)
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(
@ -66,18 +82,18 @@ def create_node(tag_name: str, singular: str):
[
UseQuoted("name"),
UseLiteral("tag_name", singular),
]
],
),
",",
),
"]",
]
],
)
mime_types = create_node("mime-types", "mime-type")
patterns = create_node("patterns", "pattern")
suffixes = create_node("suffixes", "suffix")
ext_file_filter_mime_types = create_node("mime-types", "mime-type")
ext_file_filter_patterns = create_node("patterns", "pattern")
ext_file_filter_suffixes = create_node("suffixes", "suffix")
@completer(
@ -85,32 +101,39 @@ suffixes = create_node("suffixes", "suffix")
applies_in_subclass=("Gtk", "FileFilter"),
matches=new_statement_patterns,
)
def file_filter_completer(ast_node, match_variables):
yield Completion("mime-types", CompletionItemKind.Snippet, snippet="mime-types [\"$0\"]")
yield Completion("patterns", CompletionItemKind.Snippet, snippet="patterns [\"$0\"]")
yield Completion("suffixes", CompletionItemKind.Snippet, snippet="suffixes [\"$0\"]")
def file_filter_completer(lsp, ast_node, match_variables):
yield Completion(
"mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]'
)
yield Completion("patterns", CompletionItemKind.Snippet, snippet='patterns ["$0"]')
yield Completion("suffixes", CompletionItemKind.Snippet, snippet='suffixes ["$0"]')
@decompiler("mime-types")
def decompile_mime_types(ctx, gir):
ctx.print("mime-types [")
@decompiler("mime-type", cdata=True)
def decompile_mime_type(ctx, gir, cdata):
ctx.print(f'"{cdata}",')
ctx.print(f"{escape_quote(cdata)},")
@decompiler("patterns")
def decompile_patterns(ctx, gir):
ctx.print("patterns [")
@decompiler("pattern", cdata=True)
def decompile_pattern(ctx, gir, cdata):
ctx.print(f'"{cdata}",')
ctx.print(f"{escape_quote(cdata)},")
@decompiler("suffixes")
def decompile_suffixes(ctx, gir):
ctx.print("suffixes [")
@decompiler("suffix", cdata=True)
def decompile_suffix(ctx, gir, cdata):
ctx.print(f'"{cdata}",')
ctx.print(f"{escape_quote(cdata)},")

View file

@ -18,18 +18,38 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .attributes import BaseAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type
from .values import Value
class LayoutProperty(BaseAttribute):
class LayoutProperty(AstNode):
grammar = Statement(UseIdent("name"), ":", Err(Value, "Expected a value"))
tag_name = "property"
@property
def value_type(self):
def name(self) -> str:
return self.tokens["name"]
@property
def value(self) -> Value:
return self.children[Value][0]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.Field,
self.range,
self.group.tokens["name"].range,
self.value.range.text,
)
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
# there isn't really a way to validate these
return None
return ValueTypeCtx(None)
@validate("name")
def unique_in_parent(self):
@ -39,21 +59,20 @@ class LayoutProperty(BaseAttribute):
)
layout_prop = Group(
LayoutProperty,
Statement(
UseIdent("name"),
":",
VALUE_HOOKS.expected("a value"),
)
)
class Layout(AstNode):
class ExtLayout(AstNode):
grammar = Sequence(
Keyword("layout"),
"{",
Until(layout_prop, "}"),
Until(LayoutProperty, "}"),
)
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"layout",
SymbolKind.Struct,
self.range,
self.group.tokens["layout"].range,
)
@validate("layout")
@ -64,11 +83,9 @@ class Layout(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate layout block")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("layout")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@docs("layout")
def ref_docs(self):
return get_docs_section("Syntax ExtLayout")
@completer(
@ -76,11 +93,8 @@ class Layout(AstNode):
applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns,
)
def layout_completer(ast_node, match_variables):
yield Completion(
"layout", CompletionItemKind.Snippet,
snippet="layout {\n $0\n}"
)
def layout_completer(lsp, ast_node, match_variables):
yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}")
@decompiler("layout")

View file

@ -0,0 +1,111 @@
import typing as T
from blueprintcompiler.errors import T
from blueprintcompiler.lsp_utils import DocumentSymbol
from ..ast_utils import AstNode, validate
from .common import *
from .contexts import ScopeCtx
from .gobject_object import ObjectContent, validate_parent_type
from .types import TypeName
class ExtListItemFactory(AstNode):
grammar = [
UseExact("id", "template"),
Mark("typename_start"),
Optional(TypeName),
Mark("typename_end"),
ObjectContent,
]
@property
def id(self) -> str:
return "template"
@property
def signature(self) -> str:
return f"template {self.gir_class.full_name}"
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.signature,
SymbolKind.Object,
self.range,
self.group.tokens["id"].range,
)
@property
def type_name(self) -> T.Optional[TypeName]:
if len(self.children[TypeName]) == 1:
return self.children[TypeName][0]
else:
return None
@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("id")
def container_is_builder_list(self):
validate_parent_type(
self,
"Gtk",
"BuilderListItemFactory",
"sub-templates",
)
@validate("id")
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate template block")
@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 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("id")
def type_name_upgrade(self):
if self.type_name is None:
raise UpgradeWarning(
"Expected type name after 'template' keyword",
actions=[
CodeAction(
"Add ListItem type to template block (introduced in blueprint 0.8.0)",
"template ListItem",
)
],
)
@context(ScopeCtx)
def scope_ctx(self) -> ScopeCtx:
return ScopeCtx(node=self)
@validate()
def unique_ids(self):
self.context[ScopeCtx].validate_unique_ids()
@property
def content(self) -> ObjectContent:
return self.children[ObjectContent][0]
@property
def action_widgets(self):
# 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

@ -17,173 +17,257 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from blueprintcompiler.language.values import StringValue
from .attributes import BaseAttribute
from .gobject_object import Object, ObjectContent
from .ui import UI
from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import RESERVED_IDS
class Menu(Object):
def emit_xml(self, xml: XmlEmitter):
xml.start_tag(self.tokens["tag"], id=self.tokens["id"])
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
class Menu(AstNode):
@property
def gir_class(self):
return self.root.gir.namespaces["Gtk"].lookup_type("Gio.Menu")
@property
def id(self) -> str:
return self.tokens["id"]
class MenuAttribute(BaseAttribute):
@property
def signature(self) -> str:
if self.id:
return f"Gio.Menu {self.id}"
else:
return "Gio.Menu"
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.tokens["tag"],
SymbolKind.Object,
self.range,
self.group.tokens[self.tokens["tag"]].range,
self.id,
)
@property
def tag(self) -> str:
return self.tokens["tag"]
@property
def items(self) -> T.List[T.Union["Menu", "MenuAttribute"]]:
return self.children
@validate("menu")
def has_id(self):
if self.tokens["tag"] == "menu" and self.tokens["id"] is None:
raise CompileError("Menu requires an ID")
@validate("id")
def object_id_not_reserved(self):
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"
@property
def value_type(self):
return None
def name(self) -> str:
return self.tokens["name"]
@property
def value(self) -> StringValue:
return self.children[StringValue][0]
menu_contents = Sequence()
menu_section = Group(
Menu,
[
"section",
UseLiteral("tag", "section"),
Optional(UseIdent("id")),
menu_contents
]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.Field,
self.range,
(
self.group.tokens["name"].range
if self.group.tokens["name"]
else self.range
),
self.value.range.text,
)
menu_submenu = Group(
Menu,
[
"submenu",
UseLiteral("tag", "submenu"),
Optional(UseIdent("id")),
menu_contents
]
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(None)
@validate("name")
def unique(self):
self.validate_unique_in_parent(
f"Duplicate attribute '{self.name}'", lambda x: x.name == self.name
)
menu_child = AnyOf()
menu_attribute = Group(
MenuAttribute,
[
UseIdent("name"),
":",
VALUE_HOOKS.expected("a value"),
Err(StringValue, "Expected string or translated string"),
Match(";").expected(),
]
],
)
menu_section = Group(
Menu,
[
Keyword("section"),
UseLiteral("tag", "section"),
Optional(UseIdent("id")),
Match("{").expected(),
Until(AnyOf(menu_child, menu_attribute), "}"),
],
)
menu_submenu = Group(
Menu,
[
Keyword("submenu"),
UseLiteral("tag", "submenu"),
Optional(UseIdent("id")),
Match("{").expected(),
Until(AnyOf(menu_child, menu_attribute), "}"),
],
)
menu_item = Group(
Menu,
[
"item",
Keyword("item"),
UseLiteral("tag", "item"),
Optional(UseIdent("id")),
Match("{").expected(),
Until(menu_attribute, "}"),
]
],
)
menu_item_shorthand = Group(
Menu,
[
"item",
Keyword("item"),
UseLiteral("tag", "item"),
UseLiteral("shorthand", True),
"(",
Group(
MenuAttribute,
[UseLiteral("name", "label"), VALUE_HOOKS],
[UseLiteral("name", "label"), StringValue],
),
Optional([
Optional(
[
",",
Optional([
Optional(
[
Group(
MenuAttribute,
[UseLiteral("name", "action"), VALUE_HOOKS],
[UseLiteral("name", "action"), StringValue],
),
Optional([
Optional(
[
",",
Group(
MenuAttribute,
[UseLiteral("name", "icon"), VALUE_HOOKS],
[UseLiteral("name", "icon"), StringValue],
),
])
])
]),
Match(")").expected(),
]
),
]
),
]
),
Match(")").expected(),
],
)
menu_contents.children = [
Match("{"),
Until(AnyOf(
menu_child.children = [
menu_section,
menu_submenu,
menu_item_shorthand,
menu_item,
menu_attribute,
), "}"),
]
menu = Group(
menu: Group = Group(
Menu,
[
"menu",
Keyword("menu"),
UseLiteral("tag", "menu"),
Optional(UseIdent("id")),
menu_contents
[
Match("{"),
Until(
AnyOf(
menu_child,
Fail(
menu_attribute,
"Attributes are not permitted at the top level of a menu",
),
),
"}",
),
],
],
)
from .ui import UI
@completer(
applies_in=[UI],
matches=new_statement_patterns,
)
def menu_completer(ast_node, match_variables):
yield Completion(
"menu", CompletionItemKind.Snippet,
snippet="menu {\n $0\n}"
)
def menu_completer(lsp, ast_node, match_variables):
yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}")
@completer(
applies_in=[Menu],
matches=new_statement_patterns,
)
def menu_content_completer(ast_node, match_variables):
def menu_content_completer(lsp, ast_node, match_variables):
yield Completion(
"submenu", CompletionItemKind.Snippet,
snippet="submenu {\n $0\n}"
"submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}"
)
yield Completion(
"section", CompletionItemKind.Snippet,
snippet="section {\n $0\n}"
"section", CompletionItemKind.Snippet, snippet="section {\n $0\n}"
)
yield Completion("item", CompletionItemKind.Snippet, snippet="item {\n $0\n}")
yield Completion(
"item", CompletionItemKind.Snippet,
snippet="item {\n $0\n}"
)
yield Completion(
"item (shorthand)", CompletionItemKind.Snippet,
snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")'
"item (shorthand)",
CompletionItemKind.Snippet,
snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")',
)
yield Completion(
"label", CompletionItemKind.Snippet,
snippet='label: $0;'
)
yield Completion(
"action", CompletionItemKind.Snippet,
snippet='action: "$0";'
)
yield Completion(
"icon", CompletionItemKind.Snippet,
snippet='icon: "$0";'
)
yield Completion("label", CompletionItemKind.Snippet, snippet="label: $0;")
yield Completion("action", CompletionItemKind.Snippet, snippet='action: "$0";')
yield Completion("icon", CompletionItemKind.Snippet, snippet='icon: "$0";')
@decompiler("menu")
@ -193,6 +277,7 @@ def decompile_menu(ctx, gir, id=None):
else:
ctx.print("menu {")
@decompiler("submenu")
def decompile_submenu(ctx, gir, id=None):
if id:
@ -200,13 +285,15 @@ def decompile_submenu(ctx, gir, id=None):
else:
ctx.print("submenu {")
@decompiler("item")
@decompiler("item", parent_tag="menu")
def decompile_item(ctx, gir, id=None):
if id:
ctx.print(f"item {id} {{")
else:
ctx.print("item {")
@decompiler("section")
def decompile_section(ctx, gir, id=None):
if id:

View file

@ -0,0 +1,187 @@
# gtk_scale.py
#
# Copyright 2023 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 *
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue
class ExtScaleMark(AstNode):
grammar = [
Keyword("mark"),
Match("(").expected(),
[
Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))),
UseNumber("value"),
Optional(
[
",",
UseIdent("position"),
Optional([",", StringValue]),
]
),
],
Match(")").expected(),
]
@property
def value(self) -> float:
if self.tokens["sign"] == "-":
return -self.tokens["value"]
else:
return self.tokens["value"]
@property
def position(self) -> T.Optional[str]:
return self.tokens["position"]
@property
def label(self) -> T.Optional[StringValue]:
if len(self.children[StringValue]) == 1:
return self.children[StringValue][0]
else:
return None
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
str(self.value),
SymbolKind.Field,
self.range,
self.group.tokens["mark"].range,
self.label.string if self.label else None,
)
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
if range := self.ranges["position"]:
yield SemanticToken(
range.start,
range.end,
SemanticTokenType.EnumMember,
)
@docs("position")
def position_docs(self) -> T.Optional[str]:
if member := self.root.gir.get_type("PositionType", "Gtk").members.get(
self.position
):
return member.doc
else:
return None
@validate("position")
def validate_position(self):
positions = self.root.gir.get_type("PositionType", "Gtk").members
if self.position is not None and positions.get(self.position) is None:
raise CompileError(
f"'{self.position}' is not a member of Gtk.PositionType",
did_you_mean=(self.position, positions.keys()),
)
@docs("mark")
def ref_docs(self):
return get_docs_section("Syntax ExtScaleMarks")
class ExtScaleMarks(AstNode):
grammar = [
Keyword("marks"),
Match("[").expected(),
Until(ExtScaleMark, "]", ","),
]
@property
def marks(self) -> T.List[ExtScaleMark]:
return self.children
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"marks",
SymbolKind.Array,
self.range,
self.group.tokens["marks"].range,
)
@validate("marks")
def container_is_size_group(self):
validate_parent_type(self, "Gtk", "Scale", "scale marks")
@validate("marks")
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],
applies_in_subclass=("Gtk", "Scale"),
matches=new_statement_patterns,
)
def complete_marks(lsp, ast_node, match_variables):
yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]")
@completer(
applies_in=[ExtScaleMarks],
)
def complete_mark(lsp, ast_node, match_variables):
yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),")
@decompiler("marks")
def decompile_marks(
ctx,
gir,
):
ctx.print("marks [")
@decompiler("mark", cdata=True)
def decompile_mark(
ctx: DecompileCtx,
gir,
value,
position=None,
cdata=None,
translatable="false",
comments=None,
context=None,
):
if comments is not None:
ctx.print(f"/* Translators: {comments} */")
text = f"mark ({value}"
if position:
text += f", {position}"
elif cdata:
text += f", bottom"
if truthy(translatable):
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
)
text += f", {translatable}"
text += "),"
ctx.print(text)

View file

@ -18,32 +18,58 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
from .contexts import ScopeCtx
from .gobject_object import ObjectContent, validate_parent_type
class Widget(AstNode):
grammar = UseIdent("name")
@property
def name(self) -> str:
return self.tokens["name"]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.Field,
self.range,
self.group.tokens["name"].range,
)
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.root.objects_by_id.get(self.tokens["name"])
object = self.context[ScopeCtx].objects.get(self.tokens["name"])
type = self.root.gir.get_type("Widget", "Gtk")
if object is None:
raise CompileError(
f"Could not find object with ID {self.tokens['name']}",
did_you_mean=(self.tokens['name'], self.root.objects_by_id.keys()),
did_you_mean=(
self.tokens["name"],
self.context[ScopeCtx].objects.keys(),
),
)
elif object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
)
def emit_xml(self, xml: XmlEmitter):
xml.put_self_closing("widget", name=self.tokens["name"])
@validate("name")
def unique_in_parent(self):
self.validate_unique_in_parent(
f"Object '{self.name}' is listed twice", lambda x: x.name == self.name
)
class Widgets(AstNode):
class ExtSizeGroupWidgets(AstNode):
grammar = [
Keyword("widgets"),
"[",
@ -51,6 +77,15 @@ class Widgets(AstNode):
"]",
]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"widgets",
SymbolKind.Array,
self.range,
self.group.tokens["widgets"].range,
)
@validate("widgets")
def container_is_size_group(self):
validate_parent_type(self, "Gtk", "SizeGroup", "size group properties")
@ -59,11 +94,9 @@ class Widgets(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate widgets block")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("widgets")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@docs("widgets")
def ref_docs(self):
return get_docs_section("Syntax ExtSizeGroupWidgets")
@completer(
@ -71,5 +104,15 @@ class Widgets(AstNode):
applies_in_subclass=("Gtk", "SizeGroup"),
matches=new_statement_patterns,
)
def size_group_completer(ast_node, match_variables):
def size_group_completer(lsp, ast_node, match_variables):
yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]")
@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

@ -18,28 +18,29 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .attributes import BaseTypedAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .values import Value, TranslatedStringValue
from .common import *
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue
class Item(AstNode):
grammar = VALUE_HOOKS
grammar = StringValue
@property
def value_type(self):
return StringType()
def child(self) -> StringValue:
return self.children[StringValue][0]
def emit_xml(self, xml: XmlEmitter):
value = self.children[Value][0]
attrs = value.attrs if isinstance(value, TranslatedStringValue) else {}
xml.start_tag("item", **attrs)
value.emit_xml(xml)
xml.end_tag()
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.child.range.text,
SymbolKind.String,
self.range,
self.range,
)
class Strings(AstNode):
class ExtStringListStrings(AstNode):
grammar = [
Keyword("strings"),
"[",
@ -47,7 +48,16 @@ class Strings(AstNode):
"]",
]
@validate("items")
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"strings",
SymbolKind.Array,
self.range,
self.group.tokens["strings"].range,
)
@validate("strings")
def container_is_string_list(self):
validate_parent_type(self, "Gtk", "StringList", "StringList items")
@ -55,11 +65,9 @@ class Strings(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate strings block")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("items")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@docs("strings")
def ref_docs(self):
return get_docs_section("Syntax ExtStringListStrings")
@completer(
@ -67,8 +75,27 @@ class Strings(AstNode):
applies_in_subclass=("Gtk", "StringList"),
matches=new_statement_patterns,
)
def strings_completer(ast_node, match_variables):
yield Completion(
"strings", CompletionItemKind.Snippet,
snippet="strings [$0]"
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

@ -18,18 +18,34 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
from .gobject_object import ObjectContent, validate_parent_type
class StyleClass(AstNode):
grammar = UseQuoted("name")
def emit_xml(self, xml):
xml.put_self_closing("class", name=self.tokens["name"])
@property
def name(self) -> str:
return self.tokens["name"]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.String,
self.range,
self.range,
)
@validate("name")
def unique_in_parent(self):
self.validate_unique_in_parent(
f"Duplicate style class '{self.name}'", lambda x: x.name == self.name
)
class Styles(AstNode):
class ExtStyles(AstNode):
grammar = [
Keyword("styles"),
"[",
@ -37,6 +53,15 @@ class Styles(AstNode):
"]",
]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"styles",
SymbolKind.Array,
self.range,
self.group.tokens["styles"].range,
)
@validate("styles")
def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "style classes")
@ -45,11 +70,9 @@ class Styles(AstNode):
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate styles block")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("style")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@docs("styles")
def ref_docs(self):
return get_docs_section("Syntax ExtStyles")
@completer(
@ -57,14 +80,15 @@ class Styles(AstNode):
applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns,
)
def style_completer(ast_node, match_variables):
yield Completion("styles", CompletionItemKind.Keyword, snippet="styles [\"$0\"]")
def style_completer(lsp, ast_node, match_variables):
yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]')
@decompiler("style")
def decompile_style(ctx, gir):
ctx.print(f"styles [")
@decompiler("class")
def decompile_style_class(ctx, gir, name):
ctx.print(f'"{name}",')

View file

@ -20,52 +20,126 @@
from functools import cached_property
from .gobject_object import Object
from .response_id import ResponseId
from .common import *
from .gobject_object import Object
from .response_id import ExtResponse, decompile_response_type
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [
("Gtk", "Buildable"),
("Gio", "ListStore"),
]
class ChildInternal(AstNode):
grammar = ["internal-child", UseIdent("internal_child")]
@property
def internal_child(self) -> str:
return self.tokens["internal_child"]
class ChildType(AstNode):
grammar = UseIdent("child_type").expected("a child type")
@property
def child_type(self) -> str:
return self.tokens["child_type"]
class ChildExtension(AstNode):
grammar = ExtResponse
@property
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), "]"]
@property
def child(self) -> T.Union[ChildInternal, ChildExtension, ChildType]:
return self.children[0]
class Child(AstNode):
grammar = [
Optional([
"[",
Optional(["internal-child", UseLiteral("internal_child", True)]),
UseIdent("child_type").expected("a child type"),
Optional(ResponseId),
"]",
]),
Optional(ChildAnnotation),
Object,
]
@property
def annotation(self) -> T.Optional[ChildAnnotation]:
annotations = self.children[ChildAnnotation]
return annotations[0] if len(annotations) else None
@property
def object(self) -> Object:
return self.children[Object][0]
@validate()
def parent_can_have_child(self):
if gir_class := self.parent.gir_class:
for namespace, name in ALLOWED_PARENTS:
parent_type = self.root.gir.get_type(name, namespace)
if gir_class.assignable_to(parent_type):
break
else:
hints = [
"only Gio.ListStore or Gtk.Buildable implementors can have children"
]
if hasattr(gir_class, "properties") and "child" in gir_class.properties:
hints.append(
"did you mean to assign this object to the 'child' property?"
)
raise CompileError(
f"{gir_class.full_name} doesn't have children",
hints=hints,
)
@cached_property
def response_id(self) -> T.Optional[ResponseId]:
def response_id(self) -> T.Optional[ExtResponse]:
"""Get action widget's response ID.
If child is not action widget, returns `None`.
"""
response_ids = self.children[ResponseId]
if response_ids:
return response_ids[0]
if (
self.annotation is not None
and isinstance(self.annotation.child, ChildExtension)
and isinstance(self.annotation.child.child, ExtResponse)
):
return self.annotation.child.child
else:
return None
def emit_xml(self, xml: XmlEmitter):
child_type = internal_child = None
if self.tokens["internal_child"]:
internal_child = self.tokens["child_type"]
else:
child_type = self.tokens["child_type"]
xml.start_tag("child", type=child_type, internal_child=internal_child)
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@validate()
def internal_child_unique(self):
if self.annotation is not None:
if isinstance(self.annotation.child, ChildInternal):
internal_child = self.annotation.child.internal_child
self.validate_unique_in_parent(
f"Duplicate internal child '{internal_child}'",
lambda x: (
x.annotation
and isinstance(x.annotation.child, ChildInternal)
and x.annotation.child.internal_child == internal_child
),
)
@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

@ -17,46 +17,93 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from .gobject_object import Object, ObjectContent
from blueprintcompiler.language.common import GirType
from ..gir import TemplateType
from .common import *
from .gobject_object import Object, ObjectContent
from .types import ClassName, TemplateClassName
class Template(Object):
grammar = [
"template",
UseIdent("name").expected("template class name"),
Optional([
UseExact("id", "template"),
to_parse_node(TemplateClassName).expected("template type"),
Optional(
[
Match(":"),
class_name.expected("parent class"),
]),
to_parse_node(ClassName).expected("parent class"),
]
),
ObjectContent,
]
@validate()
def not_abstract(self):
pass # does not apply to templates
@property
def id(self) -> str:
return "template"
@validate("name")
def unique_in_parent(self):
self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",)
@property
def signature(self) -> str:
if self.parent_type and self.parent_type.gir_type:
return f"template {self.class_name.as_string} : {self.parent_type.gir_type.full_name}"
else:
return f"template {self.class_name.as_string}"
def emit_xml(self, xml: XmlEmitter):
xml.start_tag(
"template",
**{"class": self.tokens["name"]},
parent=self.gir_class or self.tokens["class_name"]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.signature,
SymbolKind.Object,
self.range,
self.group.tokens["id"].range,
)
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@property
def gir_class(self) -> GirType:
if isinstance(self.class_name.gir_type, ExternType):
if gir := self.parent_type:
return TemplateType(self.class_name.gir_type.full_name, gir.gir_type)
return self.class_name.gir_type
@property
def parent_type(self) -> T.Optional[ClassName]:
if len(self.children[ClassName]) == 2:
return self.children[ClassName][1]
else:
return None
@validate()
def parent_only_if_extern(self):
if not isinstance(self.class_name.gir_type, ExternType):
if self.parent_type is not None:
raise CompileError(
"Parent type may only be specified if the template type is extern"
)
@validate("id")
def unique_in_parent(self):
self.validate_unique_in_parent(
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="Widget"):
gir_class = ctx.type_by_cname(parent)
if gir_class is None:
ctx.print(f"template {klass} : .{parent} {{")
def decompile_template(ctx: DecompileCtx, gir, klass, parent=None):
def class_name(cname: str) -> str:
if gir := ctx.type_by_cname(cname):
return decompile.full_name(gir)
else:
ctx.print(f"template {klass} : {decompile.full_name(gir_class)} {{")
return gir_class
return "$" + cname
if parent is None:
ctx.print(f"template {class_name(klass)} {{")
else:
ctx.print(f"template {class_name(klass)} : {class_name(parent)} {{")
return ctx.type_by_cname(klass) or ctx.type_by_cname(parent)

View file

@ -24,37 +24,47 @@ from .common import *
class GtkDirective(AstNode):
grammar = Statement(
Match("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
Match("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
Match("using").err(
'File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`)'
),
Match("Gtk").err(
'File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`)'
),
UseNumberText("version").expected("a version number for GTK"),
)
@validate("version")
def gtk_version(self):
if self.tokens["version"] not in ["4.0"]:
version = self.tokens["version"]
if version not in ["4.0"]:
err = CompileError("Only GTK 4 is supported")
if self.tokens["version"].startswith("4"):
err.hint("Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'.")
if version and version.startswith("4"):
err.hint(
"Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'."
)
else:
err.hint("Expected 'using Gtk 4.0;'")
raise err
try:
gir.get_namespace("Gtk", self.tokens["version"])
except:
raise CompileError("Could not find GTK 4 introspection files. Is gobject-introspection installed?", fatal=True)
gir.get_namespace("Gtk", version)
except CompileError as e:
raise CompileError(
"Could not find GTK 4 introspection files. Is gobject-introspection installed?",
fatal=True,
# preserve the hints from the original error, because it contains
# useful debugging information
hints=e.hints,
)
@property
def gir_namespace(self):
# validate the GTK version first to make sure the more specific error
# message is emitted
self.gtk_version()
return gir.get_namespace("Gtk", self.tokens["version"])
# For better error handling, just assume it's 4.0
return gir.get_namespace("Gtk", "4.0")
def emit_xml(self, xml: XmlEmitter):
xml.put_self_closing("requires", lib="gtk", version=self.tokens["version"])
@docs()
def ref_docs(self):
return get_docs_section("Syntax GtkDecl")
class Import(AstNode):
@ -64,16 +74,36 @@ class Import(AstNode):
UseNumberText("version").expected("a version number"),
)
@property
def namespace(self):
return self.tokens["namespace"]
@property
def version(self):
return self.tokens["version"]
@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
def emit_xml(self, xml):
pass
@docs()
def ref_docs(self):
return get_docs_section("Syntax Using")

View file

@ -23,33 +23,25 @@ import typing as T
from .common import *
class ResponseId(AstNode):
class ExtResponse(AstNode):
"""Response ID of action widget."""
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [
("Gtk", "Dialog"),
("Gtk", "InfoBar")
]
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")]
grammar = [
Keyword("action"),
Keyword("response"),
"=",
AnyOf(
UseIdent("response_id"),
UseNumber("response_id")
[
Optional(UseExact("sign", "-")),
UseNumber("response_id"),
],
),
Optional([
Keyword("default"), UseLiteral("is_default", True)
])
Optional([Keyword("default"), UseLiteral("is_default", True)]),
]
@validate()
def child_type_is_action(self) -> None:
"""Check that child type is "action"."""
child_type = self.parent.tokens["child_type"]
if child_type != "action":
raise CompileError(f"Only action widget can have response ID")
@validate()
def parent_has_action_widgets(self) -> None:
"""Chech that parent widget has allowed type."""
@ -61,7 +53,7 @@ class ResponseId(AstNode):
gir = self.root.gir
for namespace, name in ResponseId.ALLOWED_PARENTS:
for namespace, name in ExtResponse.ALLOWED_PARENTS:
parent_type = gir.get_type(name, namespace)
if container_type.assignable_to(parent_type):
break
@ -73,10 +65,10 @@ class ResponseId(AstNode):
@validate()
def widget_have_id(self) -> None:
"""Check that action widget have ID."""
from .gobject_object import Object
from .gtkbuilder_child import Child
_object = self.parent.children[Object][0]
if _object.tokens["id"] is None:
object = self.parent_by_type(Child).object
if object.id is None:
raise CompileError(f"Action widget must have ID")
@validate("response_id")
@ -89,28 +81,24 @@ class ResponseId(AstNode):
gir = self.root.gir
response = self.tokens["response_id"]
if isinstance(response, int):
if response < 0:
if self.tokens["sign"] == "-":
raise CompileError("Numeric response type can't be negative")
if isinstance(response, float):
raise CompileError(
"Numeric response type can't be negative")
elif isinstance(response, float):
raise CompileError(
"Response type must be GtkResponseType member or integer,"
" not float"
"Response type must be GtkResponseType member or integer," " not float"
)
else:
elif not isinstance(response, int):
responses = gir.get_type("ResponseType", "Gtk").members.keys()
if response not in responses:
raise CompileError(
f"Response type \"{response}\" doesn't exist")
raise CompileError(f'Response type "{response}" doesn\'t exist')
@validate("default")
def no_multiple_default(self) -> None:
"""Only one action widget in dialog can be default."""
from .gtkbuilder_child import Child
from .gobject_object import Object
if not self.tokens["is_default"]:
if not self.is_default:
return
action_widgets = self.parent_by_type(Object).action_widgets
@ -120,33 +108,57 @@ class ResponseId(AstNode):
if widget.tokens["is_default"]:
raise CompileError("Default response is already set")
@property
def response_id(self) -> str:
return self.tokens["response_id"]
@property
def is_default(self) -> bool:
return self.tokens["is_default"] or False
@property
def widget_id(self) -> str:
"""Get action widget ID."""
from .gobject_object import Object
from .gtkbuilder_child import Child
_object: Object = self.parent.children[Object][0]
return _object.tokens["id"]
object = self.parent_by_type(Child).object
return object.id
def emit_xml(self, xml: XmlEmitter) -> None:
"""Emit nothing.
@docs()
def ref_docs(self):
return get_docs_section("Syntax ExtResponse")
Response ID don't have to emit any XML in place,
but have to emit action-widget tag in separate
place (see `ResponseId.emit_action_widget`)
"""
@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 emit_action_widget(self, xml: XmlEmitter) -> None:
"""Emit action-widget XML.
Must be called while <action-widgets> tag is open.
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
For more details see `GtkDialog` and `GtkInfoBar` docs.
"""
xml.start_tag(
"action-widget",
response=self.tokens["response_id"],
default=self.tokens["is_default"]
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 ""
)
xml.put_text(self.widget_id)
xml.end_tag()
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

@ -1,6 +1,6 @@
# parser_utils.py
# translation_domain.py
#
# Copyright 2021 James Westman <james@jwestman.net>
# 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
@ -17,20 +17,19 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .parse_tree import *
from .common import *
class_name = AnyOf(
[
UseIdent("namespace"),
".",
UseIdent("class_name"),
],
[
".",
UseIdent("class_name"),
UseLiteral("ignore_gir", True),
],
UseIdent("class_name"),
class TranslationDomain(AstNode):
grammar = Statement(
"translation-domain",
UseQuoted("domain"),
)
@property
def domain(self):
return self.tokens["domain"]
@docs()
def ref_docs(self):
return get_docs_section("Syntax TranslationDomain")

View file

@ -0,0 +1,184 @@
# types.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 ..gir import Class, ExternType, Interface
from .common import *
class TypeName(AstNode):
grammar = AnyOf(
[
UseIdent("namespace"),
".",
UseIdent("class_name"),
],
[
AnyOf("$", [".", UseLiteral("old_extern", True)]),
UseIdent("class_name"),
UseLiteral("extern", True),
],
UseIdent("class_name"),
)
@validate()
def old_extern(self):
if self.tokens["old_extern"]:
raise UpgradeWarning(
"Use the '$' extern syntax introduced in blueprint 0.8.0",
actions=[CodeAction("Use '$' syntax", "$" + self.tokens["class_name"])],
)
@validate("class_name")
def type_exists(self):
if not self.tokens["extern"] and self.gir_ns is not None:
self.root.gir.validate_type(
self.tokens["class_name"], self.tokens["namespace"]
)
@validate("namespace")
def gir_ns_exists(self):
if not self.tokens["extern"]:
try:
self.root.gir.validate_ns(self.tokens["namespace"])
except CompileError as e:
ns = self.tokens["namespace"]
e.actions = [
self.root.import_code_action(n, version)
for n, version in gir.get_available_namespaces()
if n == ns
]
raise e
@validate()
def deprecated(self) -> None:
if self.gir_type and self.gir_type.deprecated:
hints = []
if self.gir_type.deprecated_doc:
hints.append(self.gir_type.deprecated_doc)
raise DeprecatedWarning(
f"{self.gir_type.full_name} is deprecated",
hints=hints,
)
@property
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:
if self.tokens["class_name"] and not self.tokens["extern"]:
return self.root.gir.get_type(
self.tokens["class_name"], self.tokens["namespace"]
)
return gir.ExternType(self.tokens["class_name"])
@property
def glib_type_name(self) -> str:
if gir_type := self.gir_type:
return gir_type.glib_type_name
else:
return self.tokens["class_name"]
@docs("namespace")
def namespace_docs(self):
if ns := self.root.gir.namespaces.get(self.tokens["namespace"]):
return ns.doc
@docs("class_name")
def class_docs(self):
if self.gir_type:
return self.gir_type.doc
@property
def as_string(self) -> str:
if self.tokens["extern"]:
return "$" + self.tokens["class_name"]
elif self.tokens["namespace"]:
return f"{self.tokens['namespace']}.{self.tokens['class_name']}"
else:
return self.tokens["class_name"]
class ClassName(TypeName):
@validate("namespace", "class_name")
def gir_class_exists(self):
if (
self.gir_type is not None
and not isinstance(self.gir_type, ExternType)
and not isinstance(self.gir_type, Class)
):
if isinstance(self.gir_type, Interface):
raise CompileError(
f"{self.gir_type.full_name} is an interface, not a class"
)
else:
raise CompileError(f"{self.gir_type.full_name} is not a class")
class ConcreteClassName(ClassName):
@validate("namespace", "class_name")
def not_abstract(self):
if isinstance(self.gir_type, Class) and self.gir_type.abstract:
raise CompileError(
f"{self.gir_type.full_name} can't be instantiated because it's abstract",
hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"],
)
class TemplateClassName(ClassName):
"""Handles the special case of a template type. The old syntax uses an identifier,
which is ambiguous with the new syntax. So this class displays an appropriate
upgrade warning instead of a class not found error."""
@property
def is_legacy(self):
return (
self.tokens["extern"] is None
and self.tokens["namespace"] is None
and self.root.gir.get_type(self.tokens["class_name"], "Gtk") is None
)
@property
def gir_type(self) -> gir.GirType:
if self.is_legacy:
return gir.ExternType(self.tokens["class_name"])
else:
return super().gir_type
@validate("class_name")
def type_exists(self):
if self.is_legacy:
if type := self.root.gir.get_type_by_cname(self.tokens["class_name"]):
replacement = type.full_name
else:
replacement = "$" + self.tokens["class_name"]
raise UpgradeWarning(
"Use type syntax here (introduced in blueprint 0.8.0)",
actions=[CodeAction("Use type syntax", replace_with=replacement)],
)
if not self.tokens["extern"] and self.gir_ns is not None:
self.root.gir.validate_type(
self.tokens["class_name"], self.tokens["namespace"]
)

View file

@ -17,11 +17,17 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from functools import cached_property
from .. import gir
from .imports import GtkDirective, Import
from .gtkbuilder_template import Template
from .common import *
from .contexts import ScopeCtx
from .gobject_object import Object
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):
@ -30,19 +36,25 @@ class UI(AstNode):
grammar = [
GtkDirective,
ZeroOrMore(Import),
Until(AnyOf(
Optional(TranslationDomain),
Until(
AnyOf(
Template,
OBJECT_HOOKS,
), Eof()),
menu,
Object,
),
Eof(),
),
]
@property
def gir(self):
@cached_property
def gir(self) -> gir.GirContext:
gir_ctx = gir.GirContext()
self._gir_errors = []
try:
gir_ctx.add_namespace(self.children[GtkDirective][0].gir_namespace)
if gtk := self.children[GtkDirective][0].gir_namespace:
gir_ctx.add_namespace(gtk)
except CompileError as e:
self._gir_errors.append(e)
@ -50,18 +62,85 @@ class UI(AstNode):
try:
if i.gir_namespace is not None:
gir_ctx.add_namespace(i.gir_namespace)
else:
gir_ctx.not_found_namespaces.add(i.namespace)
except CompileError as e:
e.start = i.group.tokens["namespace"].start
e.end = i.group.tokens["version"].end
e.range = i.range
self._gir_errors.append(e)
return gir_ctx
@property
def using(self) -> T.List[Import]:
return self.children[Import]
@property
def objects_by_id(self):
return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None }
def gtk_decl(self) -> GtkDirective:
return self.children[GtkDirective][0]
@property
def translation_domain(self) -> T.Optional[TranslationDomain]:
domains = self.children[TranslationDomain]
if len(domains):
return domains[0]
else:
return None
@property
def contents(self) -> T.List[T.Union[Object, Template, Menu]]:
return [
child
for child in self.children
if isinstance(child, Object)
or isinstance(child, Template)
or isinstance(child, Menu)
]
@property
def template(self) -> T.Optional[Template]:
if len(self.children[Template]):
return self.children[Template][0]
else:
return None
def is_legacy_template(self, id: str) -> bool:
return (
id not in self.context[ScopeCtx].objects
and self.template is not None
and self.template.class_name.glib_type_name == id
)
def import_code_action(self, ns: str, version: str) -> CodeAction:
if len(self.children[Import]):
pos = self.children[Import][-1].range.end
else:
pos = self.children[GtkDirective][0].range.end
return CodeAction(
f"Import {ns} {version}",
f"\nusing {ns} {version};",
Range(pos, pos, self.group.text),
)
@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)
@validate()
def gir_errors(self):
@ -70,22 +149,6 @@ class UI(AstNode):
if len(self._gir_errors):
raise MultipleErrors(self._gir_errors)
@validate()
def unique_ids(self):
passed = {}
for obj in self.iterate_children_recursive():
if obj.tokens["id"] is None:
continue
if obj.tokens["id"] in passed:
token = obj.group.tokens["id"]
raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end)
passed[obj.tokens["id"]] = obj
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("interface")
for x in self.children:
x.emit_xml(xml)
xml.end_tag()
self.context[ScopeCtx].validate_unique_ids()

View file

@ -17,101 +17,233 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from blueprintcompiler.gir import ArrayType
from blueprintcompiler.lsp_utils import SemanticToken
from .common import *
from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx
from .expression import Expression
from .gobject_object import Object
from .types import TypeName
class Value(AstNode):
pass
class TranslatedStringValue(Value):
class Translated(AstNode):
grammar = AnyOf(
[
"_",
"(",
UseQuoted("value").expected("a quoted string"),
Match(")").expected(),
],
["_", "(", UseQuoted("string"), ")"],
[
"C_",
"(",
UseQuoted("context").expected("a quoted string"),
UseQuoted("context"),
",",
UseQuoted("value").expected("a quoted string"),
Optional(","),
Match(")").expected(),
UseQuoted("string"),
")",
],
)
@property
def attrs(self):
attrs = { "translatable": "true" }
if "context" in self.tokens:
attrs["context"] = self.tokens["context"]
return attrs
def string(self) -> str:
return self.tokens["string"]
def emit_xml(self, xml: XmlEmitter):
xml.put_text(self.tokens["value"])
class LiteralValue(Value):
grammar = AnyOf(
UseNumber("value"),
UseQuoted("value"),
)
def emit_xml(self, xml: XmlEmitter):
xml.put_text(self.tokens["value"])
@property
def translate_context(self) -> T.Optional[str]:
return self.tokens["context"]
@validate()
def validate_for_type(self):
type = self.parent.value_type
if isinstance(type, gir.IntType):
try:
int(self.tokens["value"])
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer")
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
if expected_type is not None and not expected_type.assignable_to(StringType()):
raise CompileError(
f"Cannot convert translated string to {expected_type.full_name}"
)
elif isinstance(type, gir.UIntType):
try:
int(self.tokens["value"])
if int(self.tokens["value"]) < 0:
raise Exception()
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer")
@validate("context")
def context_double_quoted(self):
if self.translate_context is None:
return
elif isinstance(type, gir.FloatType):
try:
float(self.tokens["value"])
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to float")
if not str(self.group.tokens["context"]).startswith('"'):
raise CompileWarning("gettext may not recognize single-quoted strings")
elif isinstance(type, gir.StringType):
@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 = [
"typeof",
AnyOf(
[
"<",
to_parse_node(TypeName).expected("type name"),
Match(">").expected(),
],
[
UseExact("lparen", "("),
to_parse_node(TypeName).expected("type name"),
UseExact("rparen", ")").expected("')'"),
],
),
]
@property
def type(self):
return gir.TypeType()
@property
def type_name(self) -> TypeName:
return self.children[TypeName][0]
@validate()
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
if expected_type is not None and not isinstance(expected_type, gir.TypeType):
raise CompileError(f"Cannot convert GType to {expected_type.full_name}")
@validate("lparen", "rparen")
def upgrade_to_angle_brackets(self):
if self.tokens["lparen"]:
raise UpgradeWarning(
"Use angle bracket syntax introduced in blueprint 0.8.0",
actions=[
CodeAction(
"Use <> instead of ()",
f"<{self.children[TypeName][0].as_string}>",
)
],
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax TypeLiteral")
class QuotedLiteral(AstNode):
grammar = UseQuoted("value")
@property
def value(self) -> str:
return self.tokens["value"]
@property
def type(self):
return gir.StringType()
@validate()
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
if (
isinstance(expected_type, gir.IntType)
or isinstance(expected_type, gir.UIntType)
or isinstance(expected_type, gir.FloatType)
):
raise CompileError(f"Cannot convert string to number")
elif isinstance(expected_type, gir.StringType):
pass
elif isinstance(type, gir.Class) or isinstance(type, gir.Interface):
elif (
isinstance(expected_type, gir.Class)
or isinstance(expected_type, gir.Interface)
or isinstance(expected_type, gir.Boxed)
):
parseable_types = [
"Gdk.Paintable",
"Gdk.Texture",
"Gdk.Pixbuf",
"GLib.File",
"Gio.File",
"Gtk.ShortcutTrigger",
"Gtk.ShortcutAction",
"Gdk.RGBA",
"Gdk.ContentFormats",
"Gsk.Transform",
"GLib.Variant",
]
if type.full_name not in parseable_types:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}")
if expected_type.full_name not in parseable_types:
hints = []
if isinstance(expected_type, gir.TypeType):
hints.append(f"use the typeof operator: 'typeof({self.value})'")
raise CompileError(
f"Cannot convert string to {expected_type.full_name}", hints=hints
)
elif type is not None:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}")
elif expected_type is not None:
raise CompileError(f"Cannot convert string to {expected_type.full_name}")
class NumberLiteral(AstNode):
grammar = [
Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))),
UseNumber("value"),
]
@property
def type(self) -> gir.GirType:
if isinstance(self.value, int):
return gir.IntType()
else:
return gir.FloatType()
@property
def value(self) -> T.Union[int, float]:
if self.tokens["sign"] == "-":
return -self.tokens["value"]
else:
return self.tokens["value"]
@validate()
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
if isinstance(expected_type, gir.IntType):
if not isinstance(self.value, int):
raise CompileError(
f"Cannot convert {self.group.tokens['value']} to integer"
)
elif isinstance(expected_type, gir.UIntType):
if self.value < 0:
raise CompileError(
f"Cannot convert -{self.group.tokens['value']} to unsigned integer"
)
elif not isinstance(expected_type, gir.FloatType) and expected_type is not None:
raise CompileError(f"Cannot convert number to {expected_type.full_name}")
class Flag(AstNode):
grammar = UseIdent("value")
@property
def name(self) -> str:
return self.tokens["value"]
@property
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.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.parent.parent.value_type
type = self.context[ValueTypeCtx].value_type
if not isinstance(type, Enumeration):
return
if member := type.members.get(self.tokens["value"]):
@ -119,81 +251,298 @@ class Flag(AstNode):
@validate()
def validate_for_type(self):
type = self.parent.parent.value_type
if isinstance(type, gir.Bitfield) and self.tokens["value"] not in type.members:
expected_type = self.context[ValueTypeCtx].value_type
if (
isinstance(expected_type, gir.Bitfield)
and self.tokens["value"] not in expected_type.members
):
raise CompileError(
f"{self.tokens['value']} is not a member of {type.full_name}",
did_you_mean=(self.tokens['value'], type.members.keys()),
f"{self.tokens['value']} is not a member of {expected_type.full_name}",
did_you_mean=(self.tokens["value"], expected_type.members.keys()),
)
class FlagsValue(Value):
grammar = [Flag, "|", Delimited(Flag, "|")]
@validate()
def parent_is_bitfield(self):
type = self.parent.value_type
if type is not None and not isinstance(type, gir.Bitfield):
raise CompileError(f"{type.full_name} is not a bitfield type")
def emit_xml(self, xml: XmlEmitter):
xml.put_text("|".join([flag.tokens["value"] for flag in self.children[Flag]]))
def unique(self):
self.validate_unique_in_parent(
f"Duplicate flag '{self.name}'", lambda x: x.name == self.name
)
class IdentValue(Value):
grammar = UseIdent("value")
class Flags(AstNode):
grammar = [Flag, "|", Flag, ZeroOrMore(["|", Flag])]
def emit_xml(self, xml: XmlEmitter):
if isinstance(self.parent.value_type, gir.Enumeration):
xml.put_text(self.parent.value_type.members[self.tokens["value"]].nick)
else:
xml.put_text(self.tokens["value"])
@property
def flags(self) -> T.List[Flag]:
return self.children
@validate()
def validate_for_type(self):
type = self.parent.value_type
if isinstance(type, gir.Enumeration) or isinstance(type, gir.Bitfield):
if self.tokens["value"] not in type.members:
raise CompileError(
f"{self.tokens['value']} is not a member of {type.full_name}",
did_you_mean=(self.tokens['value'], type.members.keys()),
)
elif isinstance(type, gir.BoolType):
if self.tokens["value"] not in ["true", "false"]:
raise CompileError(
f"Expected 'true' or 'false' for boolean value",
did_you_mean=(self.tokens['value'], ["true", "false"]),
)
elif type is not None:
object = self.root.objects_by_id.get(self.tokens["value"])
if object is None:
raise CompileError(
f"Could not find object with ID {self.tokens['value']}",
did_you_mean=(self.tokens['value'], self.root.objects_by_id.keys()),
)
elif object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
)
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
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 docs(self):
type = self.parent.value_type
if isinstance(type, gir.Enumeration) or isinstance(type, gir.Bitfield):
if member := type.members.get(self.tokens["value"]):
def ref_docs(self):
return get_docs_section("Syntax Flags")
class IdentLiteral(AstNode):
grammar = UseIdent("value")
@property
def ident(self) -> str:
return self.tokens["value"]
@property
def type(self) -> T.Optional[gir.GirType]:
# If the expected type is known, then use that. Otherwise, guess.
if expected_type := self.context[ValueTypeCtx].value_type:
return expected_type
elif self.ident in ["true", "false"]:
return gir.BoolType()
elif object := self.context[ScopeCtx].objects.get(self.ident):
return object.gir_class
elif self.root.is_legacy_template(self.ident):
return self.root.template.class_name.gir_type
else:
return None
@validate()
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
if isinstance(expected_type, gir.BoolType):
if self.ident not in ["true", "false"]:
raise CompileError(f"Expected 'true' or 'false' for boolean value")
elif isinstance(expected_type, gir.Enumeration):
if self.ident not in expected_type.members:
raise CompileError(
f"{self.ident} is not a member of {expected_type.full_name}",
did_you_mean=(self.ident, list(expected_type.members.keys())),
)
elif self.root.is_legacy_template(self.ident):
raise UpgradeWarning(
"Use 'template' instead of the class name (introduced in 0.8.0)",
actions=[CodeAction("Use 'template'", "template")],
)
elif expected_type is not None or self.context[ValueTypeCtx].must_infer_type:
object = self.context[ScopeCtx].objects.get(self.ident)
if object is None:
if self.ident == "null":
if not self.context[ValueTypeCtx].allow_null:
raise CompileError("null is not permitted here")
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=(
self.ident,
self.context[ScopeCtx].objects.keys(),
),
)
elif (
expected_type is not None
and object.gir_class is not None
and not object.gir_class.assignable_to(expected_type)
):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {expected_type.full_name}"
)
@docs()
def docs(self) -> T.Optional[str]:
expected_type = self.context[ValueTypeCtx].value_type
if isinstance(expected_type, gir.BoolType):
return None
elif isinstance(expected_type, gir.Enumeration):
if member := expected_type.members.get(self.ident):
return member.doc
else:
return type.doc
elif isinstance(type, gir.GirNode):
return type.doc
return expected_type.doc
elif self.ident == "null" and self.context[ValueTypeCtx].allow_null:
return None
elif object := self.context[ScopeCtx].objects.get(self.ident):
return f"```\n{object.signature}\n```"
elif self.root.is_legacy_template(self.ident):
return f"```\n{self.root.template.signature}\n```"
else:
return None
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
if isinstance(self.parent.value_type, gir.Enumeration):
type = self.context[ValueTypeCtx].value_type
if isinstance(type, gir.Enumeration):
token = self.group.tokens["value"]
yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember)
def get_reference(self, _idx: int) -> T.Optional[LocationLink]:
ref = self.context[ScopeCtx].objects.get(self.ident)
if ref is None and self.root.is_legacy_template(self.ident):
ref = self.root.template
if ref:
return LocationLink(self.range, ref.range, ref.ranges["id"])
else:
return None
class Literal(AstNode):
grammar = AnyOf(
TypeLiteral,
QuotedLiteral,
NumberLiteral,
IdentLiteral,
)
@property
def value(
self,
) -> T.Union[TypeLiteral, QuotedLiteral, NumberLiteral, IdentLiteral]:
return self.children[0]
class ObjectValue(AstNode):
grammar = Object
@property
def object(self) -> Object:
return self.children[Object][0]
@validate()
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
if (
expected_type is not None
and self.object.gir_class is not None
and not self.object.gir_class.assignable_to(expected_type)
):
raise CompileError(
f"Cannot assign {self.object.gir_class.full_name} to {expected_type.full_name}"
)
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)
@property
def child(
self,
) -> T.Union[Translated, Flags, Literal]:
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)
@property
def child(
self,
) -> T.Union[Translated, QuotedLiteral]:
return self.children[0]
@property
def string(self) -> str:
if isinstance(self.child, Translated):
return self.child.string
else:
return self.child.value
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(StringType())

View file

@ -18,55 +18,78 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
import json
import sys
import traceback
import typing as T
import json, sys, traceback
from difflib import SequenceMatcher
from . import decompiler, formatter, parser, tokenizer, utils, xml_reader
from .ast_utils import AstNode
from .completions import complete
from .errors import PrintableError, CompileError, MultipleErrors
from .errors import CompileError, MultipleErrors
from .lsp_utils import *
from . import tokenizer, parser, utils, xml_reader
from .outputs.xml import XmlOutput
from .tokenizer import Token
def command(json_method):
def printerr(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def command(json_method: str):
def decorator(func):
func._json_method = json_method
return func
return decorator
class OpenFile:
def __init__(self, uri, text, version):
def __init__(self, uri: str, text: str, version: int) -> None:
self.uri = uri
self.text = text
self.version = version
self.ast = None
self.tokens = None
self.ast: T.Optional[AstNode] = None
self.tokens: T.Optional[list[Token]] = None
self._update()
def apply_changes(self, changes):
def apply_changes(self, changes) -> None:
for change in changes:
start = utils.pos_to_idx(change["range"]["start"]["line"], change["range"]["start"]["character"], self.text)
end = utils.pos_to_idx(change["range"]["end"]["line"], change["range"]["end"]["character"], self.text)
if "range" not in change:
self.text = change["text"]
continue
start = utils.pos_to_idx(
change["range"]["start"]["line"],
change["range"]["start"]["character"],
self.text,
)
end = utils.pos_to_idx(
change["range"]["end"]["line"],
change["range"]["end"]["character"],
self.text,
)
self.text = self.text[:start] + change["text"] + self.text[end:]
self._update()
def _update(self):
self.diagnostics = []
def _update(self) -> None:
self.diagnostics: list[CompileError] = []
try:
self.tokens = tokenizer.tokenize(self.text)
self.ast, errors, warnings = parser.parse(self.tokens)
self.diagnostics += warnings
if errors is not None:
self.diagnostics += errors.errors
self.diagnostics += self.ast.errors
except MultipleErrors as e:
self.diagnostics += e.errors
except CompileError as e:
self.diagnostics.append(e)
def calc_semantic_tokens(self) -> T.List[int]:
if self.ast is None:
return []
tokens = list(self.ast.get_semantic_tokens())
token_lists = [
[
@ -74,13 +97,15 @@ class OpenFile:
token.end - token.start, # length
token.type,
0, # token modifiers
] for token in tokens]
]
for token in tokens
]
# 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]
@ -89,10 +114,11 @@ class OpenFile:
class LanguageServer:
commands: T.Dict[str, T.Callable] = {}
def __init__(self, logfile=None):
def __init__(self):
self.client_capabilities = {}
self._open_files: {str: OpenFile} = {}
self.logfile = logfile
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
@ -100,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"):
@ -110,7 +136,7 @@ class LanguageServer:
if line.startswith("Content-Length:"):
content_len = int(line.split("Content-Length:")[1].strip())
line = sys.stdin.buffer.read(content_len).decode()
self._log("input: " + line)
printerr("input: " + line)
data = json.loads(line)
method = data.get("method")
@ -120,41 +146,56 @@ class LanguageServer:
if method in self.commands:
self.commands[method](self, id, params)
except Exception as e:
self._log(traceback.format_exc())
printerr(traceback.format_exc())
def _send(self, data):
data["jsonrpc"] = "2.0"
line = json.dumps(data, separators=(",", ":")) + "\r\n"
self._log("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}")
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}"
)
sys.stdout.flush()
def _log(self, msg):
if self.logfile is not None:
self.logfile.write(str(msg))
self.logfile.write("\n")
self.logfile.flush()
def _send_error(self, id, code, message, data=None):
self._send(
{
"id": id,
"error": {
"code": code,
"message": message,
"data": data,
},
}
)
def _send_response(self, id, result):
self._send({
self._send(
{
"id": id,
"result": result,
})
}
)
def _send_notification(self, method, params):
self._send({
self._send(
{
"method": method,
"params": params,
})
}
)
@command("initialize")
def initialize(self, id, params):
from . import main
self.client_capabilities = params.get("capabilities")
self._send_response(id, {
self.client_capabilities = params.get("capabilities", {})
self.client_supports_completion_choice = params.get("clientInfo", {}).get(
"name"
) in ["Visual Studio Code", "VSCodium"]
self._send_response(
id,
{
"capabilities": {
"textDocumentSync": {
"openClose": True,
@ -163,18 +204,31 @@ class LanguageServer:
"semanticTokensProvider": {
"legend": {
"tokenTypes": ["enumMember"],
"tokenModifiers": [],
},
"full": True,
},
"completionProvider": {},
"codeActionProvider": {},
"hoverProvider": True,
"documentSymbolProvider": True,
"definitionProvider": True,
"documentFormattingProvider": True,
},
"serverInfo": {
"name": "Blueprint",
"version": main.VERSION,
},
})
},
)
@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):
@ -201,14 +255,23 @@ class LanguageServer:
@command("textDocument/hover")
def hover(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
docs = open_file.ast and open_file.ast.get_docs(utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text))
docs = open_file.ast and open_file.ast.get_docs(
utils.pos_to_idx(
params["position"]["line"],
params["position"]["character"],
open_file.text,
)
)
if docs:
self._send_response(id, {
self._send_response(
id,
{
"contents": {
"kind": "markdown",
"value": docs,
}
})
},
)
else:
self._send_response(id, None)
@ -220,70 +283,218 @@ class LanguageServer:
self._send_response(id, [])
return
idx = utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text)
completions = complete(open_file.ast, open_file.tokens, idx)
self._send_response(id, [completion.to_json(True) for completion in completions])
idx = utils.pos_to_idx(
params["position"]["line"], params["position"]["character"], open_file.text
)
completions = complete(self, open_file.ast, open_file.tokens, idx)
self._send_response(
id, [completion.to_json(True) for completion in completions]
)
@command("textDocument/formatting")
def formatting(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
if open_file.text is None:
self._send_error(id, ErrorCode.RequestFailed, "Document is not open")
return
try:
formatted_blp = formatter.format(
open_file.text,
params["options"]["tabSize"],
params["options"]["insertSpaces"],
)
except PrintableError:
self._send_error(id, ErrorCode.RequestFailed, "Could not format document")
return
lst = []
for tag, i1, i2, j1, j2 in SequenceMatcher(
None, open_file.text, formatted_blp
).get_opcodes():
if tag in ("replace", "insert", "delete"):
lst.append(
TextEdit(
Range(i1, i2, open_file.text),
"" if tag == "delete" else formatted_blp[j1:j2],
).to_json()
)
self._send_response(id, lst)
@command("textDocument/x-blueprint-compile")
def compile(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
if open_file.ast is None:
self._send_error(id, ErrorCode.RequestFailed, "Document is not open")
return
xml = None
try:
output = XmlOutput()
xml = output.emit(open_file.ast, indent=2, generated_notice=False)
except:
printerr(traceback.format_exc())
self._send_error(id, ErrorCode.RequestFailed, "Could not compile document")
return
self._send_response(id, {"xml": xml})
@command("x-blueprint/decompile")
def decompile(self, id, params):
text = params.get("text")
blp = None
if text.strip() == "":
blp = ""
printerr("Decompiled to empty blueprint because input was empty")
else:
try:
blp = decompiler.decompile_string(text)
except decompiler.UnsupportedError as e:
self._send_error(id, ErrorCode.RequestFailed, e.message)
return
except:
printerr(traceback.format_exc())
self._send_error(id, ErrorCode.RequestFailed, "Invalid input")
return
self._send_response(id, {"blp": blp})
@command("textDocument/semanticTokens/full")
def semantic_tokens(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
self._send_response(id, {
self._send_response(
id,
{
"data": open_file.calc_semantic_tokens(),
})
},
)
@command("textDocument/codeAction")
def code_actions(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
range_start = utils.pos_to_idx(params["range"]["start"]["line"], params["range"]["start"]["character"], open_file.text)
range_end = utils.pos_to_idx(params["range"]["end"]["line"], params["range"]["end"]["character"], open_file.text)
range = Range(
utils.pos_to_idx(
params["range"]["start"]["line"],
params["range"]["start"]["character"],
open_file.text,
),
utils.pos_to_idx(
params["range"]["end"]["line"],
params["range"]["end"]["character"],
open_file.text,
),
open_file.text,
)
actions = [
{
"title": action.title,
"kind": "quickfix",
"diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, diagnostic)],
"diagnostics": [self._create_diagnostic(open_file.uri, diagnostic)],
"edit": {
"changes": {
open_file.uri: [{
"range": utils.idxs_to_range(diagnostic.start, diagnostic.end, open_file.text),
"newText": action.replace_with
}]
open_file.uri: [
{
"range": (
action.edit_range.to_json()
if action.edit_range
else diagnostic.range.to_json()
),
"newText": action.replace_with,
}
]
}
},
}
for diagnostic in open_file.diagnostics
if not (diagnostic.end < range_start or diagnostic.start > range_end)
if range.overlaps(diagnostic.range)
for action in diagnostic.actions
]
self._send_response(id, actions)
@command("textDocument/documentSymbol")
def document_symbols(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
symbols = open_file.ast.get_document_symbols()
def to_json(symbol: DocumentSymbol):
result = {
"name": symbol.name,
"kind": symbol.kind,
"range": symbol.range.to_json(),
"selectionRange": symbol.selection_range.to_json(),
"children": [to_json(child) for child in symbol.children],
}
if symbol.detail is not None:
result["detail"] = symbol.detail
return result
self._send_response(id, [to_json(symbol) for symbol in symbols])
@command("textDocument/definition")
def definition(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
idx = utils.pos_to_idx(
params["position"]["line"], params["position"]["character"], open_file.text
)
definition = open_file.ast.get_reference(idx)
if definition is None:
self._send_response(id, None)
else:
self._send_response(
id,
definition.to_json(open_file.uri),
)
def _send_file_updates(self, open_file: OpenFile):
self._send_notification("textDocument/publishDiagnostics", {
self._send_notification(
"textDocument/publishDiagnostics",
{
"uri": open_file.uri,
"diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, err) for err in open_file.diagnostics],
})
"diagnostics": [
self._create_diagnostic(open_file.uri, err)
for err in open_file.diagnostics
],
},
)
def _create_diagnostic(self, uri: str, err: CompileError):
message = err.message
assert err.range is not None
for hint in err.hints:
message += "\nhint: " + hint
def _create_diagnostic(self, text, uri, err):
result = {
"range": utils.idxs_to_range(err.start, err.end, text),
"message": err.message,
"severity": DiagnosticSeverity.Warning if isinstance(err, CompileWarning) else DiagnosticSeverity.Error,
"range": err.range.to_json(),
"message": message,
"severity": (
DiagnosticSeverity.Warning
if isinstance(err, CompileWarning)
else DiagnosticSeverity.Error
),
}
if isinstance(err, DeprecatedWarning):
result["tags"] = [DiagnosticTag.Deprecated]
if isinstance(err, UnusedWarning):
result["tags"] = [DiagnosticTag.Unnecessary]
if len(err.references) > 0:
result["relatedInformation"] = [
{
"location": {
"uri": uri,
"range": utils.idxs_to_range(ref.start, ref.end, text),
"range": ref.range.to_json(),
},
"message": ref.message
"message": ref.message,
}
for ref in err.references
]
@ -295,4 +506,3 @@ for name in dir(LanguageServer):
item = getattr(LanguageServer, name)
if callable(item) and hasattr(item, "_json_method"):
LanguageServer.commands[item._json_method] = item

View file

@ -18,11 +18,14 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from dataclasses import dataclass
import enum
import json
import os
import typing as T
from dataclasses import dataclass, field
from .errors import *
from .tokenizer import Range
from .utils import *
@ -31,13 +34,16 @@ class TextDocumentSyncKind(enum.IntEnum):
Full = 1
Incremental = 2
class CompletionItemTag(enum.IntEnum):
Deprecated = 1
class InsertTextFormat(enum.IntEnum):
PlainText = 1
Snippet = 2
class CompletionItemKind(enum.IntEnum):
Text = 1
Method = 2
@ -66,15 +72,21 @@ class CompletionItemKind(enum.IntEnum):
TypeParameter = 25
class ErrorCode(enum.IntEnum):
RequestFailed = -32803
@dataclass
class Completion:
label: str
kind: CompletionItemKind
signature: T.Optional[str] = None
deprecated: bool = False
sort_text: T.Optional[str] = None
docs: T.Optional[str] = None
text: T.Optional[str] = None
snippet: T.Optional[str] = None
detail: T.Optional[str] = None
def to_json(self, snippets: bool):
insert_text = self.text or self.label
@ -87,14 +99,21 @@ class Completion:
"label": self.label,
"kind": self.kind,
"tags": [CompletionItemTag.Deprecated] if self.deprecated else None,
"detail": self.signature,
"documentation": {
# 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",
"value": self.docs,
} if self.docs else None,
}
if self.docs
else None
),
"deprecated": self.deprecated,
"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}
@ -110,9 +129,100 @@ class DiagnosticSeverity(enum.IntEnum):
Hint = 4
class DiagnosticTag(enum.IntEnum):
Unnecessary = 1
Deprecated = 2
@dataclass
class SemanticToken:
start: int
end: int
type: SemanticTokenType
class SymbolKind(enum.IntEnum):
File = 1
Module = 2
Namespace = 3
Package = 4
Class = 5
Method = 6
Property = 7
Field = 8
Constructor = 9
Enum = 10
Interface = 11
Function = 12
Variable = 13
Constant = 14
String = 15
Number = 16
Boolean = 17
Array = 18
Object = 19
Key = 20
Null = 21
EnumMember = 22
Struct = 23
Event = 24
Operator = 25
TypeParameter = 26
@dataclass
class DocumentSymbol:
name: str
kind: SymbolKind
range: Range
selection_range: Range
detail: T.Optional[str] = None
children: T.List["DocumentSymbol"] = field(default_factory=list)
@dataclass
class LocationLink:
origin_selection_range: Range
target_range: Range
target_selection_range: Range
def to_json(self, target_uri: str):
return {
"originSelectionRange": self.origin_selection_range.to_json(),
"targetUri": target_uri,
"targetRange": self.target_range.to_json(),
"targetSelectionRange": self.target_selection_range.to_json(),
}
@dataclass
class TextEdit:
range: Range
newText: str
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

@ -18,16 +18,23 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
import argparse
import difflib
import os
import sys
import typing as T
import argparse, json, os, sys
from .errors import PrintableError, report_bug, MultipleErrors
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
from . import parser, tokenizer, decompiler, interactive_port
from .outputs import XmlOutput
from .utils import Colors
from .xml_emitter import XmlEmitter
VERSION = "uninstalled"
LIBDIR = None
class BlueprintApp:
def main(self):
@ -35,19 +42,82 @@ class BlueprintApp:
self.subparsers = self.parser.add_subparsers(metavar="command")
self.parser.set_defaults(func=self.cmd_help)
compile = self.add_subcommand("compile", "Compile blueprint files", self.cmd_compile)
compile = self.add_subcommand(
"compile", "Compile blueprint files", self.cmd_compile
)
compile.add_argument("--output", dest="output", default="-")
compile.add_argument("input", metavar="filename", default=sys.stdin, type=argparse.FileType('r'))
compile.add_argument("--typelib-path", nargs="?", action="append")
compile.add_argument(
"input", metavar="filename", default=sys.stdin, type=argparse.FileType("r")
)
batch_compile = self.add_subcommand("batch-compile", "Compile many blueprint files at once", self.cmd_batch_compile)
batch_compile = self.add_subcommand(
"batch-compile",
"Compile many blueprint files at once",
self.cmd_batch_compile,
)
batch_compile.add_argument("output_dir", metavar="output-dir")
batch_compile.add_argument("input_dir", metavar="input-dir")
batch_compile.add_argument("inputs", nargs="+", metavar="filenames", default=sys.stdin, type=argparse.FileType('r'))
batch_compile.add_argument("--typelib-path", nargs="?", action="append")
batch_compile.add_argument(
"inputs",
nargs="+",
metavar="filenames",
default=sys.stdin,
type=argparse.FileType("r"),
)
format = self.add_subcommand(
"format", "Format given blueprint files", self.cmd_format
)
format.add_argument(
"-f",
"--fix",
help="Apply the edits to the files",
default=False,
action="store_true",
)
format.add_argument(
"-t",
"--tabs",
help="Use tabs instead of spaces",
default=False,
action="store_true",
)
format.add_argument(
"-s",
"--spaces-num",
help="How many spaces should be used per indent",
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("lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp)
lsp.add_argument("--logfile", dest="logfile", default=None, type=argparse.FileType('a'))
lsp = self.add_subcommand(
"lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp
)
self.add_subcommand("help", "Show this message", self.cmd_help)
@ -65,18 +135,19 @@ class BlueprintApp:
except:
report_bug()
def add_subcommand(self, name, help, func):
def add_subcommand(self, name: str, help: str, func):
parser = self.subparsers.add_parser(name, help=help)
parser.set_defaults(func=func)
return parser
def cmd_help(self, opts):
self.parser.print_help()
def cmd_compile(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:
xml, warnings = self._compile(data)
@ -90,17 +161,24 @@ class BlueprintApp:
with open(opts.output, "w") as file:
file.write(xml)
except PrintableError as e:
e.pretty_print(opts.input.name, data)
e.pretty_print(opts.input.name, data, stream=sys.stderr)
sys.exit(1)
def cmd_batch_compile(self, opts):
if opts.typelib_path != None:
for typelib_path in opts.typelib_path:
add_typelib_search_path(typelib_path)
for file in opts.inputs:
data = file.read()
file_abs = os.path.abspath(file.name)
input_dir_abs = os.path.abspath(opts.input_dir)
try:
if not os.path.commonpath([file.name, opts.input_dir]):
print(f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}")
if not os.path.commonpath([file_abs, input_dir_abs]):
print(
f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}"
)
sys.exit(1)
xml, warnings = self._compile(data)
@ -111,9 +189,8 @@ class BlueprintApp:
path = os.path.join(
opts.output_dir,
os.path.relpath(
os.path.splitext(file.name)[0] + ".ui",
opts.input_dir
)
os.path.splitext(file.name)[0] + ".ui", opts.input_dir
),
)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as file:
@ -122,29 +199,157 @@ class BlueprintApp:
e.pretty_print(file.name, data)
sys.exit(1)
def cmd_format(self, opts):
input_files = []
missing_files = []
panic = False
formatted_files = 0
skipped_files = 0
for path in opts.inputs:
if os.path.isfile(path):
input_files.append(path)
elif os.path.isdir(path):
for root, subfolders, files in os.walk(path):
for file in files:
if file.endswith(".blp"):
input_files.append(os.path.join(root, file))
else:
missing_files.append(path)
for file in input_files:
with open(file, "r+") as file:
data = file.read()
errored = False
try:
self._compile(data)
except:
errored = True
formatted_str = formatter.format(data, opts.spaces_num, not opts.tabs)
if data != formatted_str:
happened = "Would format"
if opts.fix and not errored:
file.seek(0)
file.truncate()
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)
for line in difflib.unified_diff(
a_lines, b_lines, fromfile=file.name, tofile=file.name, n=5
):
# Work around https://bugs.python.org/issue2142
# See:
# https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html
if line[-1] == "\n":
diff_lines.append(line)
else:
diff_lines.append(line + "\n")
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"
panic = True
skipped_files += 1
else:
to_print += f"{happened} {file.name}"
formatted_files += 1
print(to_print)
print(Colors.CLEAR)
missing_num = len(missing_files)
summary = ""
if missing_num > 0:
print(
f"{Colors.BOLD}{Colors.RED}Could not find files:{Colors.CLEAR}{Colors.BOLD}"
)
for path in missing_files:
print(f" {path}")
print(Colors.CLEAR)
panic = True
if len(input_files) == 0:
print(f"{Colors.RED}No Blueprint files found")
sys.exit(1)
def would_be(verb):
return verb if opts.fix else f"would be {verb}"
def how_many(count, bold=True):
string = f"{Colors.BLUE}{count} {'files' if count != 1 else 'file'}{Colors.CLEAR}"
return Colors.BOLD + string + Colors.BOLD if bold else Colors.CLEAR + string
if formatted_files > 0:
summary += f"{how_many(formatted_files)} {would_be('formatted')}, "
panic = panic or not opts.fix
left_files = len(input_files) - formatted_files - skipped_files
summary += f"{how_many(left_files, False)} {would_be('left unchanged')}"
if skipped_files > 0:
summary += f", {how_many(skipped_files)} {would_be('skipped')}"
if missing_num > 0:
summary += f", {how_many(missing_num)} not found"
print(summary + Colors.CLEAR)
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(opts.logfile)
langserv = LanguageServer()
langserv.run()
def cmd_port(self, opts):
interactive_port.run(opts)
def _compile(self, data: str) -> T.Tuple[str, T.List[PrintableError]]:
def _compile(self, data: str) -> T.Tuple[str, T.List[CompileError]]:
tokens = tokenizer.tokenize(data)
ast, errors, warnings = parser.parse(tokens)
if errors:
raise errors
if len(ast.errors):
raise MultipleErrors(ast.errors)
if ast is None:
raise CompilerBugError()
return ast.generate(), warnings
formatter = XmlOutput()
return formatter.emit(ast), warnings
def main(version):
global VERSION
VERSION = version
def main(version, libdir):
global VERSION, LIBDIR
VERSION, LIBDIR = version, libdir
BlueprintApp().main()

View file

@ -0,0 +1,9 @@
from ..language import UI
class OutputFormat:
def emit(self, ui: UI) -> str:
raise NotImplementedError()
from .xml import XmlOutput

View file

@ -0,0 +1,426 @@
import typing as T
from ...language import *
from .. import OutputFormat
from .xml_emitter import XmlEmitter
class XmlOutput(OutputFormat):
def emit(self, ui: UI, indent=2, generated_notice=True) -> str:
xml = XmlEmitter(indent, generated_notice)
self._emit_ui(ui, xml)
return xml.result
def _emit_ui(self, ui: UI, xml: XmlEmitter):
if domain := ui.translation_domain:
xml.start_tag("interface", domain=domain.domain)
else:
xml.start_tag("interface")
self._emit_gtk_directive(ui.gtk_decl, xml)
for x in ui.contents:
if isinstance(x, Template):
self._emit_template(x, xml)
elif isinstance(x, Object):
self._emit_object(x, xml)
elif isinstance(x, Menu):
self._emit_menu(x, xml)
else:
raise CompilerBugError()
xml.end_tag()
def _emit_gtk_directive(self, gtk: GtkDirective, xml: XmlEmitter):
xml.put_self_closing("requires", lib="gtk", version=gtk.gir_namespace.version)
def _emit_template(self, template: Template, xml: XmlEmitter):
xml.start_tag(
"template", **{"class": template.gir_class}, parent=template.parent_type
)
self._emit_object_or_template(template, xml)
xml.end_tag()
def _emit_object(self, obj: Object, xml: XmlEmitter):
xml.start_tag(
"object",
**{"class": obj.class_name},
id=obj.id,
)
self._emit_object_or_template(obj, xml)
xml.end_tag()
def _emit_object_or_template(
self, obj: T.Union[Object, Template, ExtListItemFactory], xml: XmlEmitter
):
for child in obj.content.children:
if isinstance(child, Property):
self._emit_property(child, xml)
elif isinstance(child, Signal):
self._emit_signal(child, xml)
elif isinstance(child, Child):
self._emit_child(child, xml)
else:
self._emit_extensions(child, xml)
# List action widgets
action_widgets = obj.action_widgets
if action_widgets:
xml.start_tag("action-widgets")
for action_widget in action_widgets:
xml.start_tag(
"action-widget",
response=action_widget.response_id,
default=action_widget.is_default or None,
)
xml.put_text(action_widget.widget_id)
xml.end_tag()
xml.end_tag()
def _emit_menu(self, menu: Menu, xml: XmlEmitter):
xml.start_tag(menu.tag, id=menu.id)
for child in menu.items:
if isinstance(child, Menu):
self._emit_menu(child, xml)
elif isinstance(child, MenuAttribute):
xml.start_tag(
"attribute",
name=child.name,
**self._translated_string_attrs(child.value.child),
)
xml.put_text(child.value.string)
xml.end_tag()
else:
raise CompilerBugError()
xml.end_tag()
def _emit_property(self, property: Property, xml: XmlEmitter):
value = property.value
props: T.Dict[str, T.Optional[str]] = {
"name": property.name,
}
if isinstance(value, Value):
child = value.child
if isinstance(child, Translated):
xml.start_tag(
"property", **props, **self._translated_string_attrs(child)
)
xml.put_text(child.string)
xml.end_tag()
else:
xml.start_tag("property", **props)
self._emit_value(value, xml)
xml.end_tag()
elif isinstance(value, Binding):
if simple := value.simple_binding:
props["bind-source"] = self._object_id(value, simple.source)
props["bind-property"] = simple.property_name
flags = []
if not simple.no_sync_create:
flags.append("sync-create")
if simple.inverted:
flags.append("invert-boolean")
if simple.bidirectional:
flags.append("bidirectional")
props["bind-flags"] = "|".join(flags) or None
xml.put_self_closing("property", **props)
else:
xml.start_tag("binding", **props)
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()
def _translated_string_attrs(
self, translated: T.Optional[T.Union[QuotedLiteral, Translated]]
) -> T.Dict[str, T.Optional[str]]:
if translated is None:
return {}
elif isinstance(translated, QuotedLiteral):
return {}
else:
return {"translatable": "yes", "context": translated.translate_context}
def _emit_signal(self, signal: Signal, xml: XmlEmitter):
name = signal.name
if signal.detail_name:
name += "::" + signal.detail_name
xml.put_self_closing(
"signal",
name=name,
handler=signal.handler,
swapped=signal.is_swapped,
after=signal.is_after or None,
object=(
self._object_id(signal, signal.object_id) if signal.object_id else None
),
)
def _emit_child(self, child: Child, xml: XmlEmitter):
child_type = internal_child = None
if child.annotation is not None:
annotation = child.annotation.child
if isinstance(annotation, ChildType):
child_type = annotation.child_type
elif isinstance(annotation, ChildInternal):
internal_child = annotation.internal_child
elif isinstance(annotation, ChildExtension):
child_type = "action"
else:
raise CompilerBugError()
xml.start_tag("child", type=child_type, internal_child=internal_child)
self._emit_object(child.object, xml)
xml.end_tag()
def _emit_literal(self, literal: Literal, xml: XmlEmitter):
value = literal.value
if isinstance(value, IdentLiteral):
value_type = value.context[ValueTypeCtx].value_type
if isinstance(value_type, gir.BoolType):
xml.put_text(value.ident)
elif isinstance(value_type, gir.Enumeration):
xml.put_text(str(value_type.members[value.ident].value))
else:
xml.put_text(self._object_id(value, value.ident))
elif isinstance(value, TypeLiteral):
xml.put_text(value.type_name.glib_type_name)
else:
if isinstance(value.value, float) and value.value == int(value.value):
xml.put_text(int(value.value))
else:
xml.put_text(value.value)
def _emit_value(self, value: Value, xml: XmlEmitter):
if isinstance(value.child, Literal):
self._emit_literal(value.child, xml)
elif isinstance(value.child, Flags):
xml.put_text(
"|".join([str(flag.value or flag.name) for flag in value.child.flags])
)
else:
raise CompilerBugError()
def _emit_expression(self, expression: Expression, xml: XmlEmitter):
self._emit_expression_part(expression.last, xml)
def _emit_expression_part(self, expression: ExprBase, xml: XmlEmitter):
if isinstance(expression, LiteralExpr):
self._emit_literal_expr(expression, xml)
elif isinstance(expression, LookupOp):
self._emit_lookup_op(expression, xml)
elif isinstance(expression, Expression):
self._emit_expression(expression, xml)
elif isinstance(expression, CastExpr):
self._emit_cast_expr(expression, xml)
elif isinstance(expression, ClosureExpr):
self._emit_closure_expr(expression, xml)
else:
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:
xml.start_tag("constant", type=expr.type)
self._emit_literal(expr.literal, xml)
xml.end_tag()
def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter):
xml.start_tag("lookup", name=expr.property_name, type=expr.lhs.type)
self._emit_expression_part(expr.lhs, xml)
xml.end_tag()
def _emit_cast_expr(self, expr: CastExpr, xml: XmlEmitter):
self._emit_expression_part(expr.lhs, xml)
def _emit_closure_expr(self, expr: ClosureExpr, xml: XmlEmitter):
xml.start_tag("closure", function=expr.closure_name, type=expr.type)
for arg in expr.args:
self._emit_expression_part(arg.expr, xml)
xml.end_tag()
def _emit_attribute(
self,
tag: str,
attr: str,
name: str,
value: T.Union[Value, StringValue],
xml: XmlEmitter,
):
attrs = {attr: name}
if isinstance(value.child, Translated):
xml.start_tag(tag, **attrs, **self._translated_string_attrs(value.child))
xml.put_text(value.child.string)
xml.end_tag()
elif isinstance(value.child, QuotedLiteral):
xml.start_tag(tag, **attrs)
xml.put_text(value.child.value)
xml.end_tag()
else:
xml.start_tag(tag, **attrs)
self._emit_value(value, xml)
xml.end_tag()
def _emit_extensions(self, extension, xml: XmlEmitter):
if isinstance(extension, ExtAccessibility):
xml.start_tag("accessibility")
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):
xml.start_tag("condition")
xml.put_text(extension.condition)
xml.end_tag()
elif isinstance(extension, AdwBreakpointSetters):
for setter in extension.setters:
if setter.value is None:
continue
attrs = {}
if isinstance(setter.value.child, Translated):
attrs = self._translated_string_attrs(setter.value.child)
xml.start_tag(
"setter",
object=self._object_id(setter, setter.object_id),
property=setter.property_name,
**attrs,
)
if isinstance(setter.value.child, Translated):
xml.put_text(setter.value.child.string)
elif (
isinstance(setter.value.child, Literal)
and isinstance(setter.value.child.value, IdentLiteral)
and setter.value.child.value.ident == "null"
and setter.context[ScopeCtx].objects.get("null") is None
):
pass
else:
self._emit_value(setter.value, xml)
xml.end_tag()
elif isinstance(extension, Filters):
xml.start_tag(extension.tokens["tag_name"])
for prop in extension.children:
xml.start_tag(prop.tokens["tag_name"])
xml.put_text(prop.tokens["name"])
xml.end_tag()
xml.end_tag()
elif isinstance(extension, ExtComboBoxItems):
xml.start_tag("items")
for prop in extension.children:
self._emit_attribute("item", "id", prop.name, prop.value, xml)
xml.end_tag()
elif isinstance(extension, ExtLayout):
xml.start_tag("layout")
for prop in extension.children:
self._emit_attribute("property", "name", prop.name, prop.value, xml)
xml.end_tag()
elif isinstance(extension, ExtAdwResponseDialog):
xml.start_tag("responses")
for response in extension.responses:
xml.start_tag(
"response",
id=response.id,
**self._translated_string_attrs(response.value.child),
enabled=None if response.enabled else "false",
appearance=response.appearance,
)
xml.put_text(response.value.string)
xml.end_tag()
xml.end_tag()
elif isinstance(extension, ExtScaleMarks):
xml.start_tag("marks")
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(label),
)
if mark.label is not None:
xml.put_text(mark.label.string)
xml.end_tag()
xml.end_tag()
elif isinstance(extension, ExtStringListStrings):
xml.start_tag("items")
for string in extension.children:
value = string.child
xml.start_tag("item", **self._translated_string_attrs(value.child))
xml.put_text(value.string)
xml.end_tag()
xml.end_tag()
elif isinstance(extension, ExtListItemFactory):
child_xml = XmlEmitter(generated_notice=False)
child_xml.start_tag("interface")
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()
xml.start_tag("property", name="bytes")
xml.put_cdata(child_xml.result)
xml.end_tag()
elif isinstance(extension, ExtStyles):
xml.start_tag("style")
for style in extension.children:
xml.put_self_closing("class", name=style.name)
xml.end_tag()
elif isinstance(extension, ExtSizeGroupWidgets):
xml.start_tag("widgets")
for prop in extension.children:
xml.put_self_closing("widget", name=prop.name)
xml.end_tag()
else:
raise CompilerBugError()
def _object_id(self, node: AstNode, id: str) -> str:
if id == "template" and node.context[ScopeCtx].template is not None:
return node.context[ScopeCtx].template.gir_class.glib_type_name
else:
return id

View file

@ -17,19 +17,32 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from xml.sax import saxutils
from . import gir
from blueprintcompiler.gir import GirType
from blueprintcompiler.language.types import ClassName
class XmlEmitter:
def __init__(self, indent=2):
def __init__(self, indent=2, generated_notice=True):
self.indent = indent
self.result = '<?xml version="1.0" encoding="UTF-8"?>'
if generated_notice:
self.result += (
"\n"
"<!--\n"
"DO NOT EDIT!\n"
"This file was @generated by blueprint-compiler. Instead, edit the\n"
"corresponding .blp file and regenerate this file with blueprint-compiler.\n"
"-->"
)
self._tag_stack = []
self._needs_newline = False
def start_tag(self, tag, **attrs):
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():
@ -55,16 +68,23 @@ class XmlEmitter:
self.result += f"</{tag}>"
self._needs_newline = True
def put_text(self, text):
def put_text(self, text: T.Union[str, int, float]):
self.result += saxutils.escape(str(text))
self._needs_newline = False
def put_cdata(self, text: str):
text = text.replace("]]>", "]]]]><![CDATA[>")
self.result += f"<![CDATA[{text}]]>"
self._needs_newline = False
def _indent(self):
if self.indent is not None:
self.result += "\n" + " " * (self.indent * len(self._tag_stack))
def _to_string(self, val):
if isinstance(val, gir.GirType):
if isinstance(val, GirType):
return val.glib_type_name
elif isinstance(val, ClassName):
return val.glib_type_name
else:
return str(val)

View file

@ -20,13 +20,18 @@
"""Utilities for parsing an AST from a token stream."""
import typing as T
from collections import defaultdict
from enum import Enum
from .errors import assert_true, CompilerBugError, CompileError, CompileWarning, UnexpectedTokenError
from .tokenizer import Token, TokenType
from . import utils
from .ast_utils import AstNode
from .errors import (
CompileError,
CompilerBugError,
CompileWarning,
UnexpectedTokenError,
assert_true,
)
from .tokenizer import Range, Token, TokenType
SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE]
@ -58,23 +63,31 @@ class ParseGroup:
be converted to AST nodes by passing the children and key=value pairs to
the AST node constructor."""
def __init__(self, ast_type, start: int):
def __init__(self, ast_type: T.Type[AstNode], start: int, text: str):
self.ast_type = ast_type
self.children: T.List[ParseGroup] = []
self.keys: T.Dict[str, T.Any] = {}
self.tokens: T.Dict[str, Token] = {}
self.tokens: T.Dict[str, T.Optional[Token]] = {}
self.ranges: T.Dict[str, Range] = {}
self.start = start
self.end = None
self.end: T.Optional[int] = None
self.incomplete = False
self.text = text
def add_child(self, child):
def add_child(self, child: "ParseGroup"):
self.children.append(child)
def set_val(self, key, val, token):
def set_val(self, key: str, val: T.Any, token: T.Optional[Token]):
assert_true(key not in self.keys)
self.keys[key] = val
self.tokens[key] = token
if token:
self.set_range(key, token.range)
def set_range(self, key: str, range: Range):
assert_true(key not in self.ranges)
self.ranges[key] = range
def to_ast(self):
"""Creates an AST node from the match group."""
@ -82,45 +95,44 @@ class ParseGroup:
try:
return self.ast_type(self, children, self.keys, incomplete=self.incomplete)
except TypeError as e:
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 ")
except TypeError: # pragma: no cover
raise CompilerBugError(
f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace."
)
class ParseContext:
"""Contains the state of the parser."""
def __init__(self, tokens, index=0):
self.tokens = list(tokens)
def __init__(self, tokens: T.List[Token], text: str, index=0):
self.tokens = tokens
self.text = text
self.binding_power = 0
self.index = index
self.start = index
self.group = None
self.group_keys = {}
self.group_children = []
self.last_group = None
self.group: T.Optional[ParseGroup] = None
self.group_keys: T.Dict[str, T.Tuple[T.Any, T.Optional[Token]]] = {}
self.group_children: T.List[ParseGroup] = []
self.group_ranges: T.Dict[str, Range] = {}
self.last_group: T.Optional[ParseGroup] = None
self.group_incomplete = False
self.errors = []
self.warnings = []
self.errors: T.List[CompileError] = []
self.warnings: T.List[CompileWarning] = []
def create_child(self):
def create_child(self) -> "ParseContext":
"""Creates a new ParseContext at this context's position. The new
context will be used to parse one node. If parsing is successful, the
new context will be applied to "self". If parsing fails, the new
context will be discarded."""
ctx = ParseContext(self.tokens, self.index)
ctx = ParseContext(self.tokens, self.text, self.index)
ctx.errors = self.errors
ctx.warnings = self.warnings
ctx.binding_power = self.binding_power
return ctx
def apply_child(self, other):
def apply_child(self, other: "ParseContext"):
"""Applies a child context to this context."""
if other.group is not None:
@ -130,6 +142,8 @@ class ParseContext:
other.group.set_val(key, val, token)
for child in other.group_children:
other.group.add_child(child)
for key, range in other.group_ranges.items():
other.group.set_range(key, range)
other.group.end = other.tokens[other.index - 1].end
other.group.incomplete = other.group_incomplete
self.group_children.append(other.group)
@ -138,6 +152,7 @@ class ParseContext:
# its matched values
self.group_keys = {**self.group_keys, **other.group_keys}
self.group_children += other.group_children
self.group_ranges = {**self.group_ranges, **other.group_ranges}
self.group_incomplete |= other.group_incomplete
self.index = other.index
@ -148,26 +163,33 @@ class ParseContext:
elif other.last_group:
self.last_group = other.last_group
def start_group(self, ast_type):
def start_group(self, ast_type: T.Type[AstNode]):
"""Sets this context to have its own match group."""
assert_true(self.group is None)
self.group = ParseGroup(ast_type, self.tokens[self.index].start)
self.group = ParseGroup(ast_type, self.tokens[self.index].start, self.text)
def set_group_val(self, key, value, token):
def set_group_val(self, key: str, value: T.Any, token: T.Optional[Token]):
"""Sets a matched key=value pair on the current match group."""
assert_true(key not in self.group_keys)
self.group_keys[key] = (value, token)
def set_mark(self, key: str):
"""Sets a zero-length range on the current match group at the current position."""
self.group_ranges[key] = Range(
self.tokens[self.index].start, self.tokens[self.index].start, self.text
)
def set_group_incomplete(self):
"""Marks the current match group as incomplete (it could not be fully
parsed, but the parser recovered)."""
self.group_incomplete = True
def skip(self):
"""Skips whitespace and comments."""
while self.index < len(self.tokens) and self.tokens[self.index].type in SKIP_TOKENS:
while (
self.index < len(self.tokens)
and self.tokens[self.index].type in SKIP_TOKENS
):
self.index += 1
def next_token(self) -> Token:
@ -192,14 +214,16 @@ class ParseContext:
self.skip()
end = self.tokens[self.index - 1].end
if (len(self.errors)
if (
len(self.errors)
and isinstance((err := self.errors[-1]), UnexpectedTokenError)
and err.end == start):
err.end = end
and err.range.end == start
):
err.range.end = end
else:
self.errors.append(UnexpectedTokenError(start, end))
self.errors.append(UnexpectedTokenError(Range(start, end, self.text)))
def is_eof(self) -> Token:
def is_eof(self) -> bool:
return self.index >= len(self.tokens) or self.peek_token().type == TokenType.EOF
@ -223,65 +247,45 @@ class ParseNode:
def _parse(self, ctx: ParseContext) -> bool:
raise NotImplementedError()
def err(self, message):
def err(self, message: str) -> "Err":
"""Causes this ParseNode to raise an exception if it fails to parse.
This prevents the parser from backtracking, so you should understand
what it does and how the parser works before using it."""
return Err(self, message)
def expected(self, expect):
def expected(self, expect: str) -> "Err":
"""Convenience method for err()."""
return self.err("Expected " + expect)
def warn(self, message):
""" 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."""
def __init__(self, child, message):
def __init__(self, child, message: str):
self.child = to_parse_node(child)
self.message = message
def _parse(self, ctx):
def _parse(self, ctx: ParseContext):
if self.child.parse(ctx).failed():
start_idx = ctx.start
while ctx.tokens[start_idx].type in SKIP_TOKENS:
start_idx += 1
start_token = ctx.tokens[start_idx]
end_token = ctx.tokens[ctx.index]
raise CompileError(self.message, start_token.start, end_token.end)
return True
class Warning(ParseNode):
""" ParseNode that emits a compile warning if it parses successfully. """
def __init__(self, child, message):
self.child = to_parse_node(child)
self.message = message
def _parse(self, ctx):
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))
raise CompileError(
self.message, Range(start_token.start, start_token.start, ctx.text)
)
return True
class Fail(ParseNode):
"""ParseNode that emits a compile error if it parses successfully."""
def __init__(self, child, message):
def __init__(self, child, message: str):
self.child = to_parse_node(child)
self.message = message
def _parse(self, ctx):
def _parse(self, ctx: ParseContext):
if self.child.parse(ctx).succeeded():
start_idx = ctx.start
while ctx.tokens[start_idx].type in SKIP_TOKENS:
@ -289,13 +293,16 @@ class Fail(ParseNode):
start_token = ctx.tokens[start_idx]
end_token = ctx.tokens[ctx.index]
raise CompileError(self.message, start_token.start, end_token.end)
raise CompileError(
self.message, Range.join(start_token.range, end_token.range)
)
return True
class Group(ParseNode):
"""ParseNode that creates a match group."""
def __init__(self, ast_type, child):
def __init__(self, ast_type: T.Type[AstNode], child):
self.ast_type = ast_type
self.child = to_parse_node(child)
@ -307,6 +314,7 @@ class Group(ParseNode):
class Sequence(ParseNode):
"""ParseNode that attempts to match all of its children in sequence."""
def __init__(self, *children):
self.children = [to_parse_node(child) for child in children]
@ -320,6 +328,7 @@ class Sequence(ParseNode):
class Statement(ParseNode):
"""ParseNode that attempts to match all of its children in sequence. If any
child raises an error, the error will be logged but parsing will continue."""
def __init__(self, *children):
self.children = [to_parse_node(child) for child in children]
@ -335,7 +344,7 @@ class Statement(ParseNode):
token = ctx.peek_token()
if str(token) != ";":
ctx.errors.append(CompileError("Expected `;`", token.start, token.end))
ctx.errors.append(CompileError("Expected `;`", token.range))
else:
ctx.next_token()
return True
@ -344,12 +353,14 @@ class Statement(ParseNode):
class AnyOf(ParseNode):
"""ParseNode that attempts to match exactly one of its children. Child
nodes are attempted in order."""
def __init__(self, *children):
self.children = children
@property
def children(self):
return self._children
@children.setter
def children(self, children):
self._children = [to_parse_node(child) for child in children]
@ -365,11 +376,15 @@ class Until(ParseNode):
"""ParseNode that repeats its child until a delimiting token is found. If
the child does not match, one token is skipped and the match is attempted
again."""
def __init__(self, child, delimiter):
def __init__(self, child, delimiter, between_delimiter=None):
self.child = to_parse_node(child)
self.delimiter = to_parse_node(delimiter)
self.between_delimiter = (
to_parse_node(between_delimiter) if between_delimiter is not None else None
)
def _parse(self, ctx):
def _parse(self, ctx: ParseContext):
while not self.delimiter.parse(ctx).succeeded():
if ctx.is_eof():
return False
@ -377,6 +392,17 @@ class Until(ParseNode):
try:
if not self.child.parse(ctx).matched():
ctx.skip_unexpected_token()
if (
self.between_delimiter is not None
and not self.between_delimiter.parse(ctx).succeeded()
):
if self.delimiter.parse(ctx).succeeded():
return True
else:
if ctx.is_eof():
return False
ctx.skip_unexpected_token()
except CompileError as e:
ctx.errors.append(e)
ctx.next_token()
@ -388,10 +414,10 @@ class ZeroOrMore(ParseNode):
"""ParseNode that matches its child any number of times (including zero
times). It cannot fail to parse. If its child raises an exception, one token
will be skipped and parsing will continue."""
def __init__(self, child):
self.child = to_parse_node(child)
def _parse(self, ctx):
while True:
try:
@ -405,6 +431,7 @@ class ZeroOrMore(ParseNode):
class Delimited(ParseNode):
"""ParseNode that matches its first child any number of times (including zero
times) with its second child in between and optionally at the end."""
def __init__(self, child, delimiter):
self.child = to_parse_node(child)
self.delimiter = to_parse_node(delimiter)
@ -418,6 +445,7 @@ class Delimited(ParseNode):
class Optional(ParseNode):
"""ParseNode that matches its child zero or one times. It cannot fail to
parse."""
def __init__(self, child):
self.child = to_parse_node(child)
@ -428,6 +456,7 @@ class Optional(ParseNode):
class Eof(ParseNode):
"""ParseNode that matches an EOF token."""
def _parse(self, ctx: ParseContext) -> bool:
token = ctx.next_token()
return token.type == TokenType.EOF
@ -435,14 +464,15 @@ class Eof(ParseNode):
class Match(ParseNode):
"""ParseNode that matches the given literal token."""
def __init__(self, op):
def __init__(self, op: str):
self.op = op
def _parse(self, ctx: ParseContext) -> bool:
token = ctx.next_token()
return str(token) == self.op
def expected(self, expect: str = None):
def expected(self, expect: T.Optional[str] = None):
"""Convenience method for err()."""
if expect is None:
return self.err(f"Expected '{self.op}'")
@ -453,7 +483,8 @@ class Match(ParseNode):
class UseIdent(ParseNode):
"""ParseNode that matches any identifier and sets it in a key=value pair on
the containing match group."""
def __init__(self, key):
def __init__(self, key: str):
self.key = key
def _parse(self, ctx: ParseContext):
@ -468,7 +499,8 @@ class UseIdent(ParseNode):
class UseNumber(ParseNode):
"""ParseNode that matches a number and sets it in a key=value pair on
the containing match group."""
def __init__(self, key):
def __init__(self, key: str):
self.key = key
def _parse(self, ctx: ParseContext):
@ -477,8 +509,6 @@ class UseNumber(ParseNode):
return False
number = token.get_number()
if number % 1.0 == 0:
number = int(number)
ctx.set_group_val(self.key, number, token)
return True
@ -486,7 +516,8 @@ class UseNumber(ParseNode):
class UseNumberText(ParseNode):
"""ParseNode that matches a number, but sets its *original text* it in a
key=value pair on the containing match group."""
def __init__(self, key):
def __init__(self, key: str):
self.key = key
def _parse(self, ctx: ParseContext):
@ -501,7 +532,8 @@ class UseNumberText(ParseNode):
class UseQuoted(ParseNode):
"""ParseNode that matches a quoted string and sets it in a key=value pair
on the containing match group."""
def __init__(self, key):
def __init__(self, key: str):
self.key = key
def _parse(self, ctx: ParseContext):
@ -509,12 +541,19 @@ class UseQuoted(ParseNode):
if token.type != TokenType.QUOTED:
return False
string = (str(token)[1:-1]
.replace("\\n", "\n")
.replace("\\\"", "\"")
.replace("\\\\", "\\")
.replace("\\'", "\'"))
ctx.set_group_val(self.key, string, token)
unescaped = None
try:
unescaped = utils.unescape_quote(str(token))
except utils.UnescapeError as e:
start = ctx.tokens[ctx.index - 1].start
range = Range(start + e.start, start + e.end, ctx.text)
ctx.errors.append(
CompileError(f"Invalid escape sequence '{range.text}'", range)
)
ctx.set_group_val(self.key, unescaped, token)
return True
@ -522,7 +561,8 @@ class UseLiteral(ParseNode):
"""ParseNode that doesn't match anything, but rather sets a static key=value
pair on the containing group. Useful for, e.g., property and signal flags:
`Sequence(Keyword("swapped"), UseLiteral("swapped", True))`"""
def __init__(self, key, literal):
def __init__(self, key: str, literal: T.Any):
self.key = key
self.literal = literal
@ -531,10 +571,24 @@ class UseLiteral(ParseNode):
return True
class UseExact(ParseNode):
"""Matches the given identifier and sets it as a named token."""
def __init__(self, key: str, string: str):
self.key = key
self.string = string
def _parse(self, ctx: ParseContext):
token = ctx.next_token()
ctx.set_group_val(self.key, self.string, token)
return str(token) == self.string
class Keyword(ParseNode):
"""Matches the given identifier and sets it as a named token, with the name
being the identifier itself."""
def __init__(self, kw):
def __init__(self, kw: str):
self.kw = kw
self.set_token = True
@ -544,6 +598,15 @@ class Keyword(ParseNode):
return str(token) == self.kw
class Mark(ParseNode):
def __init__(self, key: str):
self.key = key
def _parse(self, ctx: ParseContext):
ctx.set_mark(self.key)
return True
def to_parse_node(value) -> ParseNode:
if isinstance(value, str):
return Match(value)

View file

@ -19,20 +19,29 @@
from .errors import MultipleErrors, PrintableError
from .language import OBJECT_CONTENT_HOOKS, UI, Template
from .parse_tree import *
from .parser_utils import *
from .tokenizer import TokenType
from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI
def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors], T.List[PrintableError]]:
def parse(
tokens: T.List[Token],
) -> T.Tuple[T.Optional[UI], T.Optional[MultipleErrors], T.List[CompileError]]:
"""Parses a list of tokens into an abstract syntax tree."""
ctx = ParseContext(tokens)
try:
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
errors = MultipleErrors(ctx.errors) if len(ctx.errors) else None
warnings = ctx.warnings
assert ctx.last_group is not None
ast_node = ctx.last_group.to_ast()
return (ast_node, errors, warnings)
errors = [*ctx.errors, *ast_node.errors]
warnings = [*ctx.warnings, *ast_node.warnings]
return (ast_node, MultipleErrors(errors) if len(errors) else None, warnings)
except MultipleErrors as e:
return (None, e, [])
except CompileError as e:
return (None, MultipleErrors([e]), [])

View file

@ -18,11 +18,12 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import re
import typing as T
from dataclasses import dataclass
from enum import Enum
from .errors import CompileError
from . import utils
class TokenType(Enum):
@ -38,46 +39,59 @@ class TokenType(Enum):
_tokens = [
(TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"),
(TokenType.QUOTED, r'"(\\"|[^"\n])*"'),
(TokenType.QUOTED, r"'(\\'|[^'\n])*'"),
(TokenType.NUMBER, r"0x[A-Fa-f0-9_]+"),
(TokenType.NUMBER, r"[-+]?[\d_]*\d(\.[\d_]*\d)?"),
(TokenType.NUMBER, r"[-+]?\.[\d_]*\d"),
(TokenType.QUOTED, r'"(\\(.|\n)|[^\\"\n])*"'),
(TokenType.QUOTED, r"'(\\(.|\n)|[^\\'\n])*'"),
(TokenType.NUMBER, r"0x[A-Za-z0-9_]+"),
(TokenType.NUMBER, r"[\d_]+(\.[\d_]+)?"),
(TokenType.NUMBER, r"\.[\d_]+"),
(TokenType.WHITESPACE, r"\s+"),
(TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"),
(TokenType.COMMENT, r"\/\/[^\n]*"),
(TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"),
(TokenType.OP, r"\$|<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"),
(TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"),
]
_TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens]
class Token:
def __init__(self, type, start, end, string):
def __init__(self, type: TokenType, start: int, end: int, string: str):
self.type = type
self.start = start
self.end = end
self.string = string
def __str__(self):
def __str__(self) -> str:
return self.string[self.start : self.end]
def get_number(self):
@property
def range(self) -> "Range":
return Range(self.start, self.end, self.string)
def get_number(self) -> T.Union[int, float]:
from .errors import CompileError, CompilerBugError
if self.type != TokenType.NUMBER:
return None
raise CompilerBugError()
string = str(self).replace("_", "")
try:
if string.startswith("0x"):
return int(string, 16)
elif "." in string:
return float(string)
else:
return float(string.replace("_", ""))
return int(string)
except:
raise CompileError(f"{str(self)} is not a valid number literal", self.range)
def _tokenize(ui_ml: str):
from .errors import CompileError
i = 0
while i < len(ui_ml):
matched = False
for (type, regex) in _TOKENS:
for type, regex in _TOKENS:
match = regex.match(ui_ml, i)
if match is not None:
@ -87,10 +101,55 @@ def _tokenize(ui_ml: str):
break
if not matched:
raise CompileError("Could not determine what kind of syntax is meant here", i, i)
raise CompileError(
"Could not determine what kind of syntax is meant here",
Range(i, i, ui_ml),
)
yield Token(TokenType.EOF, i, i, ui_ml)
def tokenize(data: str) -> T.List[Token]:
return list(_tokenize(data))
@dataclass
class Range:
start: int
end: int
original_text: str
@property
def length(self) -> int:
return self.end - self.start
@property
def text(self) -> str:
return self.original_text[self.start : self.end]
@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:
return b
if b is None:
return a
return Range(min(a.start, b.start), max(a.end, b.end), a.original_text)
def __contains__(self, other: T.Union[int, "Range"]) -> bool:
if isinstance(other, int):
return self.start <= other <= self.end
else:
return self.start <= other.start and self.end >= other.end
def to_json(self):
return utils.idxs_to_range(self.start, self.end, self.original_text)
def overlaps(self, other: "Range") -> bool:
return not (self.end < other.start or self.start > other.end)

View file

@ -0,0 +1,322 @@
# typelib.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
import math
import mmap
import os
import sys
import typing as T
from ctypes import *
from .errors import CompilerBugError
BLOB_TYPE_STRUCT = 3
BLOB_TYPE_BOXED = 4
BLOB_TYPE_ENUM = 5
BLOB_TYPE_FLAGS = 6
BLOB_TYPE_OBJECT = 7
BLOB_TYPE_INTERFACE = 8
TYPE_VOID = 0
TYPE_BOOLEAN = 1
TYPE_INT8 = 2
TYPE_UINT8 = 3
TYPE_INT16 = 4
TYPE_UINT16 = 5
TYPE_INT32 = 6
TYPE_UINT32 = 7
TYPE_INT64 = 8
TYPE_UINT64 = 9
TYPE_FLOAT = 10
TYPE_DOUBLE = 11
TYPE_GTYPE = 12
TYPE_UTF8 = 13
TYPE_FILENAME = 14
TYPE_ARRAY = 15
TYPE_INTERFACE = 16
TYPE_GLIST = 17
TYPE_GSLIST = 18
TYPE_GHASH = 19
TYPE_ERROR = 20
TYPE_UNICHAR = 21
class Field:
def __init__(self, offset: int, type: str, shift=0, mask=None):
self._offset = offset
self._type = type
if not mask or sys.byteorder == "little":
self._shift = shift
elif self._type == "u8" or self._type == "i8":
self._shift = 8 - (shift + mask)
elif self._type == "u16" or self._type == "i16":
self._shift = 16 - (shift + mask)
else:
self._shift = 32 - (shift + mask)
self._mask = (1 << mask) - 1 if mask else None
self._name = f"{offset}__{type}__{shift}__{mask}"
def __get__(self, typelib: "Typelib", _objtype=None):
if typelib is None:
return self
def shift_mask(n):
n = n >> self._shift
if self._mask:
n = n & self._mask
return n
tl = typelib[self._offset]
if self._type == "u8":
return shift_mask(tl.u8)
elif self._type == "u16":
return shift_mask(tl.u16)
elif self._type == "u32":
return shift_mask(tl.u32)
elif self._type == "i8":
return shift_mask(tl.i8)
elif self._type == "i16":
return shift_mask(tl.i16)
elif self._type == "i32":
return shift_mask(tl.i32)
elif self._type == "pointer":
return tl.header[tl.u32]
elif self._type == "offset":
return tl
elif self._type == "string":
return tl.string
elif self._type == "dir_entry":
return tl.header.dir_entry(tl.u16)
else:
raise CompilerBugError(self._type)
class Typelib:
AS_DIR_ENTRY = Field(0, "dir_entry")
HEADER_N_ENTRIES = Field(0x14, "u16")
HEADER_N_LOCAL_ENTRIES = Field(0x16, "u16")
HEADER_DIRECTORY = Field(0x18, "pointer")
HEADER_N_ATTRIBUTES = Field(0x1C, "u32")
HEADER_ATTRIBUTES = Field(0x20, "pointer")
HEADER_DEPENDENCIES = Field(0x24, "pointer")
HEADER_NAMESPACE = Field(0x2C, "string")
HEADER_NSVERSION = Field(0x30, "string")
HEADER_ENTRY_BLOB_SIZE = Field(0x3C, "u16")
HEADER_FUNCTION_BLOB_SIZE = Field(0x3E, "u16")
HEADER_CALLBACK_BLOB_SIZE = Field(0x40, "u16")
HEADER_SIGNAL_BLOB_SIZE = Field(0x42, "u16")
HEADER_ARG_BLOB_SIZE = Field(0x46, "u16")
HEADER_PROPERTY_BLOB_SIZE = Field(0x48, "u16")
HEADER_FIELD_BLOB_SIZE = Field(0x4A, "u16")
HEADER_VALUE_BLOB_SIZE = Field(0x4C, "u16")
HEADER_ATTRIBUTE_BLOB_SIZE = Field(0x4E, "u16")
HEADER_ENUM_BLOB_SIZE = Field(0x56, "u16")
HEADER_OBJECT_BLOB_SIZE = Field(0x5A, "u16")
HEADER_INTERFACE_BLOB_SIZE = Field(0x5C, "u16")
DIR_ENTRY_BLOB_TYPE = Field(0x0, "u16")
DIR_ENTRY_LOCAL = Field(0x2, "u16", 0, 1)
DIR_ENTRY_NAME = Field(0x4, "string")
DIR_ENTRY_OFFSET = Field(0x8, "pointer")
DIR_ENTRY_NAMESPACE = Field(0x8, "string")
ARG_NAME = Field(0x0, "string")
ARG_TYPE = Field(0xC, "u32")
SIGNATURE_RETURN_TYPE = Field(0x0, "u32")
SIGNATURE_N_ARGUMENTS = Field(0x6, "u16")
SIGNATURE_ARGUMENTS = Field(0x8, "offset")
ATTR_OFFSET = Field(0x0, "u32")
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")
TYPE_BLOB_ARRAY_INNER = Field(0x4, "u32")
BLOB_NAME = Field(0x4, "string")
STRUCT_DEPRECATED = Field(0x2, "u16", 0, 1)
ENUM_DEPRECATED = Field(0x2, "u16", 0, 1)
ENUM_GTYPE_NAME = Field(0x8, "string")
ENUM_N_VALUES = Field(0x10, "u16")
ENUM_N_METHODS = Field(0x12, "u16")
ENUM_VALUES = Field(0x18, "offset")
INTERFACE_DEPRECATED = Field(0x2, "u16", 0, 1)
INTERFACE_GTYPE_NAME = Field(0x8, "string")
INTERFACE_N_PREREQUISITES = Field(0x12, "u16")
INTERFACE_N_PROPERTIES = Field(0x14, "u16")
INTERFACE_N_METHODS = Field(0x16, "u16")
INTERFACE_N_SIGNALS = Field(0x18, "u16")
INTERFACE_N_VFUNCS = Field(0x1A, "u16")
INTERFACE_N_CONSTANTS = Field(0x1C, "u16")
INTERFACE_PREREQUISITES = Field(0x28, "offset")
OBJ_DEPRECATED = Field(0x02, "u16", 0, 1)
OBJ_ABSTRACT = Field(0x02, "u16", 1, 1)
OBJ_FUNDAMENTAL = Field(0x02, "u16", 2, 1)
OBJ_FINAL = Field(0x02, "u16", 3, 1)
OBJ_GTYPE_NAME = Field(0x08, "string")
OBJ_PARENT = Field(0x10, "dir_entry")
OBJ_GTYPE_STRUCT = Field(0x12, "string")
OBJ_N_INTERFACES = Field(0x14, "u16")
OBJ_N_FIELDS = Field(0x16, "u16")
OBJ_N_PROPERTIES = Field(0x18, "u16")
OBJ_N_METHODS = Field(0x1A, "u16")
OBJ_N_SIGNALS = Field(0x1C, "u16")
OBJ_N_VFUNCS = Field(0x1E, "u16")
OBJ_N_CONSTANTS = Field(0x20, "u16")
OBJ_N_FIELD_CALLBACKS = Field(0x22, "u16")
PROP_NAME = Field(0x0, "string")
PROP_DEPRECATED = Field(0x4, "u32", 0, 1)
PROP_READABLE = Field(0x4, "u32", 1, 1)
PROP_WRITABLE = Field(0x4, "u32", 2, 1)
PROP_CONSTRUCT = Field(0x4, "u32", 3, 1)
PROP_CONSTRUCT_ONLY = Field(0x4, "u32", 4, 1)
PROP_TYPE = Field(0xC, "u32")
SIGNAL_DEPRECATED = Field(0x0, "u16", 0, 1)
SIGNAL_DETAILED = Field(0x0, "u16", 5, 1)
SIGNAL_NAME = Field(0x4, "string")
SIGNAL_SIGNATURE = Field(0xC, "pointer")
VALUE_NAME = Field(0x4, "string")
VALUE_VALUE = Field(0x8, "i32")
def __init__(self, typelib_file, offset: int):
self._typelib_file = typelib_file
self._offset = offset
def __getitem__(self, index: int):
return Typelib(self._typelib_file, self._offset + index)
def attr(self, name):
return self.header.attr(self._offset, name)
@property
def header(self) -> "TypelibHeader":
return TypelibHeader(self._typelib_file)
@property
def u8(self) -> int:
"""Gets the 8-bit unsigned int at this location."""
return self._int(1, False)
@property
def u16(self) -> int:
"""Gets the 16-bit unsigned int at this location."""
return self._int(2, False)
@property
def u32(self) -> int:
"""Gets the 32-bit unsigned int at this location."""
return self._int(4, False)
@property
def i8(self) -> int:
"""Gets the 8-bit unsigned int at this location."""
return self._int(1, True)
@property
def i16(self) -> int:
"""Gets the 16-bit unsigned int at this location."""
return self._int(2, True)
@property
def i32(self) -> int:
"""Gets the 32-bit unsigned int at this location."""
return self._int(4, True)
@property
def string(self) -> T.Optional[str]:
"""Interprets the 32-bit unsigned int at this location as a pointer
within the typelib file, and returns the null-terminated string at that
pointer."""
loc = self.u32
if loc == 0:
return None
end = self._typelib_file.find(b"\0", loc)
return self._typelib_file[loc:end].decode("utf-8")
def _int(self, size, signed) -> int:
return int.from_bytes(
self._typelib_file[self._offset : self._offset + size],
sys.byteorder,
signed=signed,
)
class TypelibHeader(Typelib):
def __init__(self, typelib_file):
super().__init__(typelib_file, 0)
def dir_entry(self, index) -> T.Optional[Typelib]:
if index == 0:
return None
else:
return self.HEADER_DIRECTORY[(index - 1) * self.HEADER_ENTRY_BLOB_SIZE]
def attr(self, offset, name):
lower = 0
upper = self.HEADER_N_ATTRIBUTES
attr_size = self.HEADER_ATTRIBUTE_BLOB_SIZE
attrs = self.HEADER_ATTRIBUTES
mid = 0
while lower <= upper:
mid = math.floor((upper + lower) / 2)
attr = attrs[mid * attr_size]
if attr.ATTR_OFFSET < offset:
lower = mid + 1
elif attr.ATTR_OFFSET > offset:
upper = mid - 1
else:
while mid >= 0 and attrs[(mid - 1) * attr_size].ATTR_OFFSET == offset:
mid -= 1
break
if attrs[mid * attr_size].ATTR_OFFSET != offset:
# no match found
return None
while attrs[mid * attr_size].ATTR_OFFSET == offset:
if attrs[mid * attr_size].ATTR_NAME == name:
return attrs[mid * attr_size].ATTR_VALUE
mid += 1
return None
def attr_by_index(self, index):
pass
@property
def dir_entries(self):
return [self.dir_entry(i) for i in range(self[0x16].u16)]
def load_typelib(path: str) -> Typelib:
with open(path, "rb") as f:
return Typelib(f.read(), 0)

View file

@ -18,18 +18,20 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from dataclasses import dataclass
class Colors:
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[33m'
FAINT = '\033[2m'
BOLD = '\033[1m'
BLUE = '\033[34m'
UNDERLINE = '\033[4m'
NO_UNDERLINE = '\033[24m'
CLEAR = '\033[0m'
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[33m"
PURPLE = "\033[35m"
FAINT = "\033[2m"
BOLD = "\033[1m"
BLUE = "\033[34m"
UNDERLINE = "\033[4m"
NO_UNDERLINE = "\033[24m"
CLEAR = "\033[0m"
def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]:
@ -56,7 +58,11 @@ def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]:
cost = 1
else:
cost = 2
distances[i][j] = min(distances[i-1][j] + 2, distances[i][j-1] + 2, distances[i-1][j-1] + cost)
distances[i][j] = min(
distances[i - 1][j] + 2,
distances[i][j - 1] + 2,
distances[i - 1][j - 1] + cost,
)
return distances[m - 1][n - 1]
@ -68,17 +74,18 @@ def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]:
def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]:
if idx == 0:
if idx == 0 or len(text) == 0:
return (0, 0)
sp = text[:idx].splitlines(keepends=True)
line_num = len(sp)
col_num = len(sp[-1])
line_num = text.count("\n", 0, idx) + 1
col_num = idx - text.rfind("\n", 0, idx) - 1
return (line_num - 1, col_num)
def pos_to_idx(line: int, col: int, text: str) -> int:
lines = text.splitlines(keepends=True)
return sum([len(line) for line in lines[:line]]) + col
def idxs_to_range(start: int, end: int, text: str):
start_l, start_c = idx_to_pos(start, text)
end_l, end_c = idx_to_pos(end, text)
@ -92,3 +99,58 @@ def idxs_to_range(start: int, end: int, text: str):
"character": end_c,
},
}
@dataclass
class UnescapeError(Exception):
start: int
end: int
def escape_quote(string: str) -> str:
return (
'"'
+ (
string.replace("\\", "\\\\")
.replace('"', '\\"')
.replace("\n", "\\n")
.replace("\t", "\\t")
)
+ '"'
)
def unescape_quote(string: str) -> str:
string = string[1:-1]
REPLACEMENTS = {
"\n": "\n",
"\\": "\\",
"n": "\n",
"t": "\t",
'"': '"',
"'": "'",
}
result = ""
i = 0
while i < len(string):
c = string[i]
if c == "\\":
i += 1
if i >= len(string):
from .errors import CompilerBugError
raise CompilerBugError()
if r := REPLACEMENTS.get(string[i]):
result += r
else:
raise UnescapeError(i, i + 2)
else:
result += c
i += 1
return result

View file

@ -18,76 +18,82 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from collections import defaultdict
from functools import cached_property
import typing as T
from xml import sax
# To speed up parsing, we ignore all tags except these
PARSE_GIR = set([
"repository", "namespace", "class", "interface", "property", "glib:signal",
"include", "implements", "type", "parameter", "parameters", "enumeration",
"member", "bitfield",
])
PARSE_GIR = set(
[
"repository",
"namespace",
"class",
"interface",
"property",
"glib:signal",
"include",
"implements",
"type",
"parameter",
"parameters",
"enumeration",
"member",
"bitfield",
]
)
class Element:
def __init__(self, tag, attrs: T.Dict[str, str]):
def __init__(self, tag: str, attrs: T.Dict[str, str]):
self.tag = tag
self.attrs = attrs
self.children: T.Dict[str, T.List["Element"]] = defaultdict(list)
self.children: T.List["Element"] = []
self.cdata_chunks: T.List[str] = []
@cached_property
def cdata(self):
return ''.join(self.cdata_chunks)
return "".join(self.cdata_chunks)
def get_elements(self, name) -> T.List["Element"]:
return self.children.get(name, [])
def get_elements(self, name: str) -> T.List["Element"]:
return [child for child in self.children if child.tag == name]
def __getitem__(self, key):
def __getitem__(self, key: str):
return self.attrs.get(key)
class Handler(sax.handler.ContentHandler):
def __init__(self, parse_type):
def __init__(self):
self.root = None
self.stack = []
self.skipping = 0
self._interesting_elements = parse_type
def startElement(self, name, attrs):
if self._interesting_elements is not None and name not in self._interesting_elements:
self.skipping += 1
if self.skipping > 0:
return
element = Element(name, attrs.copy())
if len(self.stack):
last = self.stack[-1]
last.children[name].append(element)
last.children.append(element)
else:
self.root = element
self.stack.append(element)
def endElement(self, name):
if self.skipping == 0:
self.stack.pop()
if self._interesting_elements is not None and name not in self._interesting_elements:
self.skipping -= 1
def characters(self, content):
if not self.skipping:
self.stack[-1].cdata_chunks.append(content)
def parse(filename, parse_type=None):
def parse(filename):
parser = sax.make_parser()
handler = Handler(parse_type)
handler = Handler()
parser.setContentHandler(handler)
parser.parse(filename)
return handler.root
def parse_string(xml):
handler = Handler()
parser = sax.parseString(xml, handler)
return handler.root

View file

@ -1,9 +1,13 @@
FROM fedora:latest
RUN dnf install -y meson python3-pip gtk4-devel gobject-introspection-devel libadwaita-devel python3-devel
RUN pip3 install furo mypy sphinx coverage
RUN dnf install -y meson gcc g++ python3-pip gobject-introspection-devel \
python3-devel python3-gobject git diffutils xorg-x11-server-Xvfb \
appstream-devel dbus-x11 "dnf-command(builddep)" glslc
RUN dnf build-dep -y gtk4 libadwaita
RUN pip3 install furo mypy sphinx coverage black isort
COPY install_deps.sh .
RUN ./install_deps.sh
# The version on PyPI is very old and doesn't install. Use the upstream package registry instead.
RUN pip install pythonfuzz --extra-index-url https://gitlab.com/api/v4/projects/19904939/packages/pypi/simple
RUN dnf install -y git

26
build-aux/install_deps.sh Executable file
View file

@ -0,0 +1,26 @@
#!/bin/bash
set -e
echo "===== Install GTK ====="
git clone --depth=1 https://gitlab.gnome.org/GNOME/gtk.git
cd gtk
meson setup builddir \
--prefix=/usr \
-Ddocumentation=true \
-Dbuild-demos=false \
-Dbuild-examples=false \
-Dbuild-tests=false \
-Dbuild-testsuite=false
ninja -C builddir install
cd -
rm -rf gtk
echo "===== Install libadwaita ====="
git clone --depth=1 https://gitlab.gnome.org/GNOME/libadwaita.git
cd libadwaita
meson builddir \
--prefix=/usr
ninja -C builddir install
cd -
rm -rf libadwaita

24
docs/_static/styles.css vendored Normal file
View file

@ -0,0 +1,24 @@
.experimental-admonition {
display: flex;
align-items: center;
}
.experimental-admonition img {
width: 64px;
}
p.grammar-block {
font-family: var(--font-stack--monospace);
white-space: pre;
overflow: auto;
font-size: var(--code-font-size);
padding: .625rem .875rem;
line-height: 1.5;
background: #f8f8f8;
border-radius: .2rem;
}
body:not([data-theme="light"]) .grammar-block {
background: #202020;
color: #d0d0d0;
}

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

@ -17,9 +17,9 @@
# -- Project information -----------------------------------------------------
project = 'Blueprint'
copyright = '2021, James Westman'
author = 'James Westman'
project = "Blueprint"
copyright = "2021-2023, James Westman"
author = "James Westman"
# -- General configuration ---------------------------------------------------
@ -27,16 +27,15 @@ author = 'James Westman'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
]
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
@ -44,9 +43,11 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'furo'
html_theme = "furo"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
html_css_files = ["styles.css"]

View file

@ -1,420 +0,0 @@
========
Examples
========
Namespaces and libraries
------------------------
GTK declaration
~~~~~~~~~~~~~~~
.. code-block::
// Required in every blueprint file. Defines the major version
// of GTK the file is designed for.
using Gtk 4.0;
Importing libraries
~~~~~~~~~~~~~~~~~~~
.. code-block::
// Import Adwaita 1. The name given here is the GIR namespace name, which
// might not match the library name or C prefix.
using Adw 1;
Objects
-------
Defining objects with properties
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
Gtk.Box {
orientation: vertical;
Gtk.Label {
label: "Hello, world!";
}
}
Referencing an object in code
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
// Your code can reference the object by `my_window`
Gtk.Window my_window {
title: "My window";
}
Using classes defined by your app
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use a leading ``.`` to tell the compiler that the class is defined in your
app, not in the GIR, so it should skip validation.
.. code-block::
.MyAppCustomWidget my_widget {
my-custom-property: 3.14;
}
Templates
---------
Defining a template
~~~~~~~~~~~~~~~~~~~
Many language bindings have a way to create subclasses that are defined both
in code and in the blueprint file. Check your language's documentation on
how to use this feature.
In this example, we create a class called ``MyAppWindow`` that inherits from
``Gtk.ApplicationWindow``.
.. code-block::
template MyAppWindow : Gtk.ApplicationWindow {
my-custom-property: 3.14;
}
Referencing a template object
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to use a template object elsewhere in your blueprint, you can use
the template class name as the object ID:
.. code-block::
template MyAppWindow : ApplicationWindow { }
Gtk.Label {
visible: bind MyAppWindow.visible;
}
Properties
----------
Translations
~~~~~~~~~~~~
Use ``_("...")`` to mark strings as translatable. You can put a comment for
translators on the line above if needed.
.. code-block::
Gtk.Label label {
/* Translators: This is the main text of the welcome screen */
label: _("Hello, world!");
}
Use ``C_("context", "...")`` to add a *message context* to a string to
disambiguate it, in case the same string appears in different places. Remember,
two strings might be the same in one language but different in another depending
on context.
.. code-block::
Gtk.Label label {
/* Translators: This is a section in the preferences window */
label: C_("preferences window", "Hello, world!");
}
Referencing objects by ID
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
Gtk.Range range1 {
adjustment: my_adjustment;
}
Gtk.Range range2 {
adjustment: my_adjustment;
}
Gtk.Adjustment my_adjustment {
}
Defining object properties inline
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
Gtk.Range {
adjustment: Gtk.Adjustment my_adjustment {
value: 10;
};
}
Gtk.Range range1 {
// You can even still reference the object by ID
adjustment: my_adjustment;
}
.. note::
Note the semicolon after the closing brace of the ``Gtk.Adjustment``. It is
required.
Bindings
~~~~~~~~
Use the ``bind`` keyword to bind a property to another object's property in
the same file.
.. code-block::
Gtk.ProgressBar bar1 {
}
Gtk.ProgressBar bar2 {
value: bind bar1.value;
}
Binding Flags
~~~~~~~~~~~~~
Use the ``no-sync-create`` keyword to only update the target value when the
source value changes, not when the binding is first created.
.. code-block::
Gtk.ProgressBar bar1 {
value: 10;
}
Gtk.ProgressBar bar2 {
value: bind bar1.value no-sync-create;
}
Use the ``bidirectional`` keyword to bind properties in both directions.
.. code-block::
// Text of entry1 is bound to text
// of entry2 and vice versa
Gtk.Entry entry1 {
text: bind entry2.text bidirectional;
}
Gtk.Entry entry2 {
}
Use the ``inverted`` keyword to invert to bind a boolean property
to inverted value of another one.
.. code-block::
// When switch1 is on, switch2 will be off
Gtk.Switch switch1 {
active: bind switch2.active inverted bidirectional;
}
// When switch2 is on, switch1 will be off
Gtk.Switch switch2 {
}
Signals
-------
Basic Usage
~~~~~~~~~~~
.. code-block::
Gtk.Button {
// on_button_clicked is defined in your application
clicked => on_button_clicked();
}
Flags
~~~~~
.. code-block::
Gtk.Button {
clicked => on_button_clicked() swapped;
}
Object
~~~~~~
By default the widget is passed to callback as first argument. However,
you can specify another object to use as first argument of callback.
.. code-block::
Gtk.Entry {
activate => grab_focus(another_entry);
}
Gtk.Entry another_entry {
}
CSS Styles
----------
Basic Usage
~~~~~~~~~~~
.. code-block::
Gtk.Label {
styles ["dim-label", "title"]
}
Menus
-----
Basic Usage
~~~~~~~~~~~
.. code-block::
menu my_menu {
section {
label: _("File");
item {
label: _("Open");
action: "win.open";
}
item {
label: _("Save");
action: "win.save";
}
submenu {
label: _("Save As");
item {
label: _("PDF");
action: "win.save_as_pdf";
}
}
}
}
Item Shorthand
~~~~~~~~~~~~~~
For menu items with only a label, action, and/or icon, you can define all three
on one line. The action and icon are optional.
.. code-block::
menu {
item (_("Copy"), "app.copy", "copy-symbolic")
}
Layout Properties
-----------------
Basic Usage
~~~~~~~~~~~
.. code-block::
Gtk.Grid {
Gtk.Label {
layout {
row: 0;
column: 1;
}
}
}
Accessibility Properties
------------------------
Basic Usage
~~~~~~~~~~~
.. code-block::
Gtk.Widget {
accessibility {
orientation: vertical;
labelled-by: my_label;
checked: true;
}
}
Gtk.Label my_label {}
Widget-Specific Items
---------------------
Gtk.ComboBoxText
~~~~~~~~~~~~~~~~
.. code-block::
Gtk.ComboBoxText {
items [
item1: "Item 1",
item2: _("Items can be translated"),
"The item ID is not required",
]
}
Gtk.FileFilter
~~~~~~~~~~~~~~
.. code-block::
Gtk.FileFilter {
mime-types ["image/jpeg", "video/webm"]
patterns ["*.txt"]
suffixes ["png"]
}
Gtk.SizeGroup
~~~~~~~~~~~~~
.. code-block::
Gtk.SizeGroup {
mode: both;
widgets [label1, label2]
}
Gtk.Label label1 {}
Gtk.Label label2 {}
Gtk.StringList
~~~~~~~~~~~~~~
.. code-block::
Gtk.StringList {
strings ["Hello, world!", _("Translated string")]
}
Gtk.Dialog and Gtk.InfoBar
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
Gtk.Dialog {
[action response=ok]
Gtk.Button ok_response {}
[action response=cancel]
Gtk.Button cancel_response {}
[action response=9]
Gtk.Button app_defined_response {}
}

163
docs/experimental.svg Normal file
View file

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="128"
height="128"
viewBox="0 0 33.866666 33.866666"
version="1.1"
id="svg145234"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
sodipodi:docname="beaker.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview145236"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="2.3786088"
inkscape:cx="113.72194"
inkscape:cy="67.26621"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid145800" />
</sodipodi:namedview>
<defs
id="defs145231">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1082">
<path
sodipodi:nodetypes="ccsccccccccccssccccccsccccc"
inkscape:connector-curvature="0"
id="path1084"
d="m 59.306656,179.84113 c -1.539651,-0.0204 -2.554714,0.50293 -3.259361,1.28705 -0.704636,0.78405 -1.080179,1.88075 -1.080179,2.99185 0,1.1112 0.375543,2.20784 1.080179,2.99194 0.704647,0.78412 1.71971,1.30878 3.259361,1.28701 0.596478,-0.008 1.084356,0.47321 1.084359,1.06974 v 31.27782 c 4e-6,0.2286 -0.07322,0.45119 -0.208928,0.63515 l -29.921313,40.60614 c -5.03e-4,7.6e-4 -0.001,0.001 -0.002,0.002 -1.584091,2.39264 -1.933179,5.40515 -1.95979,7.96457 v 0.004 c -2.1e-5,0.002 2.1e-5,0.004 0,0.006 0.005,8.86974 7.174927,16.0356 16.046057,16.0356 H 97.83189 c 8.8738,0 16.04477,-7.17027 16.04594,-16.04397 -0.007,-2.82242 -0.49265,-5.62802 -1.97021,-7.97704 -9.97128,-13.53236 -19.94162,-27.0653 -29.91287,-40.59772 -0.13575,-0.18403 -0.20895,-0.40672 -0.20888,-0.6354 v -31.27771 c 1e-5,-0.59653 0.48788,-1.07782 1.08436,-1.06973 2.40019,0.0329 4.33954,-1.87832 4.33954,-4.27897 0,-2.40065 -1.93937,-4.31292 -4.33954,-4.27891 -0.005,3e-5 -0.01,3e-5 -0.0145,0 H 59.321313 c -0.0049,3e-5 -0.0097,3e-5 -0.01456,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#99c1f1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</clipPath>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter1221"
x="-0.12864004"
width="1.2572789"
y="-0.10374497"
height="1.2074448">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="4.1703178"
id="feGaussianBlur1223" />
</filter>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient1205"
id="radialGradient1271"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.9534006,0,0,0.87645096,-163.67294,29.472168)"
cx="41.498363"
cy="254.6526"
fx="41.498363"
fy="254.6526"
r="34.247517" />
<linearGradient
inkscape:collect="always"
id="linearGradient1205">
<stop
style="stop-color:#33d17a;stop-opacity:1"
offset="0"
id="stop1201" />
<stop
style="stop-color:#3584e4;stop-opacity:1"
offset="1"
id="stop1203" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient1013"
id="radialGradient1273"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(9.0225063,0,0,6.9495164,-619.87454,-1384.9283)"
cx="62"
cy="229.19627"
fx="62"
fy="229.19627"
r="16.015505" />
<linearGradient
inkscape:collect="always"
id="linearGradient1013">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop1009" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop1011" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g425"
transform="matrix(0.26458333,0,0,0.26458333,32.647151,-39.952084)"
style="display:inline;enable-background:new">
<g
transform="translate(-130.47915,-20)"
clip-path="url(#clipPath1082)"
id="g1261">
<path
sodipodi:nodetypes="ccsccccccccccssccccccsccccc"
inkscape:connector-curvature="0"
id="path1255"
d="m 59.306656,179.84113 c -1.539651,-0.0204 -2.554714,0.50293 -3.259361,1.28705 -0.704636,0.78405 -1.080179,1.88075 -1.080179,2.99185 0,1.1112 0.375543,2.20784 1.080179,2.99194 0.704647,0.78412 1.71971,1.30878 3.259361,1.28701 0.596478,-0.008 1.084356,0.47321 1.084359,1.06974 v 31.27782 c 4e-6,0.2286 -0.07322,0.45119 -0.208928,0.63515 l -29.921313,40.60614 c -5.03e-4,7.6e-4 -0.001,0.001 -0.002,0.002 -1.584091,2.39264 -1.933179,5.40515 -1.95979,7.96457 v 0.004 c -2.1e-5,0.002 2.1e-5,0.004 0,0.006 0.005,8.86974 7.174927,16.0356 16.046057,16.0356 H 97.83189 c 8.8738,0 16.04477,-7.17027 16.04594,-16.04397 -0.007,-2.82242 -0.49265,-5.62802 -1.97021,-7.97704 -9.97128,-13.53236 -19.94162,-27.0653 -29.91287,-40.59772 -0.13575,-0.18403 -0.20895,-0.40672 -0.20888,-0.6354 v -31.27771 c 1e-5,-0.59653 0.48788,-1.07782 1.08436,-1.06973 2.40019,0.0329 4.33954,-1.87832 4.33954,-4.27897 0,-2.40065 -1.93937,-4.31292 -4.33954,-4.27891 -0.005,3e-5 -0.01,3e-5 -0.0145,0 H 59.321313 c -0.0049,3e-5 -0.0097,3e-5 -0.01456,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.322;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#99c1f1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.81;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#1c71d8;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.881889;filter:url(#filter1221);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 59.306656,179.84113 c -1.539651,-0.0204 -2.554714,0.50293 -3.259361,1.28705 -0.704636,0.78405 -1.080179,1.88075 -1.080179,2.99185 0,1.1112 0.375543,2.20784 1.080179,2.99194 0.704647,0.78412 1.71971,1.30878 3.259361,1.28701 0.596478,-0.008 1.084356,0.47321 1.084359,1.06974 v 31.27782 c 4e-6,0.2286 -0.07322,0.45119 -0.208928,0.63515 l -29.921313,40.60614 c -5.03e-4,7.6e-4 -0.001,0.001 -0.002,0.002 -1.584091,2.39264 -1.933179,5.40515 -1.95979,7.96457 v 0.004 c -2.1e-5,0.002 2.1e-5,0.004 0,0.006 0.005,8.86974 7.174927,16.0356 16.046057,16.0356 H 97.83189 c 8.8738,0 16.04477,-7.17027 16.04594,-16.04397 -0.007,-2.82242 -0.49265,-5.62802 -1.97021,-7.97704 -9.97128,-13.53236 -19.94162,-27.0653 -29.91287,-40.59772 -0.13575,-0.18403 -0.20895,-0.40672 -0.20888,-0.6354 v -31.27771 c 1e-5,-0.59653 0.48788,-1.07782 1.08436,-1.06973 2.40019,0.0329 4.33954,-1.87832 4.33954,-4.27897 0,-2.40065 -1.93937,-4.31292 -4.33954,-4.27891 -0.005,3e-5 -0.01,3e-5 -0.0145,0 H 59.321313 c -0.0049,3e-5 -0.0097,3e-5 -0.01456,0 z"
id="path1257"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccsccccccccccssccccccsccccc" />
<path
d="m 79,221 h 10 m -10,-4 h 10 m -10,-4 h 10 m -14,-4 h 10 m -6,-4 h 10 m -10,-4 h 10 m -10,-4 h 10 m -14,-4 h 10"
style="fill:none;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.519685"
id="path1259"
inkscape:connector-curvature="0" />
</g>
<path
sodipodi:nodetypes="cccsscccccccc"
inkscape:connector-curvature="0"
id="path1263"
d="m -34.29794,233.66755 8.04173,10.91453 c 0.74491,1.13854 1.09081,3.07356 1.11345,5.02175 -0.009,5.8719 -4.66171,10.51971 -10.53703,10.51971 h -47.42097 c -5.87918,0 -10.53438,-4.65437 -10.53704,-10.53357 0.006,-2.0657 0.38011,-3.80592 1.16367,-5.07197 2.665,-3.61676 5.32998,-7.23362 7.99498,-10.85045 10.97111,12.38168 41.40432,-11.28458 50.18121,0 z m 8.04173,10.90414 c 0.0181,0.0184 0.48159,0.49211 0.74461,0.76019 -0.11172,-0.0775 -0.28578,-0.19921 -0.28572,-0.19914 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#1c71d8;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#radialGradient1271);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m -84.48262,233.66755 -8.04173,10.91453 c -0.7449,1.13854 -1.09081,3.07356 -1.11345,5.02175 0.009,5.8719 4.66171,10.51971 10.53703,10.51971 h 47.42097 c 5.87918,0 10.53438,-4.65437 10.53704,-10.53357 -0.006,-2.0657 -0.38011,-3.80592 -1.16366,-5.07197 -2.66501,-3.61676 -5.32999,-7.23362 -7.99499,-10.85045 -10.97111,12.38168 -41.40431,-11.28458 -50.18121,0 z m -8.04173,10.90414 c -0.0181,0.0184 -0.48159,0.49211 -0.74461,0.76019 0.11172,-0.0775 0.28578,-0.19921 0.28572,-0.19914 z"
id="path1265"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccsscccccccc" />
<path
sodipodi:nodetypes="cssccccsscc"
inkscape:connector-curvature="0"
id="path1267"
d="M -16.64126,250.7207 C -17.04348,259.23501 -24.03184,266 -32.64712,266 h -53.48633 c -8.54507,0 -15.493,-6.65433 -15.99804,-15.07031 -0.0161,0.34274 -0.0454,0.69541 -0.0488,1.02539 v 0.004 c -2e-5,0.002 2e-5,0.004 0,0.006 0.005,8.8695 7.17574,16.03492 16.04687,16.03492 h 53.48633 c 8.8738,0 16.04375,-7.17122 16.04492,-16.04492 -10e-4,-0.41261 -0.016,-0.82386 -0.0391,-1.23438 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.623;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#1a5fb4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
inkscape:connector-curvature="0"
id="path1269"
d="m -66.48305,207.95117 a 4.0004,4.0004 0 0 0 -3.49219,2.10547 l -20,36 a 4.0004,4.0004 0 1 0 6.99219,3.88672 l 20,-36 a 4.0004,4.0004 0 0 0 -3.5,-5.99219 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.48;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#radialGradient1273);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -12,11 +12,12 @@ a module in your flatpak manifest:
{
"name": "blueprint-compiler",
"buildsystem": "meson",
"cleanup": ["*"],
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/jwestman/blueprint-compiler",
"branch": "main"
"url": "https://gitlab.gnome.org/GNOME/blueprint-compiler",
"tag": "v0.16.0"
}
]
}

View file

@ -1,6 +1,14 @@
Overview
========
.. warning::
.. container:: experimental-admonition
.. image:: experimental.svg
**Blueprint is still experimental.** Future versions may have breaking changes, and most GTK tutorials use XML syntax.
Blueprint is a markup language and compiler for GTK 4 user interfaces.
.. toctree::
@ -10,14 +18,15 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces.
setup
translations
flatpak
examples
reference/index
packaging
.. code-block::
.. code-block:: blueprint
using Gtk 4.0;
template MyAppWindow : ApplicationWindow {
template $MyAppWindow: ApplicationWindow {
default-width: 600;
default-height: 300;
title: _("Hello, Blueprint!");
@ -26,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;
}
}
@ -44,31 +53,96 @@ Features
- **Concise syntax.** No more clumsy XML! Blueprint is designed from the ground
up to match GTK's widget model, including templates, child types, signal
handlers, and menus.
- **Easy to learn.** The syntax should be very familiar to most people. Scroll
through the :doc:`examples page <examples>` for a quick overview of the whole
language.
- **Modern tooling.** IDE integration for `GNOME Builder <https://developer.gnome.org/documentation/introduction/builder.html>`_
is in progress, and a VS Code extension is also planned.
Built with Blueprint
--------------------
- `Extension Manager <https://github.com/mjakeman/extension-manager>`_
- `Feeds <https://gitlab.gnome.org/World/gfeeds>`_
- `Geopard <https://github.com/ranfdev/Geopard>`_
- `Giara <https://gitlab.gnome.org/World/giara>`_
- `Health <https://gitlab.gnome.org/World/Health>`_
- `HydraPaper <https://gitlab.com/gabmus/HydraPaper>`_
- `Identity <https://gitlab.gnome.org/YaLTeR/identity>`_
- `Plitki <https://github.com/YaLTeR/plitki>`_
- `Solanum <https://gitlab.gnome.org/World/Solanum>`_
- `Text Pieces <https://github.com/liferooter/textpieces>`_
- `Video Trimmer <https://gitlab.gnome.org/YaLTeR/video-trimmer>`_
- `WhatIP <https://gitlab.gnome.org/GabMus/whatip>`_
- `Workbench <https://github.com/sonnyp/Workbench>`_
- **Easy to learn.** The syntax should be very familiar to most people. Take a look at the :doc:`reference <reference/index>` to get started.
- **Modern tooling.** Blueprint ships a `Language Server <https://microsoft.github.io/language-server-protocol/>`_ for IDE integration.
Links
-----
- `Source code <https://gitlab.gnome.org/jwestman/blueprint-compiler>`_
- `Vim syntax highlighting plugin <https://github.com/thetek42/vim-blueprint-syntax>`_
- `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>`_
- `Vim syntax highlighting plugin by gabmus <https://gitlab.com/gabmus/vim-blueprint>`_
- `GNU Emacs major mode by DrBluefall <https://github.com/DrBluefall/blueprint-mode>`_
- `Visual Studio Code plugin by bodil <https://github.com/bodil/vscode-blueprint>`_
History
-------
1. `Simplify our UI declarative language, a strawman proposal <https://discourse.gnome.org/t/simplify-our-ui-declarative-language-a-strawman-proposal/2913>`_
2. `A Markup Language for GTK <https://www.jwestman.net/2021/10/22/a-markup-language-for-gtk.html>`_
3. `Introducing Blueprint: A New Way to Craft User Interfaces <https://www.jwestman.net/2021/12/02/introducing-blueprint-a-new-way-to-craft-user-interfaces.html>`_
4. `Next Steps for Blueprint <https://www.jwestman.net/2022/04/12/next-steps-for-blueprint.html>`_
Built with Blueprint
--------------------
- `AdwSteamGtk <https://github.com/Foldex/AdwSteamGtk>`_
- `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>`_
- `Design <https://github.com/dubstar-04/Design>`_
- `Dev Toolbox <https://github.com/aleiepure/devtoolbox>`_
- `Dialect <https://github.com/dialect-app/dialect>`_
- `Diccionario de la Lengua <https://codeberg.org/rafaelmardojai/diccionario-lengua>`_
- `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>`_
- `Eyedropper <https://github.com/FineFindus/eyedropper>`_
- `favagtk <https://gitlab.gnome.org/johannesjh/favagtk>`_
- `Feeds <https://gitlab.gnome.org/World/gfeeds>`_
- `File Shredder <https://github.com/ADBeveridge/raider>`_
- `Flare <https://gitlab.com/schmiddi-on-mobile/flare>`_
- `Flowtime <https://github.com/Diego-Ivan/Flowtime>`_
- `Fretboard <https://github.com/bragefuglseth/fretboard>`_
- `Frog <https://github.com/TenderOwl/Frog>`_
- `Geopard <https://github.com/ranfdev/Geopard>`_
- `Giara <https://gitlab.gnome.org/World/giara>`_
- `Girens <https://gitlab.gnome.org/tijder/girens>`_
- `Gradience <https://github.com/GradienceTeam/Gradience>`_
- `Graphs <https://gitlab.gnome.org/World/Graphs>`_
- `Health <https://gitlab.gnome.org/World/Health>`_
- `HydraPaper <https://gitlab.com/gabmus/HydraPaper>`_
- `Identity <https://gitlab.gnome.org/YaLTeR/identity>`_
- `Jogger <https://codeberg.org/baarkerlounger/jogger>`_
- `Junction <https://github.com/sonnyp/Junction/>`_
- `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/>`_
- `Master Key <https://gitlab.com/guillermop/master-key/>`_
- `Misson Center <https://github.com/flathub/io.missioncenter.MissionCenter>`_
- `NewCaw <https://github.com/CodedOre/NewCaw>`_
- `Paper <https://gitlab.com/posidon_software/paper>`_
- `Paper Plane <https://github.com/paper-plane-developers/paper-plane>`_
- `Parabolic <https://github.com/NickvisionApps/Parabolic>`_
- `Passes <https://github.com/pablo-s/passes>`_
- `Pipeline <https://gitlab.com/schmiddi-on-mobile/pipeline>`_
- `Playhouse <https://github.com/sonnyp/Playhouse>`_
- `Plitki <https://github.com/YaLTeR/plitki>`_
- `Raider <https://github.com/ADBeveridge/raider>`_
- `Retro <https://github.com/sonnyp/Retro>`_
- `Solanum <https://gitlab.gnome.org/World/Solanum>`_
- `Sudoku Solver <https://gitlab.com/cyberphantom52/sudoku-solver>`_
- `Swatch <https://gitlab.gnome.org/GabMus/swatch>`_
- `Switcheroo <https://gitlab.com/adhami3310/Switcheroo>`_
- `Tagger <https://github.com/NickvisionApps/Tagger>`_
- `Tangram <https://github.com/sonnyp/Tangram/>`_
- `Text Pieces <https://github.com/liferooter/textpieces>`_
- `Upscaler <https://gitlab.gnome.org/World/Upscaler>`_
- `Video Trimmer <https://gitlab.gnome.org/YaLTeR/video-trimmer>`_
- `Webfont Kit Generator <https://github.com/rafaelmardojai/webfont-kit-generator>`_
- `WhatIP <https://gitlab.gnome.org/GabMus/whatip>`_
- `Who Wants To Be a Millionaire <https://github.com/martinszeltins/who-wants-to-be-a-millionaire/>`_
- `Workbench <https://github.com/sonnyp/Workbench>`_

View file

@ -5,7 +5,15 @@ sphinx = find_program(['sphinx-build-3', 'sphinx-build'], required: true)
custom_target('docs',
command: [sphinx, '-b', 'html', '-c', meson.current_source_dir(), meson.current_source_dir(), '@OUTPUT@'],
output: 'en',
build_by_default: true
build_always_stale: true,
)
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',
)

36
docs/packaging.rst Normal file
View file

@ -0,0 +1,36 @@
====================
For Distro Packagers
====================
blueprint-compiler is a build tool that converts UI definitions written in
Blueprint into XML files that are installed with the app and that GTK can read.
So for most applications that use blueprint-compiler, it is a build dependency.
It is a Python program, but like most GNOME-related projects, it uses
`Meson <https://mesonbuild.com>`_ as its build system.
GObject Introspection
~~~~~~~~~~~~~~~~~~~~~
Blueprint files can import GObject Introspection namespaces like this:
.. code-block:: blueprint
using Gtk 4.0;
using Adw 1;
To compile a blueprint file, ``.typelib`` files for all of the imported
namespaces must be installed. All blueprint files must import Gtk 4.0, so
``Gtk-4.0.typelib`` is effectively a runtime dependency of blueprint-compiler.
blueprint-compiler also depends on pygobject, because it uses GIRepository
to determine the search path for typelib files.
So, if a package uses blueprint-compiler, its build dependencies should include
the typelib files for any namespaces imported in its blueprint files. (Note
that many apps also have the same typelib files as runtime dependencies,
separately from blueprint).
In addition, the blueprint language server uses ``.gir`` files to provide
documentation on hover. Some distros package these files separately from the
main package (e.g. in a ``-devel`` package). The language server will not crash
if these files are not present, but for a good user experience you should make
sure they are installed.

View file

@ -0,0 +1,188 @@
===========
Diagnostics
===========
.. _Diagnostic abstract_class:
abstract_class
--------------
Objects can't be created from abstract classes. Abstract classes are used as base classes for other classes, but they don't have functionality on their own. You may want to use a non-abstract subclass instead.
.. _Diagnostic bad_syntax:
bad_syntax
----------
The tokenizer encountered an unexpected sequence of characters that aren't part of any known blueprint syntax.
.. _Diagnostic child_not_accepted:
child_not_accepted
------------------
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:
conversion_error
----------------
The value's type cannot be converted to the target type.
Subclasses may be converted to their superclasses, but not vice versa. A type that implements an interface can be converted to that interface's type. Many boxed types can be parsed from strings in a type-specific way.
.. _Diagnostic expected_bool:
expected_bool
-------------
A boolean value was expected, but the value is not ``true`` or ``false``.
.. _Diagnostic extension_not_repeatable:
extension_not_repeatable
------------------------
This extension can't be used more than once in an object.
.. _Diagnostic extension_wrong_parent_type:
extension_wrong_parent_type
---------------------------
No extension with the given name exists for this object's class (or, for a :ref:`child extension<Syntax ChildExtension>`, the parent class).
.. _Diagnostic invalid_number_literal:
invalid_number_literal
----------------------
The tokenizer encountered what it thought was a number, but it couldn't parse it as a number.
.. _Diagnostic member_dne:
member_dne
----------
The value is being interpreted as a member of an enum or flag type, but that type doesn't have a member with the given name.
.. _Diagnostic missing_gtk_declaration:
missing_gtk_declaration
-----------------------
All blueprint files must start with a GTK declaration, e.g. ``using Gtk 4.0;``.
.. _Diagnostic multiple_templates:
multiple_templates
------------------
Only one :ref:`template<Syntax Template>` is allowed per blueprint file, but there are multiple. The template keyword indicates which object is the one being instantiated.
.. _Diagnostic namespace_not_found:
namespace_not_found
--------------------
The ``.typelib`` files for the given namespace could not be found. There are several possibilities:
* There is a typo in the namespace name, e.g. ``Adwaita`` instead of ``Adw``
* The version number is incorrect, e.g. ``Adw 1.0`` instead of ``Adw 1``. The library's documentation will tell you the correct version number to use.
* The packages for the library are not installed. On some distributions, the ``.typelib`` file is in a separate package from the main library, such as a ``-devel`` package.
* There is an issue with the path to the typelib file. The ``GI_TYPELIB_PATH`` environment variable can be used to add additional paths to search.
.. _Diagnostic namespace_not_imported:
namespace_not_imported
----------------------
The given namespace was not imported at the top of the file. Importing the namespace is necessary because it tells blueprint-compiler which version of the library to use.
.. _Diagnostic object_dne:
object_dne
----------
No object with the given ID exists in the current scope.
.. _Diagnostic property_dne:
property_dne
------------
The class or interface doesn't have a property with the given name.
.. _Diagnostic property_convert_error:
property_convert_error
----------------------
The value given for the property can't be converted to the property's type.
.. _Diagnostic property_construct_only:
property_construct_only
-----------------------
The property can't be bound because it is a construct-only property, meaning it can only be set once when the object is first constructed. Binding it to an expression could cause its value to change later.
.. _Diagnostic property_read_only:
property_read_only
------------------
This property can't be set because it is marked as read-only.
.. _Diagnostic signal_dne:
signal_dne
----------
The class or interface doesn't have a signal with the given name.
.. _Diagnostic type_dne:
type_dne
--------
The given type doesn't exist in the namespace.
.. _Diagnostic type_not_a_class:
type_not_a_class
----------------
The given type exists in the namespace, but it isn't a class. An object's type must be a concrete (not abstract) class, not an interface or boxed type.
.. _Diagnostic version_conflict:
version_conflict
----------------
This error occurs when two versions of a namespace are imported (possibly transitively) in the same file. For example, this will cause a version conflict:
.. code-block:: blueprint
using Gtk 4.0;
using Gtk 3.0;
But so will this:
.. code-block:: blueprint
using Gtk 4.0;
using Handy 1;
because libhandy imports ``Gtk 3.0``.
.. _Diagnostic wrong_compiler_version:
wrong_compiler_version
----------------------
This version of blueprint-compiler is for GTK 4 blueprints only. Future GTK versions will use different versions of blueprint-compiler.

View file

@ -0,0 +1,88 @@
=======================
Document Root & Imports
=======================
.. _Syntax Root:
Document Root
-------------
.. rst-class:: grammar-block
Root = :ref:`GtkDecl<Syntax GtkDecl>` (:ref:`Using<Syntax Using>`)* (:ref:`TranslationDomain<Syntax TranslationDomain>`)? ( :ref:`Template<Syntax Template>` | :ref:`Menu<Syntax Menu>` | :ref:`Object<Syntax Object>` )* EOF
A blueprint document consists of a :ref:`GTK declaration<Syntax GtkDecl>`, one or more :ref:`imports<Syntax Using>`, and a list of :ref:`objects<Syntax Object>` and/or a :ref:`template<Syntax Template>`.
Example
~~~~~~~
.. code-block:: blueprint
// Gtk Declaration
using Gtk 4.0;
// Import Statement
using Adw 1;
// Object
Window my_window {}
.. _Syntax GtkDecl:
GTK Declaration
---------------
.. rst-class:: grammar-block
GtkDecl = 'using' 'Gtk' '4.0' ';'
Every blueprint file begins with the line ``using Gtk 4.0;``, which declares the target GTK version for the file. Tools that read blueprint files should verify that they support the declared version.
Example
~~~~~~~
.. code-block:: blueprint
using Gtk 4.0;
.. _Syntax Using:
GObject Introspection Imports
-----------------------------
.. rst-class:: grammar-block
Using = 'using' <namespace::ref:`IDENT<Syntax IDENT>`> <version::ref:`NUMBER<Syntax NUMBER>`> ';'
To use classes and types from namespaces other than GTK itself, those namespaces must be imported at the top of the file. This tells the compiler what version of the namespace to import.
You'll need the GIR name and version, not the package name and not the exact version number. These are listed at the top of each library's documentation homepage:
.. image:: gir-namespace.png
The compiler requires typelib files for these libraries to be installed. They are usually installed with the library, but on some distros, you may need to install the package that provides ``{namespace}-{version}.typelib`` (e.g. ``Adw-1.typelib``).
Example
~~~~~~~
.. code-block:: blueprint
// Import libadwaita
using Adw 1;
.. _Syntax TranslationDomain:
Translation Domain
------------------
.. rst-class:: grammar-block
TranslationDomain = 'translation-domain' <domain::ref:`QUOTED<Syntax QUOTED>`> ';'
The translation domain is used to look up translations for translatable strings in the blueprint file. If no translation domain is specified, strings will be looked up in the program's global domain.
See `Gtk.Builder:translation-domain <https://docs.gtk.org/gtk4/property.Builder.translation-domain.html>`_ for more information.

View file

@ -0,0 +1,112 @@
===========
Expressions
===========
Expressions make your user interface code *reactive*. This means when your
application's data changes, the user interface reacts to the change
automatically.
.. code-block:: blueprint
label: bind template.account.username;
/* ^ ^ ^
| creates lookup expressions that are re-evaluated when
| the account's username *or* the account itself changes
|
binds the `label` property to the expression's output
*/
When a value is bound to an expression using the ``bind`` keyword, the binding
monitors all the object properties that are inputs to the expression, and
reevaluates it if any of them change.
This is a powerful tool for ensuring consistency and simplifying your code.
Rather than pushing changes to the user interface wherever they may occur,
you can define your data model with GObject and let GTK take care of the rest.
.. _Syntax Expression:
Expressions
-----------
.. rst-class:: grammar-block
Expression = ( :ref:`ClosureExpression<Syntax ClosureExpression>` | :ref:`Literal<Syntax Literal>` | ( '(' Expression ')' ) ) ( :ref:`LookupExpression<Syntax LookupExpression>` | :ref:`CastExpression<Syntax CastExpression>` )*
.. note::
The grammar above is designed to eliminate `left recursion <https://en.wikipedia.org/wiki/Left_recursion>`_, which can make parsing more complex. In this format, an expression consists of a prefix (such as a literal value or closure invocation) followed by zero or more infix or suffix operators.
Expressions are composed of property lookups and/or closures. Property lookups are the inputs to the expression, and closures provided in application code can perform additional calculations on those inputs.
.. _Syntax LookupExpression:
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>`_.
The type of a property expression is the type of the property it refers to.
.. _Syntax ClosureExpression:
Closures
--------
.. rst-class:: grammar-block
ClosureExpression = '$' <name::ref:`IDENT<Syntax IDENT>`> '(' ( :ref:`Expression<Syntax Expression>` ),* ')'
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, 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:
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. 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 $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

@ -0,0 +1,374 @@
==========
Extensions
==========
.. _Syntax Extension:
Properties are the main way to set values on objects, but they are limited by the GObject type system in what values they can accept. Some classes, therefore, have specialized syntax for certain features.
.. note::
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/GNOME/blueprint-compiler/-/issues>`_.
.. rst-class:: grammar-block
Extension = :ref:`ExtAccessibility<Syntax ExtAccessibility>`
| :ref:`ExtAdwAlertDialog<Syntax ExtAdwAlertDialog>`
| :ref:`ExtAdwMessageDialog<Syntax ExtAdwMessageDialog>`
| :ref:`ExtAdwBreakpoint<Syntax ExtAdwBreakpoint>`
| :ref:`ExtComboBoxItems<Syntax ExtComboBoxItems>`
| :ref:`ExtFileFilterMimeTypes<Syntax ExtFileFilter>`
| :ref:`ExtFileFilterPatterns<Syntax ExtFileFilter>`
| :ref:`ExtFileFilterSuffixes<Syntax ExtFileFilter>`
| :ref:`ExtLayout<Syntax ExtLayout>`
| :ref:`ExtListItemFactory<Syntax ExtListItemFactory>`
| :ref:`ExtSizeGroupWidgets<Syntax ExtSizeGroupWidgets>`
| :ref:`ExtStringListStrings<Syntax ExtStringListStrings>`
| :ref:`ExtStyles<Syntax ExtStyles>`
.. _Syntax ExtAccessibility:
Accessibility Properties
------------------------
.. rst-class:: grammar-block
ExtAccessibility = 'accessibility' '{' ExtAccessibilityProp* '}'
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:
Adw.Breakpoint
--------------
.. rst-class:: grammar-block
ExtAdwBreakpointCondition = 'condition' '(' <condition::ref:`QUOTED<Syntax QUOTED>`> ')'
ExtAdwBreakpoint = 'setters' '{' ExtAdwBreakpointSetter* '}'
ExtAdwBreakpointSetter = <object::ref:`IDENT<Syntax IDENT>`> '.' <property::ref:`IDENT<Syntax IDENT>`> ':' :ref:`Value <Syntax Value>` ';'
Valid in `Adw.Breakpoint <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Breakpoint.html>`_.
Defines the condition for a breakpoint and the properties that will be set at that breakpoint. See the documentation for `Adw.Breakpoint <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Breakpoint.html>`_.
.. note::
The `Adw.Breakpoint:condition <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/property.Breakpoint.condition.html>`_ property has type `Adw.BreakpointCondition <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/struct.BreakpointCondition.html>`_, which GtkBuilder doesn't know how to parse from a string. Therefore, the ``condition`` syntax is used instead.
.. _Syntax ExtAdwAlertDialog:
Adw.AlertDialog Responses
----------------------------
.. rst-class:: grammar-block
ExtAdwAlertDialog = 'responses' '[' (ExtAdwAlertDialogResponse),* ']'
ExtAdwAlertDialogResponse = <id::ref:`IDENT<Syntax IDENT>`> ':' :ref:`StringValue<Syntax StringValue>` ExtAdwAlertDialogFlag*
ExtAdwAlertDialogFlag = 'destructive' | 'suggested' | 'disabled'
Valid in `Adw.AlertDialog <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.AlertDialog.html>`_.
The ``responses`` block defines the buttons that will be added to the dialog. The ``destructive`` or ``suggested`` flag sets the appearance of the button, and the ``disabled`` flag can be used to disable the button.
.. code-block:: blueprint
using Adw 1;
Adw.AlertDialog {
responses [
cancel: _("Cancel"),
delete: _("Delete") destructive,
save: "Save" suggested,
wipeHardDrive: "Wipe Hard Drive" destructive disabled,
]
}
.. _Syntax ExtAdwMessageDialog:
Adw.MessageDialog Responses
----------------------------
.. rst-class:: grammar-block
ExtAdwMessageDialog = 'responses' '[' (ExtAdwMessageDialogResponse),* ']'
ExtAdwMessageDialogResponse = <id::ref:`IDENT<Syntax IDENT>`> ':' :ref:`StringValue<Syntax StringValue>` ExtAdwMessageDialogFlag*
ExtAdwMessageDialogFlag = 'destructive' | 'suggested' | 'disabled'
Valid in `Adw.MessageDialog <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.MessageDialog.html>`_.
The ``responses`` block defines the buttons that will be added to the dialog. The ``destructive`` or ``suggested`` flag sets the appearance of the button, and the ``disabled`` flag can be used to disable the button.
.. code-block:: blueprint
using Adw 1;
Adw.MessageDialog {
responses [
cancel: _("Cancel"),
delete: _("Delete") destructive,
save: "Save" suggested,
wipeHardDrive: "Wipe Hard Drive" destructive disabled,
]
}
.. _Syntax ExtComboBoxItems:
Gtk.ComboBoxText Items
----------------------
.. rst-class:: grammar-block
ExtComboBoxItems = 'items' '[' (ExtComboBoxItem),* ']'
ExtComboBoxItem = ( <id::ref:`IDENT<Syntax IDENT>`> ':' )? :ref:`StringValue<Syntax StringValue>`
Valid in `Gtk.ComboBoxText <https://docs.gtk.org/gtk4/class.ComboBoxText.html>`_, which is deprecated as of Gtk 4.10.
The ``items`` block defines the items that will be added to the combo box. The optional ID can be used to refer to the item rather than its label.
.. code-block:: blueprint
ComboBoxText {
items [
item1: "Item 1",
item2: "Item 2",
item3: "Item 3",
]
}
.. _Syntax ExtFileFilter:
Gtk.FileFilter Filters
----------------------
.. rst-class:: grammar-block
ExtFileFilterMimeTypes = 'mime-types' '[' (ExtFileFilterItem),* ']'
ExtFileFilterPatterns = 'patterns' '[' (ExtFileFilterItem),* ']'
ExtFileFilterSuffixes = 'suffixes' '[' (ExtFileFilterItem),* ']'
ExtFileFilterItem = <item::ref:`QUOTED<Syntax QUOTED>`>
Valid in `Gtk.FileFilter <https://docs.gtk.org/gtk4/class.FileFilter.html>`_.
The ``mime-types``, ``patterns``, and ``suffixes`` blocks define the items that will be added to the file filter. The ``mime-types`` block accepts mime types (including wildcards for subtypes, such as ``image/*``). The ``patterns`` block accepts glob patterns, and the ``suffixes`` block accepts file extensions.
.. code-block:: blueprint
FileFilter {
mime-types [ "text/plain", "image/*" ]
patterns [ "*.txt" ]
suffixes [ "png", "jpg" ]
}
.. _Syntax ExtLayout:
Widget Layouts
--------------
.. rst-class:: grammar-block
ExtLayout = 'layout' '{' ExtLayoutProp* '}'
ExtLayoutProp = <name::ref:`IDENT<Syntax IDENT>`> ':' :ref:`Value<Syntax Value>` ';'
Valid in `Gtk.Widget <https://docs.gtk.org/gtk4/class.Widget.html>`_.
The ``layout`` block describes how the widget should be positioned within its parent. The available properties depend on the parent widget's layout manager.
.. code-block:: blueprint
Grid {
Button {
layout {
column: 0;
row: 0;
}
}
Button {
layout {
column: 1;
row: 0;
}
}
Button {
layout {
column: 0;
row: 1;
row-span: 2;
}
}
}
.. _Syntax ExtListItemFactory:
Gtk.BuilderListItemFactory Templates
------------------------------------
.. rst-class:: grammar-block
ExtListItemFactory = 'template' :ref:`TypeName<Syntax TypeName>` :ref:`ObjectContent<Syntax Object>`
Valid in `Gtk.BuilderListItemFactory <https://docs.gtk.org/gtk4/class.BuilderListItemFactory.html>`_.
The ``template`` block defines the template that will be used to create list items. This block is unique within Blueprint because it defines a completely separate sub-blueprint which is used to create each list item. The sub-blueprint may not reference objects in the main blueprint or vice versa.
The template type must be `Gtk.ListItem <https://docs.gtk.org/gtk4/class.ListItem.html>`_, `Gtk.ColumnViewRow <https://docs.gtk.org/gtk4/class.ColumnViewRow.html>`_, or `Gtk.ColumnViewCell <https://docs.gtk.org/gtk4/class.ColumnViewCell.html>`_. The template object can be referenced with the ``template`` keyword.
.. code-block:: blueprint
ListView {
factory: BuilderListItemFactory {
template ListItem {
child: Label {
label: bind template.item as <StringObject>.string;
};
}
};
model: NoSelection {
model: StringList {
strings [ "Item 1", "Item 2", "Item 3" ]
};
};
}
.. _Syntax ExtScaleMarks:
Gtk.Scale Marks
---------------
.. rst-class:: grammar-block
ExtScaleMarks = 'marks' '[' (ExtScaleMark),* ']'
ExtScaleMark = 'mark' '(' ( '-' | '+' )? <value::ref:`NUMBER<Syntax NUMBER>`> ( ',' <position::ref:`IDENT<Syntax IDENT>`> ( ',' :ref:`StringValue<Syntax StringValue>` )? )? ')'
Valid in `Gtk.Scale <https://docs.gtk.org/gtk4/class.Scale.html>`_.
The ``marks`` block defines the marks on a scale. A single ``mark`` has up to three arguments: a value, an optional position, and an optional label. The position can be ``left``, ``right``, ``top``, or ``bottom``. The label may be translated.
.. _Syntax ExtSizeGroupWidgets:
Gtk.SizeGroup Widgets
---------------------
.. rst-class:: grammar-block
ExtSizeGroupWidgets = 'widgets' '[' (ExtSizeGroupWidget),* ']'
ExtSizeGroupWidget = <id::ref:`IDENT<Syntax IDENT>`>
Valid in `Gtk.SizeGroup <https://docs.gtk.org/gtk4/class.SizeGroup.html>`_.
The ``widgets`` block defines the widgets that will be added to the size group.
.. code-block:: blueprint
Box {
Button button1 {}
Button button2 {}
}
SizeGroup {
widgets [button1, button2]
}
.. _Syntax ExtStringListStrings:
Gtk.StringList Strings
----------------------
.. rst-class:: grammar-block
ExtStringListStrings = 'strings' '[' (ExtStringListItem),* ']'
ExtStringListItem = :ref:`StringValue<Syntax StringValue>`
Valid in `Gtk.StringList <https://docs.gtk.org/gtk4/class.StringList.html>`_.
The ``strings`` block defines the strings in the string list.
.. code-block:: blueprint
StringList {
strings ["violin", "guitar", _("harp")]
}
.. _Syntax ExtStyles:
CSS Styles
----------
.. rst-class:: grammar-block
ExtStyles = 'styles' '[' ExtStylesProp* ']'
ExtStylesProp = <name::ref:`QUOTED<Syntax QUOTED>`>
Valid in any `Gtk.Widget <https://docs.gtk.org/gtk4/class.Widget.html>`_.
The ``styles`` block defines CSS classes that will be added to the widget.
.. code-block:: blueprint
Button {
styles ["suggested-action"]
}
.. _Syntax ChildExtension:
Child Extensions
----------------
.. rst-class:: grammar-block
ChildExtension = :ref:`ExtResponse<Syntax ExtResponse>`
Child extensions are similar to regular extensions, but they apply to a child of the object rather than the object itself. They are used to add properties to child widgets of a container, such as the buttons in a `Gtk.Dialog <https://docs.gtk.org/gtk4/class.Dialog.html>`_. The child extension takes the place of a child type inside the square brackets.
Currently, the only child extension is :ref:`ExtResponse<Syntax ExtResponse>`.
.. _Syntax ExtResponse:
Dialog & InfoBar Responses
--------------------------
.. rst-class:: grammar-block
ExtResponse = 'action' 'response' '=' ( <name::ref:`IDENT<Syntax IDENT>`> | <id::ref:`NUMBER<Syntax NUMBER>`> ) 'default'?
Valid as a child extension for children of `Gtk.Dialog <https://docs.gtk.org/gtk4/class.Dialog.html>`_ or `Gtk.InfoBar <https://docs.gtk.org/gtk4/class.InfoBar.html>`_, which are both deprecated as of Gtk 4.10.
The ``action response`` extension sets the ``action`` child type for the child and sets the child's integer response type. The response type may be either a member of the `Gtk.ResponseType <https://docs.gtk.org/gtk4/enum.ResponseType.html>`_ enum or a positive, application-defined integer.
No more than one child of a dialog or infobar may have the ``default`` flag.
.. code-block:: blueprint
Dialog {
[action response=ok default]
Button {}
[action response=cancel]
Button {}
[action response=1]
Button {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

50
docs/reference/index.rst Normal file
View file

@ -0,0 +1,50 @@
================
Syntax Reference
================
This is the official specification of the blueprint format.
The grammar is expressed as a `parsing expression grammar <https://en.wikipedia.org/wiki/Parsing_expression_grammar>`_. This has two important implications: the parser will never backtrack, and alternation (e.g. a ``|`` in the specification) will always take the *first* branch that matches, even if that causes an error later. These properties make PEGs both unambiguous and simple to implement in code.
Blueprint uses C-style line comments (``// comment for the rest of the line``) and block comments (``/* multiline comment... */``).
Wherever commas are used as delimiters in repetition (expressed in this reference as ``( <rule> ),*``), the trailing comma is permitted and optional.
.. toctree::
:maxdepth: 1
document_root
objects
templates
values
expressions
menus
extensions
diagnostics
Tokens
------
.. _Syntax IDENT:
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 and signals are traditionally kebab-case.
.. _Syntax NUMBER:
NUMBER
~~~~~~
Numbers begin with an ASCII digit and consist of ASCII digits, underscores, dots ``.``, and letters (for radix pre-/suffixes). More than one dot in a number is not allowed. Underscores are permitted for increased readability, and are ignored.
Hexadecimal numbers may be specified using the ``0x`` prefix and may use uppercase or lowercase letters, or a mix. Hexadecimal values may not have a fractional part. They are generally converted to decimal in the output.
.. _Syntax QUOTED:
QUOTED
~~~~~~
Quotes begin with an ASCII single quote ``'`` or double quote ``"`` and end with the same character they started with. An ASCII backslash ``\`` begins an escape sequence; this allows newlines ``\n``, tabs ``\t``, and quotes ``\'``, ``\"`` to be inserted. It also allows multiline strings by escaping a newline character, which will be ignored.

62
docs/reference/menus.rst Normal file
View file

@ -0,0 +1,62 @@
=====
Menus
=====
.. _Syntax Menu:
Menus
-----
.. rst-class:: grammar-block
Menu = 'menu' <id::ref:`IDENT<Syntax IDENT>`>? '{' MenuChild* '}'
MenuChild = ( MenuSection | MenuSubmenu | :ref:`MenuItemShorthand<Syntax MenuItemShorthand>` | MenuItem )
MenuSection = 'section' <id::ref:`IDENT<Syntax IDENT>`>? '{' ( MenuChild | MenuAttribute )* '}'
MenuSubmenu = 'submenu' <id::ref:`IDENT<Syntax IDENT>`>? '{' ( MenuChild | MenuAttribute )* '}'
MenuItem = 'item' '{' MenuAttribute* '}'
MenuAttribute = <name::ref:`IDENT<Syntax IDENT>`> ':' :ref:`StringValue<Syntax StringValue>` ';'
Menus, such as the application menu, are defined using the ``menu`` keyword. Menus have the type `Gio.MenuModel <https://docs.gtk.org/gio/class.MenuModel.html>`_ and can be referenced by ID. They cannot be defined inline.
Example
~~~~~~~
.. code-block:: blueprint
menu my_menu {
submenu {
label: _("File");
item {
label: _("New");
action: "app.new";
icon: "document-new-symbolic";
}
}
}
MenuButton {
menu-model: my_menu;
}
.. _Syntax MenuItemShorthand:
Item Shorthand
--------------
.. rst-class:: grammar-block
MenuItemShorthand = 'item' '(' :ref:`StringValue<Syntax StringValue>` ( ',' ( :ref:`StringValue<Syntax StringValue>` ( ',' :ref:`StringValue<Syntax StringValue>`? )? )? )? ')'
The most common menu attributes are ``label``, ``action``, and ``icon``. Because they're so common, Blueprint provides a shorter syntax for menu items with just these properties.
Example
~~~~~~~
.. code-block:: blueprint
menu {
item ("label")
item ("label", "action")
item ("label", "action", "icon")
}

190
docs/reference/objects.rst Normal file
View file

@ -0,0 +1,190 @@
=======
Objects
=======
.. _Syntax Object:
Objects
-------
.. rst-class:: grammar-block
Object = :ref:`TypeName<Syntax TypeName>` <id::ref:`IDENT<Syntax IDENT>`>? ObjectContent
ObjectContent = '{' (:ref:`Signal<Syntax Signal>` | :ref:`Property<Syntax Property>` | :ref:`Extension<Syntax Extension>` | :ref:`Child<Syntax Child>`)* '}'
Objects are the basic building blocks of a GTK user interface. Your widgets are all objects, as are some other features such as list models.
Optionally, objects may have an ID to provide a handle for other parts of the blueprint and your code to access objects.
.. note::
Object IDs must be unique within their scope. The document root is a scope, but :ref:`sub-templates<Syntax ExtListItemFactory>` have their own, isolated scope.
Example
~~~~~~~
.. code-block:: blueprint
Label label1 {
label: "Hello, world!";
}
Label label2 {
label: bind-property file.name;
}
.. _Syntax TypeName:
Type Names
----------
.. rst-class:: grammar-block
TypeName = TypeNameFull | TypeNameExternal | TypeNameShort
TypeNameFull = <namespace::ref:`IDENT<Syntax IDENT>`> '.' <name::ref:`IDENT<Syntax IDENT>`>
TypeNameExternal = '$' <name::ref:`IDENT<Syntax IDENT>`>
TypeNameShort = <name::ref:`IDENT<Syntax IDENT>`>
There are three forms of type names: full, short, and external. Full type names take the form ``{namespace}.{name}``, e.g. ``Gtk.ApplicationWindow`` or ``Adw.Leaflet``. Because GTK types are so common, the Gtk namespace may be omitted, shortening ``Gtk.ApplicationWindow`` to just ``ApplicationWindow``.
External type names refer to types defined in your application. They are prefixed with ``$`` and do not have a dot between the namespace and class name. In fact, anywhere a ``$`` is used in a blueprint, it refers to something that must be defined in your application code.
.. _Syntax Property:
Properties
----------
.. rst-class:: grammar-block
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.
Most properties are static and directly specified in the blueprint, but properties can also be bound to a data model using the ``bind`` or ``bind-property`` keywords.
A property's value can be another object, either inline or referenced by ID.
Example
~~~~~~~
.. code-block:: blueprint
Label {
label: "text";
}
Button {
/* Inline object value. Notice the semicolon after the object. */
child: Image {
/* ... */
};
}
.. _Syntax Signal:
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' | 'not-swapped'
Signals are one way to respond to user input (another is `actions <https://docs.gtk.org/gtk4/actions.html>`_, which use the `action-name property <https://docs.gtk.org/gtk4/property.Actionable.action-name.html>`_).
Signals provide a handle for your code to listen to events in the UI. The handler name is prefixed with ``$`` to indicate that it's an external symbol which needs to be provided by your code; if it isn't, things might not work correctly, or at all.
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
~~~~~~~
.. code-block:: blueprint
Button {
clicked => $on_button_clicked();
}
.. _Syntax Child:
Children
--------
.. rst-class:: grammar-block
Child = ChildAnnotation? :ref:`Object<Syntax Object>`
ChildAnnotation = '[' ( ChildInternal | :ref:`ChildExtension<Syntax ChildExtension>` | ChildType ) ']'
ChildInternal = 'internal-child' <internal-child::ref:`IDENT<Syntax IDENT>`>
ChildType = <child_type::ref:`IDENT<Syntax IDENT>`>
Some objects can have children. This defines the hierarchical structure of a user interface: containers contain widgets, which can be other containers, and so on.
Child annotations are defined by the parent widget. Some widgets, such as `HeaderBar <https://docs.gtk.org/gtk4/class.HeaderBar.html>`_, have "child types" which allow different child objects to be treated in different ways. Some, such as `Dialog <https://docs.gtk.org/gtk4/class.Dialog.html>`_ and `InfoBar <https://docs.gtk.org/gtk4/class.InfoBar.html>`_, define child :ref:`extensions<Syntax ChildExtension>`, which provide more detailed information about the child.
Internal children are a special case. Rather than creating a new object, children marked with ``[internal-child <name>]`` modify an existing object provided by the parent. This is used, for example, for the ``content_area`` of a `Dialog <https://docs.gtk.org/gtk4/class.Dialog.html>`_.
.. note::
The objects at the root of a blueprint cannot have child annotations, since there is no root widget for them to be a child of.
.. note::
Some widgets, like `Button <https://docs.gtk.org/gtk4/class.Button.html>`_, use a property to set their child instead. Widgets added in this way don't have child annotations.
Examples
~~~~~~~~
Add children to a container
+++++++++++++++++++++++++++
.. code-block:: blueprint
Button {
Image {}
}
Child types
+++++++++++
.. code-block:: blueprint
HeaderBar {
[start]
Label {
}
[end]
Button {
}
}
Child extensions
++++++++++++++++
.. code-block:: blueprint
Dialog {
// Here, a child extension annotation defines the button's response.
[action response=cancel]
Button {}
}
Internal children
+++++++++++++++++
.. code-block:: blueprint
Dialog {
[internal-child content_area]
Box {
// Unlike most objects in a blueprint, this internal-child widget
// represents the properties, signal handlers, children, and extensions
// of an existing Box created by the Dialog, not a new Box created by
// the blueprint.
}
}

View file

@ -0,0 +1,96 @@
===================
Composite Templates
===================
.. _Syntax Template:
Composite Templates
-------------------
.. rst-class:: grammar-block
Template = 'template' :ref:`TypeName<Syntax TypeName>` ( ':' :ref:`TypeName<Syntax TypeName>` )? :ref:`ObjectContent<Syntax Object>`
Widget subclassing is one of the primary techniques for structuring an application. For example, a maps app might have a `Gtk.ApplicationWindow <https://docs.gtk.org/gtk4/class.ApplicationWindow.html>`_ subclass, ``MapsApplicationWindow``, that implements the functionality of its main window. But a maps app has a lot of functionality, so the headerbar might be split into its own `Gtk.HeaderBar <https://docs.gtk.org/gtk4/class.HeaderBar.html>`_ subclass, ``MapsHeaderBar``, for the sake of organization.
You could implement this with the following blueprint:
.. code-block:: blueprint
using Gtk 4.0;
$MapsApplicationWindow window {
$MapsHeaderBar {
/* probably a lot of buttons ... */
}
$MapsMainView {
/* a lot more UI definitions ... */
}
}
There are two problems with this approach:
1. The widget code may be organized neatly into different files, but the UI is not. This blueprint contains the entire UI definition for the app.
2. Widgets aren't in control of their own contents. It shouldn't be up to the caller to construct a widget using the correct blueprint--that's an implementation detail of the widget.
We can solve these problems by giving each widget its own blueprint file, which we reference in the widget's constructor. Then, whenever the widget is instantiated (by another blueprint, or by the application), it will get all the children and properties defined in its blueprint.
For this to work, we need to specify in the blueprint which object is the one being instantiated. We do this with a template block:
.. code-block:: blueprint
using Gtk 4.0;
template $MapsHeaderBar : Gtk.HeaderBar {
/* probably a lot of buttons ... */
}
Gio.ListStore bookmarked_places_store {
/* This isn't the object being instantiated, just an auxillary object. GTK knows this because it isn't the
one marked with 'template'. */
}
This blueprint can only be used by the ``MapsHeaderBar`` constructor. Instantiating it with ``Gtk.Builder`` won't work since it needs an existing, under-construction ``MapsHeaderBar`` to use for the template object. The ``template`` block must be at the top level of the file (not nested within another object) and only one is allowed per file.
This ``MapsHeaderBar`` class, along with its blueprint template, can then be referenced in another blueprint:
.. code-block:: blueprint
using Gtk 4.0;
ApplicationWindow {
$MapsHeaderBar {
/* Nothing needed here, the widgets are in the MapsHeaderBar template. */
}
}
Type & Parent Parameters
~~~~~~~~~~~~~~~~~~~~~~~~
The type name that directly follows the ``template`` keyword is the type of the template class. In most cases, this will be an extern type starting with ``$`` and matching the class name in the application code. Templates for use in a `Gtk.BuilderListItemFactory <https://docs.gtk.org/gtk4/class.BuilderListItemFactory.html>`_ use ``ListItem`` as the type name instead.
The parent type is optional, and may only be present if the template type is extern. It enables limited type checking for the properties and signals of the template object.
Referencing a Template
----------------------
To reference the template object in a binding or expression, use the ``template`` keyword:
.. code-block:: blueprint
template $MyTemplate {
prop1: "Hello, world!";
prop2: bind template.prop1;
}
Language Implementations
------------------------
- **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

181
docs/reference/values.rst Normal file
View file

@ -0,0 +1,181 @@
======
Values
======
.. _Syntax Value:
Values
------
.. rst-class:: grammar-block
Value = :ref:`Translated<Syntax Translated>` | :ref:`Flags<Syntax Flags>` | :ref:`Literal<Syntax Literal>`
.. _Syntax Literal:
Literals
--------
.. rst-class:: grammar-block
Literal = :ref:`TypeLiteral<Syntax TypeLiteral>` | QuotedLiteral | NumberLiteral | IdentLiteral
QuotedLiteral = <value::ref:`QUOTED<Syntax QUOTED>`>
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, ``null``, types, boolean values, or enum members.
.. _Syntax TypeLiteral:
Type Literals
-------------
.. rst-class:: grammar-block
TypeLiteral = 'typeof' '<' :ref:`TypeName<Syntax TypeName>` '>'
Sometimes, you need to specify a type as a value. For example, when creating a list store, you may need to specify the type of the items in the list store. This is done using a ``typeof<>`` literal.
The type of a ``typeof<>`` literal is `GType <https://docs.gtk.org/gobject/alias.Type.html>`_, GObject's "meta-type" for type information.
Example
~~~~~~~
.. code-block:: blueprint
Gio.ListStore {
item-type: typeof<GObject.Object>;
}
.. _Syntax Flags:
Flags
-----
.. rst-class:: grammar-block
Flags = <first::ref:`IDENT<Syntax IDENT>`> '|' ( <rest::ref:`IDENT<Syntax IDENT>`> )|+
Flags are used to specify a set of options. One or more of the available flag values may be specified, and they are combined using ``|``.
Example
~~~~~~~
.. code-block:: blueprint
Adw.TabView {
shortcuts: control_tab | control_shift_tab;
}
.. _Syntax Translated:
Translated Strings
------------------
.. rst-class:: grammar-block
Translated = ( '_' '(' <string::ref:`QUOTED<Syntax QUOTED>`> ')' ) | ( '\C_' '(' <context::ref:`QUOTED<Syntax QUOTED>`> ',' <string::ref:`QUOTED<Syntax QUOTED>`> ')' )
Use ``_("...")`` to mark strings as translatable. You can put a comment for translators on the line above if needed.
.. code-block:: blueprint
Gtk.Label label {
/* Translators: This is the main text of the welcome screen */
label: _("Hello, world!");
}
Use ``C_("context", "...")`` to add a *message context* to a string to disambiguate it, in case the same string appears in different places. Remember, two strings might be the same in one language but different in another depending on context.
.. code-block:: blueprint
Gtk.Label label {
/* Translators: This is a section in the preferences window */
label: C_("preferences window", "Hello, world!");
}
.. _Syntax Binding:
Bindings
--------
.. rst-class:: grammar-block
Binding = 'bind' :ref:`Expression<Syntax Expression>` (BindingFlag)*
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.
The simplest bindings connect to a property of another object in the blueprint. When that other property changes, the bound property updates as well. More advanced bindings can do multi-step property lookups and can even call application code to compute values. See :ref:`the expressions page<Syntax Expression>`.
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:
- ``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
~~~~~~~~~~~~~~~~
Bindings with more complex expressions are implemented with `Gtk.Expression <https://docs.gtk.org/gtk4/class.Expression.html>`_. These bindings do not support flags.
Example
~~~~~~~
.. code-block:: blueprint
/* Use bindings to show a label when a switch
* is active, without any application code */
Switch show_label {}
Label {
visible: bind show_label.active;
label: _("I'm a label that's only visible when the switch is enabled!");
}
.. _Syntax ObjectValue:
Object Values
-------------
.. rst-class:: grammar-block
ObjectValue = :ref:`Object<Syntax Object>`
The value of a property can be an object, specified inline. This is particularly useful for widgets that use a ``child`` property rather than a list of child widgets. Objects constructed in this way can even have IDs and be referenced in other places in the blueprint.
Such objects cannot have child annotations because they aren't, as far as blueprint is concerned, children of another object.
.. _Syntax StringValue:
String Values
-------------
.. rst-class:: grammar-block
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

@ -5,7 +5,7 @@ Translations
Blueprint files can be translated with xgettext. To mark a string as translated,
use the following syntax:
.. code-block::
.. code-block:: blueprint
_("translated string")
@ -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
--------
@ -34,7 +36,7 @@ conflicts. Two strings that are the same in English, but appear in different
contexts, might be different in another language! To disambiguate, use ``C_``
instead of ``_`` and add a context string as the first argument:
.. code-block::
.. code-block:: blueprint
C_("shortcuts window", "Quit")

22
justfile Normal file
View file

@ -0,0 +1,22 @@
default: black isort
# Format with black formatter
black:
black ./
# Sort imports using isort
isort:
isort ./ --profile black
# Run all tests
test: mypy unittest
# Check typings with mypy
mypy:
mypy --python-version=3.9 blueprintcompiler/
# Test code with unittest
unittest:
python3 -m unittest

View file

@ -1,14 +1,14 @@
project('blueprint-compiler',
version: '0.2.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',
@ -17,22 +17,29 @@ configure_file(
install_dir: join_paths(datadir, 'pkgconfig'),
)
config = configuration_data({
'VERSION': meson.project_version(),
'LIBDIR': get_option('prefix') / get_option('libdir'),
})
if meson.is_subproject()
config.set('MODULE_PATH', meson.current_source_dir())
else
config.set('MODULE_PATH', py.get_install_dir())
endif
blueprint_compiler = configure_file(
input: 'blueprint-compiler.py',
output: 'blueprint-compiler',
configuration: {
'VERSION': meson.project_version(),
},
configuration: config,
install: not meson.is_subproject(),
install_dir: get_option('bindir'),
)
# Don't use the output configure_file here--that file is in the build directory
# and won't be able to find the python modules in the source directory.
meson.override_find_program('blueprint-compiler', find_program('blueprint-compiler.py'))
if not meson.is_subproject()
install_subdir('blueprintcompiler', install_dir: datadir / 'blueprint-compiler')
if meson.is_subproject()
meson.override_find_program('blueprint-compiler', blueprint_compiler)
else
install_subdir('blueprintcompiler', install_dir: py.get_install_dir())
endif
subdir('tests')

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,69 @@
using Gtk 4.0;
using Adw 1;
template $MyTemplate: Label {
/**
* A list of strings.
*/
StringList {
// comment
strings [
"Hello",
C_("Greeting", "World"),
]
}
object: Button {
label: "Click me";
};
flags: a | b;
[child]
Label {}
[child]
Label label2 {}
// Single line comment.
/**
* Multiline comment.
*/
// Single line comment.
value: bind (1.0) as <double>;
as: 1;
signal => $on_signal() after; // Inline comment
type_value: typeof<$MyTemplate>;
}
Dialog {
[action response=ok]
$MyButton {}
}
menu menu {
item ("test")
item {
label: "test";
}
item ("test")
}
Adw.MessageDialog {
responses [
save: "Save" suggested disabled,
]
}
Adw.Breakpoint {
condition ("width < 100")
setters {
label2.label: _("Hello, world!");
label2.visible: false;
label2.extra-menu: null;
}
}

1
tests/formatting/in1.blp Normal file
View file

@ -0,0 +1 @@
using Gtk 4.0;using Adw 1;Overlay{Label label{label:_("'Hello World!' \"\n\t\"");}[overlay]Button{notify::icon-name=>$on_icon_name_changed(label)swapped;styles["destructive"]}visible:bind $isVisible(label.visible,my-menu)as<bool>;width-request:bind label.width-request no-sync-create;}menu my-menu{item(_("Label"), "action-name", "icon-name")item{action:"win.format";}}

40
tests/formatting/in2.blp Normal file
View file

@ -0,0 +1,40 @@
using Gtk 4.0;
using Adw 1;
Overlay {
Label
label
{
label
:
_
(
"'Hello World!' \"\n\t\""
)
;
}
[
overlay
] Button
{ notify
:: icon-name
=> $ on_icon_name_changed ( label )
swapped ;
styles
[ "destructive" ]
}
visible
: bind $ isVisible ( label.visible ,
my-menu ) as
< bool > ; width-request : bind label . width-request no-sync-create ; }
menu my-menu
{ item ( _ ( "Label" ) , "action-name" , "icon-name" ) item { action : "win.format" ; } }

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",
]
}

28
tests/formatting/out.blp Normal file
View file

@ -0,0 +1,28 @@
using Gtk 4.0;
using Adw 1;
Overlay {
Label label {
label: _("'Hello World!' \"\n\t\"");
}
[overlay]
Button {
notify::icon-name => $on_icon_name_changed(label) swapped;
styles [
"destructive",
]
}
visible: bind $isVisible(label.visible, my-menu) as <bool>;
width-request: bind label.width-request no-sync-create;
}
menu my-menu {
item (_("Label"), "action-name", "icon-name")
item {
action: "win.format";
}
}

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Label {
label: "\"'\'\t\n\\'";
}

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Label {
label: "\"'\'\t\n\\'";
}

View file

@ -1,13 +1,22 @@
import os, sys
import os
import sys
from pythonfuzz.main import PythonFuzz
from blueprintcompiler.outputs.xml import XmlOutput
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from blueprintcompiler import tokenizer, parser, decompiler
from blueprintcompiler import decompiler, gir, parser, tokenizer, utils
from blueprintcompiler.completions import complete
from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError, CompilerBugError
from blueprintcompiler.errors import (
CompileError,
CompilerBugError,
MultipleErrors,
PrintableError,
)
from blueprintcompiler.tokenizer import Token, TokenType, tokenize
from blueprintcompiler import utils
@PythonFuzz
def fuzz(buf):
@ -17,8 +26,9 @@ def fuzz(buf):
tokens = tokenizer.tokenize(blueprint)
ast, errors, warnings = parser.parse(tokens)
if errors is None and len(ast.errors) == 0:
actual = ast.generate()
xml = XmlOutput()
if errors is None and ast is not None:
xml.emit(ast)
except CompilerBugError as e:
raise e
except PrintableError:
@ -26,5 +36,10 @@ def fuzz(buf):
except UnicodeDecodeError:
pass
if __name__ == "__main__":
# Make sure Gtk 4.0 is accessible, otherwise every test will fail on that
# and nothing interesting will be tested
gir.get_namespace("Gtk", "4.0")
fuzz()

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

@ -1 +1 @@
5,18,1,Cannot convert 1 to Gtk.Orientation
5,18,1,Cannot convert number to Gtk.Orientation

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