Compare commits

...

242 commits
v0.6.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
349 changed files with 10394 additions and 2485 deletions

2
.gitignore vendored
View file

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

View file

@ -3,22 +3,23 @@ stages:
- pages - pages
build: build:
image: registry.gitlab.gnome.org/jwestman/blueprint-compiler image: registry.gitlab.gnome.org/gnome/blueprint-compiler
stage: build stage: build
script: script:
- mypy --python-version=3.9 blueprintcompiler - black --check --diff ./ tests
- coverage run -m unittest - 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 report
- coverage html - coverage html
- coverage xml - coverage xml
- meson _build -Ddocs=true --prefix=/usr - meson _build -Ddocs=true --prefix=/usr
- ninja -C _build - ninja -C _build
- ninja -C _build test
- ninja -C _build install - ninja -C _build install
- ninja -C _build docs/en - ninja -C _build docs/en
- git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git
- cd blueprint-regression-tests - cd blueprint-regression-tests
- git checkout 94613f275efc810610768d5ee8b2aec28392c3e8 - git checkout 5f9e155c1333e84e6f683cdb26b02a5925fd8db3
- ./test.sh - ./test.sh
- cd .. - cd ..
coverage: '/TOTAL.*\s([.\d]+)%/' coverage: '/TOTAL.*\s([.\d]+)%/'
@ -32,9 +33,8 @@ build:
path: coverage.xml path: coverage.xml
fuzz: fuzz:
image: registry.gitlab.gnome.org/jwestman/blueprint-compiler image: registry.gitlab.gnome.org/gnome/blueprint-compiler
stage: build stage: build
allow_failure: true
script: script:
- meson _build - meson _build
- ninja -C _build install - ninja -C _build install

View file

@ -8,6 +8,10 @@ If you learn something useful, please add it to this file.
python -m unittest python -m unittest
``` ```
# Formatting
Blueprint uses [Black](https://github.com/psf/black) for code formatting.
# Build the docs # Build the docs
```sh ```sh

View file

@ -5,8 +5,14 @@ in the NEWS file.
2. Update the version number, according to semver: 2. Update the version number, according to semver:
- At the top of meson.build - At the top of meson.build
- In docs/flatpak.rst - In docs/flatpak.rst
3. Make a new commit with just these two changes. Use `Release v{version}` 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.
as the commit message. Tag the commit as `v{version}` and push the tag.
4. Create a "Post-release version bump" commit. 4. Create a "Post-release version bump" commit.
5. Go to the Releases page in GitLab and create a new release from the tag. 5. Go to the Releases page in GitLab and create a new release from the tag.
6. Announce the release through relevant channels (Twitter, TWIG, etc.) 6. Announce the release through relevant channels (Mastodon, TWIG, etc.)
## Related projects
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)

197
NEWS.md
View file

@ -1,3 +1,200 @@
# 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 # v0.6.0
## Breaking Changes ## Breaking Changes

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,7 +19,8 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import os, sys import os
import sys
# These variables should be set by meson. If they aren't, we're running # These variables should be set by meson. If they aren't, we're running
# uninstalled, and we might have to guess some values. # uninstalled, and we might have to guess some values.

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,31 +17,80 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from collections import ChainMap, defaultdict from collections import ChainMap, defaultdict
from functools import cached_property from functools import cached_property
import typing as T
from .errors import * from .errors import *
from .lsp_utils import SemanticToken from .lsp_utils import DocumentSymbol, LocationLink, SemanticToken
from .tokenizer import Range
TType = T.TypeVar("TType")
class Children: class Children:
"""Allows accessing children by type using array syntax.""" """Allows accessing children by type using array syntax."""
def __init__(self, children): def __init__(self, children):
self._children = children self._children = children
def __iter__(self):
def __iter__(self) -> T.Iterator["AstNode"]:
return iter(self._children) 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): def __getitem__(self, key):
if isinstance(key, int): if isinstance(key, int):
if key >= len(self._children):
return None
else:
return self._children[key] return self._children[key]
else: else:
return [child for child in self._children if isinstance(child, key)] 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: class AstNode:
"""Base class for nodes in the abstract syntax tree.""" """Base class for nodes in the abstract syntax tree."""
completers: T.List = [] completers: T.List = []
attrs_by_type: T.Dict[T.Type, T.List] = {}
def __init__(self, group, children, tokens, incomplete=False): def __init__(self, group, children, tokens, incomplete=False):
self.group = group self.group = group
@ -55,19 +104,33 @@ class AstNode:
def __init_subclass__(cls): def __init_subclass__(cls):
cls.completers = [] 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): def root(self):
if self.parent is None: if self.parent is None:
return self return self
else: else:
return self.parent.root 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: if self.parent is None:
return None raise CompilerBugError()
elif isinstance(self.parent, type): elif isinstance(self.parent, type):
return self.parent return self.parent
else: else:
@ -75,7 +138,19 @@ class AstNode:
@cached_property @cached_property
def errors(self): 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): def _get_errors(self):
for validator in self.validators: for validator in self.validators:
@ -85,15 +160,23 @@ class AstNode:
yield e yield e
if e.fatal: if e.fatal:
return return
except MultipleErrors as e:
for error in e.errors:
yield error
if error.fatal:
return
for child in self.children: for child in self.children:
yield from child._get_errors() yield from child._get_errors()
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)): for name in dir(type(self)):
item = getattr(type(self), name) item = getattr(type(self), name)
if isinstance(item, attr_type): if isinstance(item, attr_type):
yield name, item 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]: def get_docs(self, idx: int) -> T.Optional[str]:
for name, attr in self._attrs_by_type(Docs): for name, attr in self._attrs_by_type(Docs):
@ -101,29 +184,46 @@ class AstNode:
token = self.group.tokens.get(attr.token_name) token = self.group.tokens.get(attr.token_name)
if token and token.start <= idx < token.end: if token and token.start <= idx < token.end:
return getattr(self, name) return getattr(self, name)
else:
return getattr(self, name)
for child in self.children: for child in self.children:
if child.group.start <= idx < child.group.end: if idx in child.range:
docs = child.get_docs(idx) if docs := child.get_docs(idx):
if docs is not None:
return docs return docs
for name, attr in self._attrs_by_type(Docs):
if not attr.token_name:
return getattr(self, name)
return None return None
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
for child in self.children: for child in self.children:
yield from child.get_semantic_tokens() yield from child.get_semantic_tokens()
def get_reference(self, idx: int) -> T.Optional[LocationLink]:
def iterate_children_recursive(self) -> T.Iterator["AstNode"]:
yield self
for child in self.children: 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: for child in self.parent.children:
if child is self: if child is self:
break break
@ -132,19 +232,38 @@ class AstNode:
if check is None or check(child): if check is None or check(child):
raise CompileError( raise CompileError(
error, 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 """Decorator for functions that validate an AST node. Exceptions raised
during validation are marked with range information from the tokens.""" during validation are marked with range information from the tokens."""
def decorator(func): def decorator(func):
def inner(self): def inner(self: AstNode):
if skip_incomplete and self.incomplete: if skip_incomplete and self.incomplete:
return 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: try:
func(self) func(self)
except CompileError as e: except CompileError as e:
@ -153,25 +272,18 @@ def validate(token_name=None, end_token_name=None, skip_incomplete=False):
if self.incomplete: if self.incomplete:
return return
# This mess of code sets the error's start and end positions fill_error(e)
# 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
# Re-raise the exception # Re-raise the exception
raise e raise e
except MultipleErrors as e:
if self.incomplete:
return
for error in e.errors:
fill_error(error)
raise e
inner._validator = True inner._validator = True
return inner return inner
@ -197,3 +309,28 @@ def docs(*args, **kwargs):
return Docs(func, *args, **kwargs) return Docs(func, *args, **kwargs)
return decorator 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,21 +19,25 @@
import typing as T import typing as T
from . import gir, language from . import annotations, gir, language
from .language.types import ClassName
from .ast_utils import AstNode from .ast_utils import AstNode
from .completions_utils import * from .completions_utils import *
from .language.types import ClassName
from .lsp_utils import Completion, CompletionItemKind from .lsp_utils import Completion, CompletionItemKind
from .parser import SKIP_TOKENS from .parser import SKIP_TOKENS
from .tokenizer import TokenType, Token from .tokenizer import Token, TokenType
Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] 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: for child in ast_node.children:
if child.group.start <= idx and (idx < child.group.end or (idx == child.group.end and child.incomplete)): if child.group.start <= idx and (
yield from _complete(child, tokens, idx, token_idx) idx < child.group.end or (idx == child.group.end and child.incomplete)
):
yield from _complete(lsp, child, tokens, idx, token_idx)
return return
prev_tokens: T.List[Token] = [] prev_tokens: T.List[Token] = []
@ -46,10 +50,12 @@ def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int
token_idx -= 1 token_idx -= 1
for completer in ast_node.completers: 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 token_idx = 0
# find the current token # find the current token
for i, token in enumerate(tokens): for i, token in enumerate(tokens):
@ -61,23 +67,29 @@ def complete(ast_node: AstNode, tokens: T.List[Token], idx: int) -> T.Iterator[C
idx = tokens[token_idx].start idx = tokens[token_idx].start
token_idx -= 1 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]) @completer([language.GtkDirective])
def using_gtk(ast_node, match_variables): def using_gtk(lsp, ast_node, match_variables):
yield Completion("using Gtk 4.0;", CompletionItemKind.Keyword) yield Completion(
"using Gtk 4.0", CompletionItemKind.Keyword, snippet="using Gtk 4.0;\n"
)
@completer( @completer(
applies_in=[language.UI, language.ObjectContent, language.Template], 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.") yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.")
for ns in ast_node.root.children[language.Import]: for ns in ast_node.root.children[language.Import]:
if ns.gir_namespace is not None: 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( @completer(
@ -85,48 +97,122 @@ def namespace(ast_node, match_variables):
matches=[ matches=[
[(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)], [(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)],
[(TokenType.IDENT, None), (TokenType.OP, ".")], [(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]) ns = ast_node.root.gir.namespaces.get(match_variables[0])
if ns is not None: if ns is not None:
for c in ns.classes.values(): 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( @completer(
applies_in=[language.UI, language.ObjectContent, language.Template], applies_in=[language.UI, language.ObjectContent, language.Template],
matches=new_statement_patterns, 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") ns = ast_node.root.gir.namespaces.get("Gtk")
if ns is not None: if ns is not None:
for c in ns.classes.values(): 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( @completer(
applies_in=[language.ObjectContent], applies_in=[language.ObjectContent],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def property_completer(ast_node, match_variables): def property_completer(lsp, ast_node, match_variables):
if ast_node.gir_class: if ast_node.gir_class and hasattr(ast_node.gir_class, "properties"):
for prop in ast_node.gir_class.properties: for prop_name, prop in ast_node.gir_class.properties.items():
yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;") 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( @completer(
applies_in=[language.Property, language.BaseTypedAttribute], applies_in=[language.Property, language.A11yProperty],
matches=[ matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]],
[(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("true", CompletionItemKind.Constant)
yield Completion("false", CompletionItemKind.Constant) yield Completion("false", CompletionItemKind.Constant)
@ -135,22 +221,32 @@ def prop_value_completer(ast_node, match_variables):
applies_in=[language.ObjectContent], applies_in=[language.ObjectContent],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def signal_completer(ast_node, match_variables): def signal_completer(lsp, ast_node, match_variables):
if ast_node.gir_class: if ast_node.gir_class and hasattr(ast_node.gir_class, "signals"):
for signal in ast_node.gir_class.signals: for signal_name, signal in ast_node.gir_class.signals.items():
if not isinstance(ast_node.parent, language.Object): if not isinstance(ast_node.parent, language.Object):
name = "on" name = "on"
else: else:
name = "on_" + (ast_node.parent.children[ClassName][0].tokens["id"] or ast_node.parent.children[ClassName][0].tokens["class_name"].lower()) name = "on_" + (
yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;") ast_node.parent.children[ClassName][0].tokens["id"]
or ast_node.parent.children[ClassName][0]
.tokens["class_name"]
@completer( .lower()
applies_in=[language.UI],
matches=new_statement_patterns
) )
def template_completer(ast_node, match_variables):
yield Completion( yield Completion(
"template", CompletionItemKind.Snippet, signal_name,
snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}" 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 import typing as T
from .tokenizer import Token, TokenType
from .lsp_utils import Completion from .lsp_utils import Completion
from .tokenizer import Token, TokenType
new_statement_patterns = [ new_statement_patterns = [
[(TokenType.PUNCTUATION, "{")], [(TokenType.PUNCTUATION, "{")],
[(TokenType.PUNCTUATION, "}")], [(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 completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None):
def decorator(func): def decorator(func):
def inner(prev_tokens: T.List[Token], ast_node): def inner(prev_tokens: T.List[Token], ast_node, lsp):
# For completers that apply in ObjectContent nodes, we can further # For completers that apply in ObjectContent nodes, we can further
# check that the object is the right class # check that the object is the right class
if applies_in_subclass is not None: if applies_in_subclass is not None:
type = ast_node.root.gir.get_type(applies_in_subclass[1], applies_in_subclass[0]) type = ast_node.root.gir.get_type(
if ast_node.gir_class and not ast_node.gir_class.assignable_to(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 return
any_match = len(matches) == 0 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)): for i in range(0, len(pattern)):
type, value = pattern[i] type, value = pattern[i]
token = prev_tokens[i - len(pattern)] 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 break
if value is None: if value is None:
match_variables.append(str(token)) 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: if not any_match:
return return
yield from func(ast_node, match_variables) yield from func(lsp, ast_node, match_variables)
for c in applies_in: for c in applies_in:
c.completers.append(inner) c.completers.append(inner)

View file

@ -17,20 +17,20 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import re
from enum import Enum
import typing as T import typing as T
from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from .xml_reader import Element, parse from . import formatter
from .gir import * from .gir import *
from .utils import Colors from .utils import Colors, escape_quote
from .xml_reader import Element, parse, parse_string
__all__ = ["decompile"] __all__ = ["decompile"]
_DECOMPILERS: T.Dict = {} _DECOMPILERS: dict[str, list] = defaultdict(list)
_CLOSING = { _CLOSING = {
"{": "}", "{": "}",
"[": "]", "[": "]",
@ -51,26 +51,34 @@ class LineType(Enum):
class DecompileCtx: class DecompileCtx:
def __init__(self): def __init__(self, parent_gir: T.Optional[GirContext] = None) -> None:
self._result = "" self.sub_decompiler = parent_gir is not None
self.gir = GirContext() self._result: str = ""
self._indent = 0 self.gir = parent_gir or GirContext()
self._blocks_need_end = [] self._blocks_need_end: T.List[str] = []
self._last_line_type = LineType.NONE 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")) self.gir.add_namespace(get_namespace("Gtk", "4.0"))
@property @property
def result(self): def result(self) -> str:
imports = "\n".join([ imports = ""
if not self.sub_decompiler:
import_lines = sorted(
[
f"using {ns} {namespace.version};" f"using {ns} {namespace.version};"
for ns, namespace in self.gir.namespaces.items() for ns, namespace in self.gir.namespaces.items()
]) if ns != "Gtk"
return imports + "\n" + self._result ]
)
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): if type := self.gir.get_type_by_cname(cname):
return type return type
@ -83,114 +91,204 @@ class DecompileCtx:
except: except:
pass pass
return None
def start_block(self): def start_block(self) -> None:
self._blocks_need_end.append(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(): if close := self._blocks_need_end.pop():
self.print(close) 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 self._blocks_need_end[-1] = text
def print(self, line: str, newline: bool = True) -> None:
def print(self, line, newline=True): self._result += line
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"
if line.endswith("{") or line.endswith("["): if line.endswith("{") or line.endswith("["):
if len(self._blocks_need_end): if len(self._blocks_need_end):
self._blocks_need_end[-1] = _CLOSING[line[-1]] self._blocks_need_end[-1] = _CLOSING[line[-1]]
self._indent += 1
def print_attribute(self, name, value, type): # 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): def get_enum_name(value):
for member in type.members.values(): for member in type.members.values():
if member.nick == value or member.c_ident == value: if (
member.nick == value
or member.c_ident == value
or str(member.value) == value
):
return member.name return member.name
return value.replace('-', '_') return value.replace("-", "_")
if type is None: if translatable is not None and truthy(translatable[0]):
self.print(f"{name}: \"{escape_quote(value)}\";") return decompile_translatable(value, *translatable)
elif type is None:
return "", f"{escape_quote(value)}"
elif type.assignable_to(FloatType()): elif type.assignable_to(FloatType()):
self.print(f"{name}: {value};") return "", str(value)
elif type.assignable_to(BoolType()): elif type.assignable_to(BoolType()):
val = truthy(value) 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 ( elif (
type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf")) 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.Texture"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable")) or type.assignable_to(
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction")) self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable")
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger")) )
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)}\";") return "", escape_quote(value)
elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")): elif value == self.template_class:
self.print(f"{name}: {value};") 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): elif isinstance(type, Bitfield):
flags = [get_enum_name(flag) for flag in value.split("|")] flags = [get_enum_name(flag) for flag in value.split("|")]
self.print(f"{name}: {' | '.join(flags)};") return "", " | ".join(flags)
elif isinstance(type, Enumeration): elif isinstance(type, Enumeration):
self.print(f"{name}: {get_enum_name(value)};") return "", get_enum_name(value)
elif isinstance(type, TypeType):
if t := self.type_by_cname(value):
return "", f"typeof<{full_name(t)}>"
else: 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: try:
decompiler = _DECOMPILERS.get(xml.tag) decompilers = [d for d in _DECOMPILERS[xml.tag] if d._filter(ctx)]
if decompiler is None: if len(decompilers) == 0:
raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>") 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 decompiler._cdata:
if len(xml.children): if len(xml.children):
args["cdata"] = None kwargs["cdata"] = None
else: else:
args["cdata"] = xml.cdata kwargs["cdata"] = xml.cdata
ctx._node_stack.append(xml)
ctx.start_block() ctx.start_block()
gir = decompiler(ctx, gir, **args)
for child in xml.children: try:
_decompile_element(ctx, gir, child) gir = decompiler(*args, **kwargs)
ctx.end_block()
except UnsupportedError as e:
raise e
except TypeError as e: except TypeError as e:
raise UnsupportedError(tag=xml.tag) 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() ctx = DecompileCtx()
xml = parse(data) xml = parse(data)
_decompile_element(ctx, None, xml) decompile_element(ctx, None, xml)
return ctx.result 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: def canon(string: str) -> str:
if string == "class": if string == "class":
@ -198,37 +296,61 @@ def canon(string: str) -> str:
else: else:
return string.replace("-", "_").lower() 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 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): if isinstance(gir, GirContext):
return gir.get_type_by_cname(cname) return gir.get_type_by_cname(cname)
else: else:
return gir.get_containing(Repository).get_type_by_cname(cname) 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): def decorator(func):
func._cdata = cdata 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 func
return decorator return decorator
def escape_quote(string: str) -> str:
return (string
.replace("\\", "\\\\")
.replace("\'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n"))
@decompiler("interface") @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 return gir
@ -242,14 +364,43 @@ def decompile_placeholder(ctx, gir):
pass pass
@decompiler("property", cdata=True) def decompile_translatable(
def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=None, bind_flags=None, translatable="false", comments=None, context=None): string: str,
name = name.replace("_", "-") translatable: T.Optional[str],
if comments is not None: context: T.Optional[str],
ctx.print(f"/* Translators: {comments} */") 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: if cdata is None:
ctx.print(f"{name}: ", False) ctx.print(f"{name}: ")
ctx.end_block_with(";") ctx.end_block_with(";")
elif bind_source: elif bind_source:
flags = "" flags = ""
@ -260,21 +411,50 @@ def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=No
flags += " inverted" flags += " inverted"
if "bidirectional" in bind_flags: if "bidirectional" in bind_flags:
flags += " bidirectional" flags += " bidirectional"
if bind_source == ctx.template_class:
bind_source = "template"
ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};") ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
elif truthy(translatable): elif truthy(translatable):
if context is not None: comments, translatable = decompile_translatable(
ctx.print(f"{name}: C_(\"{escape_quote(context)}\", \"{escape_quote(cdata)}\");") cdata, translatable, context, comments
else: )
ctx.print(f"{name}: _(\"{escape_quote(cdata)}\");") if comments is not None:
ctx.print(comments)
ctx.print(f"{name}: {translatable};")
elif gir is None or gir.properties.get(name) is None: 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: 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 return gir
@decompiler("attribute", cdata=True) @decompiler("attribute", cdata=True)
def decompile_attribute(ctx, gir, name, cdata, translatable="false", comments=None, context=None): def decompile_attribute(
decompile_property(ctx, gir, name, cdata, translatable=translatable, comments=comments, context=context) ctx, gir, name, cdata, translatable="false", comments=None, context=None
):
decompile_property(
ctx,
gir,
name,
cdata,
translatable=translatable,
comments=comments,
context=context,
)
@decompiler("attributes") @decompiler("attributes")
def decompile_attributes(ctx, gir): def decompile_attributes(ctx, gir):
@ -291,5 +471,7 @@ class UnsupportedError(Exception):
print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}") print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}")
if self.tag: if self.tag:
print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}") 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 print(
probably need to port this file manually.{Colors.CLEAR}\n""") 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 # SPDX-License-Identifier: LGPL-3.0-or-later
from dataclasses import dataclass import sys
import traceback
import typing as T import typing as T
import sys, traceback from dataclasses import dataclass
from . import utils from . import utils
from .tokenizer import Range
from .utils import Colors from .utils import Colors
class PrintableError(Exception): class PrintableError(Exception):
"""Parent class for errors that can be pretty-printed for the user, e.g. """Parent class for errors that can be pretty-printed for the user, e.g.
compilation warnings and errors.""" compilation warnings and errors."""
def pretty_print(self, filename, code): def pretty_print(self, filename, code, stream=sys.stdout):
raise NotImplementedError() raise NotImplementedError()
@dataclass @dataclass
class ErrorReference: class ErrorReference:
start: int range: Range
end: int
message: str message: str
@ -44,12 +47,20 @@ class CompileError(PrintableError):
category = "error" category = "error"
color = Colors.RED 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) super().__init__(message)
self.message = message self.message = message
self.start = start self.range = range
self.end = end
self.hints = hints or [] self.hints = hints or []
self.actions = actions or [] self.actions = actions or []
self.references = references or [] self.references = references or []
@ -58,12 +69,11 @@ class CompileError(PrintableError):
if did_you_mean is not None: if did_you_mean is not None:
self._did_you_mean(*did_you_mean) self._did_you_mean(*did_you_mean)
def hint(self, hint: str): def hint(self, hint: str) -> "CompileError":
self.hints.append(hint) self.hints.append(hint)
return self return self
def _did_you_mean(self, word: str, options: T.List[str]) -> None:
def _did_you_mean(self, word: str, options: T.List[str]):
if word.replace("_", "-") in options: if word.replace("_", "-") in options:
self.hint(f"use '-', not '_': `{word.replace('_', '-')}`") self.hint(f"use '-', not '_': `{word.replace('_', '-')}`")
return return
@ -79,28 +89,65 @@ class CompileError(PrintableError):
self.hint("Did you check your spelling?") self.hint("Did you check your spelling?")
self.hint("Are your dependencies up to date?") self.hint("Are your dependencies up to date?")
def pretty_print(self, filename, code, stream=sys.stdout): def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None:
line_num, col_num = utils.idx_to_pos(self.start + 1, code) assert self.range is not None
line = code.splitlines(True)[line_num]
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 # Display 1-based line numbers
line_num += 1 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}: 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: for hint in self.hints:
stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n") 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: 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 = code.splitlines(True)[line_num]
line_num += 1 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}: 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") stream.write("\n")
@ -110,15 +157,29 @@ class CompileWarning(CompileError):
color = Colors.YELLOW color = Colors.YELLOW
class DeprecatedWarning(CompileWarning):
pass
class UnusedWarning(CompileWarning):
pass
class UpgradeWarning(CompileWarning):
category = "upgrade"
color = Colors.PURPLE
class UnexpectedTokenError(CompileError): class UnexpectedTokenError(CompileError):
def __init__(self, start, end): def __init__(self, range: Range) -> None:
super().__init__("Unexpected tokens", start, end) super().__init__("Unexpected tokens", range)
@dataclass @dataclass
class CodeAction: class CodeAction:
title: str title: str
replace_with: str replace_with: str
edit_range: T.Optional[Range] = None
class MultipleErrors(PrintableError): class MultipleErrors(PrintableError):
@ -126,13 +187,13 @@ class MultipleErrors(PrintableError):
a list and re-thrown using the MultipleErrors exception. It will 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.""" 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__() super().__init__()
self.errors = errors 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: for error in self.errors:
error.pretty_print(filename, code) error.pretty_print(filename, code, stream)
if len(self.errors) != 1: if len(self.errors) != 1:
print(f"{len(self.errors)} errors") print(f"{len(self.errors)} errors")
@ -149,13 +210,17 @@ def assert_true(truth: bool, message: T.Optional[str]=None):
def report_bug(): # pragma: no cover def report_bug(): # pragma: no cover
"""Report an error and ask people to report it.""" """Report an error and ask people to report it."""
from . import main
print(traceback.format_exc()) print(traceback.format_exc())
print(f"Arguments: {sys.argv}\n") print(f"Arguments: {sys.argv}")
print(f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** 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, The blueprint-compiler program has crashed. Please report the above stacktrace,
along with the input file(s) if possible, on GitLab: along with the input file(s) if possible, on GitLab:
{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue {Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue
{Colors.CLEAR}""") {Colors.CLEAR}"""
)
sys.exit(1) 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,26 +18,27 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import difflib import difflib
import os import os
import typing as T
from . import decompiler, tokenizer, parser from . import decompiler, parser, tokenizer
from .errors import CompilerBugError, MultipleErrors, PrintableError
from .outputs.xml import XmlOutput from .outputs.xml import XmlOutput
from .errors import MultipleErrors, PrintableError
from .utils import Colors from .utils import Colors
# A tool to interactively port projects to blueprints. # A tool to interactively port projects to blueprints.
class CouldNotPort: class CouldNotPort:
def __init__(self, message): def __init__(self, message: str):
self.message = message self.message = message
def change_suffix(f): def change_suffix(f):
return f.removesuffix(".ui") + ".blp" return f.removesuffix(".ui") + ".blp"
def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
if os.path.exists(out_file): if os.path.exists(out_file):
return CouldNotPort("already exists") return CouldNotPort("already exists")
@ -55,20 +56,23 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
if errors: if errors:
raise errors raise errors
if len(ast.errors): if not ast:
raise MultipleErrors(ast.errors) raise CompilerBugError()
output = XmlOutput() output = XmlOutput()
output.emit(ast) output.emit(ast)
except PrintableError as e: except PrintableError as e:
e.pretty_print(out_file, decompiled) 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"in {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE}")
print( print(
f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the 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: 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") return CouldNotPort("does not compile")
@ -84,6 +88,8 @@ def listdir_recursive(subdir):
for file in files: for file in files:
if file in ["_build", "build"]: if file in ["_build", "build"]:
continue continue
if file.startswith("."):
continue
full = os.path.join(subdir, file) full = os.path.join(subdir, file)
if full == "./subprojects": if full == "./subprojects":
# skip the subprojects directory # skip the subprojects directory
@ -108,7 +114,9 @@ def enter():
def step1(): 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"): if os.path.exists("subprojects/blueprint-compiler.wrap"):
print("subprojects/blueprint-compiler.wrap already exists, skipping\n") print("subprojects/blueprint-compiler.wrap already exists, skipping\n")
@ -121,17 +129,20 @@ def step1():
pass pass
from .main import VERSION from .main import VERSION
VERSION = "main" if VERSION == "uninstalled" else "v" + VERSION VERSION = "main" if VERSION == "uninstalled" else "v" + VERSION
with open("subprojects/blueprint-compiler.wrap", "w") as wrap: with open("subprojects/blueprint-compiler.wrap", "w") as wrap:
wrap.write(f"""[wrap-git] wrap.write(
f"""[wrap-git]
directory = blueprint-compiler directory = blueprint-compiler
url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git
revision = {VERSION} revision = {VERSION}
depth = 1 depth = 1
[provide] [provide]
program_names = blueprint-compiler""") program_names = blueprint-compiler"""
)
print() print()
@ -146,7 +157,9 @@ def step2():
if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"): if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"):
gitignore.write("\n/subprojects/blueprint-compiler\n") gitignore.write("\n/subprojects/blueprint-compiler\n")
else: else:
print("'/subprojects/blueprint-compiler' already in .gitignore, skipping") print(
"'/subprojects/blueprint-compiler' already in .gitignore, skipping"
)
else: else:
if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"): if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"):
with open(".gitignore", "w") as gitignore: with open(".gitignore", "w") as gitignore:
@ -169,9 +182,13 @@ def step3():
if isinstance(result, CouldNotPort): if isinstance(result, CouldNotPort):
if result.message == "already exists": if result.message == "already exists":
print(Colors.FAINT, end="") 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: 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 success += 1
print() print()
@ -180,7 +197,9 @@ def step3():
elif success == len(files): elif success == len(files):
print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}") print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}")
elif success > 0: 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: else:
print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}") print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}")
@ -204,22 +223,33 @@ def step3():
def step4(ported): def step4(ported):
print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}") 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: for meson_file in meson_files:
with open(meson_file, "r") as f: with open(meson_file, "r") as f:
if "gnome.compile_resources" in f.read(): if "gnome.compile_resources" in f.read():
parent = os.path.dirname(meson_file) parent = os.path.dirname(meson_file)
file_list = "\n ".join([ file_list = "\n ".join(
[
f"'{os.path.relpath(file, parent)}'," f"'{os.path.relpath(file, parent)}',"
for file in ported for file in ported
if file.startswith(parent) if file.startswith(parent)
]) ]
)
if len(file_list): if len(file_list):
print(f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}") print(
print(f""" f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}"
)
print(
f"""
blueprints = custom_target('blueprints', blueprints = custom_target('blueprints',
input: files( input: files(
{file_list} {file_list}
@ -227,14 +257,17 @@ blueprints = custom_target('blueprints',
output: '.', output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
) )
""") """
)
enter() 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} arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}
dependencies: blueprints, dependencies: blueprints,
""") """
)
enter() enter()
print() print()
@ -244,7 +277,9 @@ def step5(in_files):
print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}") print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}")
if not os.path.exists("po/POTFILES.in"): 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 return
with open("po/POTFILES.in", "r") as potfiles: with open("po/POTFILES.in", "r") as potfiles:
@ -257,12 +292,22 @@ def step5(in_files):
new_data = "".join(lines) new_data = "".join(lines)
print(f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}")
print( print(
"".join([ f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}"
(Colors.GREEN if line.startswith('+') else Colors.RED + Colors.FAINT if line.startswith('-') else '') + line + 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) for line in difflib.unified_diff(old_lines, lines)
]) ]
)
) )
if yesno("Is this ok?"): if yesno("Is this ok?"):
@ -291,5 +336,6 @@ def run(opts):
step5(in_files) step5(in_files)
step6(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,45 +1,78 @@
from .attributes import BaseAttribute, BaseTypedAttribute from .adw_breakpoint import (
from .expression import IdentExpr, LookupOp, Expr 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_object import Object, ObjectContent
from .gobject_property import Property from .gobject_property import Property
from .gobject_signal import Signal from .gobject_signal import Signal
from .gtk_a11y import A11y from .gtk_a11y import A11yProperty, ExtAccessibility
from .gtk_combo_box_text import Items from .gtk_combo_box_text import ExtComboBoxItems
from .gtk_file_filter import mime_types, patterns, suffixes, Filters from .gtk_file_filter import (
from .gtk_layout import Layout Filters,
from .gtk_menu import menu, Menu, MenuAttribute ext_file_filter_mime_types,
from .gtk_size_group import Widgets ext_file_filter_patterns,
from .gtk_string_list import Strings ext_file_filter_suffixes,
from .gtk_styles import Styles )
from .gtkbuilder_child import Child 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 .gtkbuilder_template import Template
from .imports import GtkDirective, Import from .imports import GtkDirective, Import
from .ui import UI
from .types import ClassName from .types import ClassName
from .values import TypeValue, IdentValue, TranslatedStringValue, FlagsValue, Flag, QuotedValue, NumberValue, Value from .ui import UI
from .values import (
from .common import * ArrayValue,
ExprValue,
Flag,
Flags,
IdentLiteral,
Literal,
NumberLiteral,
ObjectValue,
QuotedLiteral,
StringValue,
Translated,
TypeLiteral,
Value,
)
OBJECT_CONTENT_HOOKS.children = [ OBJECT_CONTENT_HOOKS.children = [
Signal, Signal,
Property, Property,
A11y, AdwBreakpointCondition,
Styles, AdwBreakpointSetters,
Layout, ExtAccessibility,
mime_types, ExtAdwResponseDialog,
patterns, ExtComboBoxItems,
suffixes, ext_file_filter_mime_types,
Widgets, ext_file_filter_patterns,
Items, ext_file_filter_suffixes,
Strings, ExtLayout,
ExtListItemFactory,
ExtScaleMarks,
ExtSizeGroupWidgets,
ExtStringListStrings,
ExtStyles,
Child, Child,
] ]
VALUE_HOOKS.children = [ LITERAL.children = [Literal]
TypeValue,
TranslatedStringValue,
FlagsValue,
IdentValue,
QuotedValue,
NumberValue,
]

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

@ -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,16 +18,46 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # 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 .. import decompiler as decompile
from ..decompiler import DecompileCtx, decompiler from .. import gir
from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration from ..ast_utils import AstNode, context, docs, validate
from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType 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 ..parse_tree import *
OBJECT_CONTENT_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

@ -18,41 +18,367 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from ..decompiler import decompile_element
from .common import * from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
from .types import TypeName
expr = Sequence()
expr = Pratt() 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 Expr(AstNode): class Expression(ExprBase):
grammar = expr grammar = expr
@property
def last(self) -> ExprBase:
return self.children[-1]
class InfixExpr(AstNode): @property
def type(self) -> T.Optional[GirType]:
return self.last.type
class InfixExpr(ExprBase):
@property @property
def lhs(self): def lhs(self):
children = list(self.parent_by_type(Expr).children) children = list(self.parent_by_type(Expression).children)
return children[children.index(self) - 1] return children[children.index(self) - 1]
class IdentExpr(AstNode): class LiteralExpr(ExprBase):
grammar = UseIdent("ident") grammar = LITERAL
@property @property
def ident(self) -> str: def is_object(self) -> bool:
return self.tokens["ident"] 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): class LookupOp(InfixExpr):
grammar = [".", UseIdent("property")] grammar = [".", UseIdent("property")]
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(None, must_infer_type=True)
@property @property
def property_name(self) -> str: def property_name(self) -> str:
return self.tokens["property"] 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 = [ expr.children = [
Prefix(IdentExpr), AnyOf(ClosureExpr, LiteralExpr, ["(", Expression, ")"]),
Prefix(["(", Expr, ")"]), ZeroOrMore(AnyOf(LookupOp, CastExpr)),
Infix(10, LookupOp),
] ]
@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,10 +21,26 @@
import typing as T import typing as T
from functools import cached_property from functools import cached_property
from blueprintcompiler.errors import T
from blueprintcompiler.lsp_utils import DocumentSymbol
from .common import * from .common import *
from .response_id import ResponseId from .response_id import ExtResponse
from .types import ClassName, ConcreteClassName from .types import ClassName, ConcreteClassName
RESERVED_IDS = {
"this",
"self",
"template",
"true",
"false",
"null",
"none",
"item",
"expr",
"typeof",
}
class ObjectContent(AstNode): class ObjectContent(AstNode):
grammar = ["{", Until(OBJECT_CONTENT_HOOKS, "}")] grammar = ["{", Until(OBJECT_CONTENT_HOOKS, "}")]
@ -33,6 +49,7 @@ class ObjectContent(AstNode):
def gir_class(self): def gir_class(self):
return self.parent.gir_class return self.parent.gir_class
class Object(AstNode): class Object(AstNode):
grammar: T.Any = [ grammar: T.Any = [
ConcreteClassName, ConcreteClassName,
@ -45,7 +62,7 @@ class Object(AstNode):
return self.tokens["id"] return self.tokens["id"]
@property @property
def class_name(self) -> T.Optional[ClassName]: def class_name(self) -> ClassName:
return self.children[ClassName][0] return self.children[ClassName][0]
@property @property
@ -53,11 +70,32 @@ class Object(AstNode):
return self.children[ObjectContent][0] return self.children[ObjectContent][0]
@property @property
def gir_class(self): 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_class(self) -> GirType:
if self.class_name is None:
raise CompilerBugError()
return self.class_name.gir_type return self.class_name.gir_type
@cached_property @cached_property
def action_widgets(self) -> T.List[ResponseId]: def action_widgets(self) -> T.List[ExtResponse]:
"""Get list of widget's action widgets. """Get list of widget's action widgets.
Empty if object doesn't have action widgets. Empty if object doesn't have action widgets.
@ -66,24 +104,36 @@ class Object(AstNode):
return [ return [
child.response_id child.response_id
for child in self.children[ObjectContent][0].children[Child] for child in self.content.children[Child]
if child.response_id if child.response_id
] ]
@validate("id")
def object_id_not_reserved(self):
from .gtkbuilder_template import Template
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): def validate_parent_type(node, ns: str, name: str, err_msg: str):
parent = node.root.gir.get_type(name, ns) parent = node.root.gir.get_type(name, ns)
container_type = node.parent_by_type(Object).gir_class container_type = node.parent_by_type(Object).gir_class
if container_type and not container_type.assignable_to(parent): 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") @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) 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: if id is None:
ctx.print(f"{klass_name} {{") ctx.print(f"{klass_name} {{")
else: else:
ctx.print(f"{klass_name} {id} {{") ctx.print(f"{klass_name} {id} {{")
ctx.push_obj_type(gir_class)
return gir_class return gir_class

View file

@ -18,88 +18,83 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .expression import Expr from .binding import Binding
from .gobject_object import Object
from .gtkbuilder_template import Template
from .values import Value, TranslatedStringValue
from .common import * from .common import *
from .contexts import ValueTypeCtx
from .values import ArrayValue, ExprValue, ObjectValue, Value
class Property(AstNode): class Property(AstNode):
grammar = AnyOf( grammar = Statement(
[ UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue)
UseIdent("name"),
":",
Keyword("bind"),
UseIdent("bind_source"),
".",
UseIdent("bind_property"),
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"),
UseLiteral("binding", True),
":",
"bind",
Expr,
),
Statement(
UseIdent("name"),
":",
AnyOf(
Object,
VALUE_HOOKS,
).expected("a value"),
),
) )
@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 @property
def gir_class(self): def gir_class(self):
return self.parent.parent.gir_class return self.parent.parent.gir_class
@property @property
def gir_property(self): def gir_property(self) -> T.Optional[gir.Property]:
if self.gir_class is not None: if self.gir_class is not None and not isinstance(self.gir_class, ExternType):
return self.gir_class.properties.get(self.tokens["name"]) return self.gir_class.properties.get(self.tokens["name"])
else:
return None
@property @property
def value_type(self): def document_symbol(self) -> DocumentSymbol:
if self.gir_property is not None: if isinstance(self.value, ObjectValue) or self.value is None:
return self.gir_property.type 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") @validate("name")
def property_exists(self): 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 # Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself # This happens for classes defined by the app itself
return 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: if self.gir_property is None:
raise CompileError( raise CompileError(
f"Class {self.gir_class.full_name} does not contain a property called {self.tokens['name']}", 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()) 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"]
) )
@validate("name") @validate("name")
@ -107,27 +102,23 @@ class Property(AstNode):
if self.gir_property is not None and not self.gir_property.writable: if self.gir_property is not None and not self.gir_property.writable:
raise CompileError(f"{self.gir_property.full_name} is not 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") @validate("name")
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent( self.validate_unique_in_parent(
f"Duplicate property '{self.tokens['name']}'", 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") @docs("name")
def property_docs(self): def property_docs(self):

View file

@ -19,26 +19,72 @@
import typing as T import typing as T
from .gtkbuilder_template import Template
from .common import * 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): class Signal(AstNode):
grammar = Statement( grammar = Statement(
UseIdent("name"), UseIdent("name"),
Optional([ Optional(
[
"::", "::",
UseIdent("detail_name").expected("a signal detail name"), 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"), UseIdent("handler").expected("the name of a function to handle the signal"),
Match("(").expected("argument list"), Match("(").expected("argument list"),
Optional(UseIdent("object")).expected("object identifier"), Optional(UseIdent("object")).expected("object identifier"),
Match(")").expected(), Match(")").expected(),
ZeroOrMore(AnyOf( ZeroOrMore(SignalFlag),
[Keyword("swapped"), UseLiteral("swapped", True)], Mark("detail_end"),
[Keyword("after"), UseLiteral("after", True)],
)),
) )
@property @property
@ -49,6 +95,13 @@ class Signal(AstNode):
def detail_name(self) -> T.Optional[str]: def detail_name(self) -> T.Optional[str]:
return self.tokens["detail_name"] 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 @property
def handler(self) -> str: def handler(self) -> str:
return self.tokens["handler"] return self.tokens["handler"]
@ -58,68 +111,139 @@ class Signal(AstNode):
return self.tokens["object"] return self.tokens["object"]
@property @property
def is_swapped(self) -> bool: def flags(self) -> T.List[SignalFlag]:
return self.tokens["swapped"] or False 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 @property
def is_after(self) -> bool: def is_after(self) -> bool:
return self.tokens["after"] or False return any(x.flag == "after" for x in self.flags)
@property @property
def gir_signal(self): def gir_signal(self) -> T.Optional[gir.Signal]:
if self.gir_class is not None: if self.gir_class is not None and not isinstance(self.gir_class, ExternType):
return self.gir_class.signals.get(self.tokens["name"]) return self.gir_class.signals.get(self.tokens["name"])
else:
return None
@property @property
def gir_class(self): def gir_class(self):
return self.parent.parent.gir_class 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") @validate("name")
def signal_exists(self): 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 # Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself # This happens for classes defined by the app itself
return 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: if self.gir_signal is None:
raise CompileError( raise CompileError(
f"Class {self.gir_class.full_name} does not contain a signal called {self.tokens['name']}", 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") @validate("object")
def object_exists(self): def object_exists(self):
object_id = self.tokens["object"] object_id = self.tokens["object"]
if object_id is None: if object_id is None:
return return
if self.root.objects_by_id.get(object_id) is None: if self.context[ScopeCtx].objects.get(object_id) is None:
raise CompileError( raise CompileError(f"Could not find object with ID '{object_id}'")
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") @docs("name")
def signal_docs(self): def signal_docs(self):
if self.gir_signal is not None: if self.gir_signal is not None:
return self.gir_signal.doc return self.gir_signal.doc
@docs("detail_name")
def detail_docs(self):
if self.name == "notify":
if self.gir_class is not None and not isinstance(
self.gir_class, ExternType
):
prop = self.gir_class.properties.get(self.tokens["detail_name"])
if prop is not None:
return prop.doc
@docs("=>")
def ref_docs(self):
return get_docs_section("Syntax Signal")
@decompiler("signal") @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 "" object_name = object or ""
if object_name == ctx.template_class:
object_name = "template"
name = name.replace("_", "-") name = name.replace("_", "-")
line = f"{name} => ${handler}({object_name})"
if decompile.truthy(swapped): if decompile.truthy(swapped):
ctx.print(f"{name} => {handler}({object_name}) swapped;") line += " swapped"
else: elif swapped is not None:
ctx.print(f"{name} => {handler}({object_name});") line += " not-swapped"
if decompile.truthy(after):
line += " after"
line += ";"
ctx.print(line)
return gir return gir

View file

@ -17,10 +17,12 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type import typing as T
from .attributes import BaseTypedAttribute
from .values import Value
from .common import * from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type
from .values import Value
def get_property_types(gir): def get_property_types(gir):
@ -86,6 +88,7 @@ def get_state_types(gir):
"selected": BoolType(), "selected": BoolType(),
} }
def get_types(gir): def get_types(gir):
return { return {
**get_property_types(gir), **get_property_types(gir),
@ -93,7 +96,19 @@ def get_types(gir):
**get_state_types(gir), **get_state_types(gir),
} }
allow_duplicates = [
"controls",
"described-by",
"details",
"flow-to",
"labelled-by",
"owns",
]
def _get_docs(gir, name): def _get_docs(gir, name):
name = name.replace("-", "_")
if gir_type := ( if gir_type := (
gir.get_type("AccessibleProperty", "Gtk").members.get(name) gir.get_type("AccessibleProperty", "Gtk").members.get(name)
or gir.get_type("AccessibleRelation", "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 return gir_type.doc
class A11yProperty(BaseTypedAttribute): class A11yProperty(AstNode):
grammar = Statement( grammar = Statement(
UseIdent("name"), UseIdent("name"),
":", ":",
VALUE_HOOKS.expected("a value"), AnyOf(Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]),
) )
@property @property
@ -127,8 +142,22 @@ class A11yProperty(BaseTypedAttribute):
return self.tokens["name"].replace("_", "-") return self.tokens["name"].replace("_", "-")
@property @property
def value_type(self) -> GirType: def values(self) -> T.List[Value]:
return get_types(self.root.gir).get(self.tokens["name"]) 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") @validate("name")
def is_valid_property(self): def is_valid_property(self):
@ -146,19 +175,46 @@ class A11yProperty(BaseTypedAttribute):
check=lambda child: child.tokens["name"] == self.tokens["name"], 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") @docs("name")
def prop_docs(self): def prop_docs(self):
if self.tokens["name"] in get_types(self.root.gir): if self.tokens["name"] in get_types(self.root.gir):
return _get_docs(self.root.gir, self.tokens["name"]) return _get_docs(self.root.gir, self.tokens["name"])
class A11y(AstNode): class ExtAccessibility(AstNode):
grammar = [ grammar = [
Keyword("accessibility"), Keyword("accessibility"),
"{", "{",
Until(A11yProperty, "}"), 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") @validate("accessibility")
def container_is_widget(self): def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "accessibility properties") validate_parent_type(self, "Gtk", "Widget", "accessibility properties")
@ -167,38 +223,65 @@ class A11y(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate accessibility block") self.validate_unique_in_parent("Duplicate accessibility block")
@docs("accessibility")
def ref_docs(self):
return get_docs_section("Syntax ExtAccessibility")
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def a11y_completer(ast_node, match_variables): def a11y_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion(
"accessibility", CompletionItemKind.Snippet, "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}"
snippet="accessibility {\n $0\n}"
) )
@completer( @completer(
applies_in=[A11y], applies_in=[ExtAccessibility],
matches=new_statement_patterns, 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(): 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) @decompiler("accessibility", skip_children=True, element=True)
def decompile_relation(ctx, gir, name, cdata): def decompile_accessibility(ctx: DecompileCtx, _gir, element):
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):
ctx.print("accessibility {") 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 # SPDX-License-Identifier: LGPL-3.0-or-later
from .attributes import BaseTypedAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .common import * from .common import *
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue
class Item(BaseTypedAttribute): class Item(AstNode):
tag_name = "item" grammar = [
attr_name = "id" Optional([UseIdent("name"), ":"]),
StringValue,
]
@property @property
def value_type(self): def name(self) -> T.Optional[str]:
return StringType() return self.tokens["name"]
@property
def value(self) -> StringValue:
return self.children[StringValue][0]
item = Group( @property
Item, def document_symbol(self) -> DocumentSymbol:
[ return DocumentSymbol(
Optional([ self.value.range.text,
UseIdent("name"), SymbolKind.String,
":", self.range,
]), self.value.range,
VALUE_HOOKS, 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 = [ grammar = [
Keyword("items"), 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") @validate("items")
def container_is_combo_box_text(self): def container_is_combo_box_text(self):
validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items") validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items")
@ -60,14 +84,41 @@ class Items(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate items block") self.validate_unique_in_parent("Duplicate items block")
@docs("items")
def ref_docs(self):
return get_docs_section("Syntax ExtComboBoxItems")
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "ComboBoxText"), applies_in_subclass=("Gtk", "ComboBoxText"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def items_completer(ast_node, match_variables): def items_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]")
"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,38 +18,63 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .common import * from .common import *
from .gobject_object import ObjectContent, validate_parent_type
class Filters(AstNode): 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() @validate()
def container_is_file_filter(self): def container_is_file_filter(self):
validate_parent_type(self, "Gtk", "FileFilter", "file filter properties") validate_parent_type(self, "Gtk", "FileFilter", "file filter properties")
@validate() @validate("tag_name")
def unique_in_parent(self): 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( self.validate_unique_in_parent(
f"Duplicate {self.tokens['tag_name']} block", f"Duplicate {self.tokens['tag_name']} block",
check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"], check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"],
) )
wrapped_validator(self)
@docs("tag_name")
def ref_docs(self):
return get_docs_section("Syntax ExtFileFilter")
class FilterString(AstNode): class FilterString(AstNode):
pass @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): def create_node(tag_name: str, singular: str):
return Group( return Group(
Filters, Filters,
[ [
Keyword(tag_name), UseExact("tag_name", tag_name),
UseLiteral("tag_name", tag_name),
"[", "[",
Delimited( Delimited(
Group( Group(
@ -57,18 +82,18 @@ def create_node(tag_name: str, singular: str):
[ [
UseQuoted("name"), UseQuoted("name"),
UseLiteral("tag_name", singular), UseLiteral("tag_name", singular),
] ],
), ),
",", ",",
), ),
"]", "]",
] ],
) )
mime_types = create_node("mime-types", "mime-type") ext_file_filter_mime_types = create_node("mime-types", "mime-type")
patterns = create_node("patterns", "pattern") ext_file_filter_patterns = create_node("patterns", "pattern")
suffixes = create_node("suffixes", "suffix") ext_file_filter_suffixes = create_node("suffixes", "suffix")
@completer( @completer(
@ -76,32 +101,39 @@ suffixes = create_node("suffixes", "suffix")
applies_in_subclass=("Gtk", "FileFilter"), applies_in_subclass=("Gtk", "FileFilter"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def file_filter_completer(ast_node, match_variables): def file_filter_completer(lsp, ast_node, match_variables):
yield Completion("mime-types", CompletionItemKind.Snippet, snippet="mime-types [\"$0\"]") yield Completion(
yield Completion("patterns", CompletionItemKind.Snippet, snippet="patterns [\"$0\"]") "mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]'
yield Completion("suffixes", CompletionItemKind.Snippet, snippet="suffixes [\"$0\"]") )
yield Completion("patterns", CompletionItemKind.Snippet, snippet='patterns ["$0"]')
yield Completion("suffixes", CompletionItemKind.Snippet, snippet='suffixes ["$0"]')
@decompiler("mime-types") @decompiler("mime-types")
def decompile_mime_types(ctx, gir): def decompile_mime_types(ctx, gir):
ctx.print("mime-types [") ctx.print("mime-types [")
@decompiler("mime-type", cdata=True) @decompiler("mime-type", cdata=True)
def decompile_mime_type(ctx, gir, cdata): def decompile_mime_type(ctx, gir, cdata):
ctx.print(f'"{cdata}",') ctx.print(f"{escape_quote(cdata)},")
@decompiler("patterns") @decompiler("patterns")
def decompile_patterns(ctx, gir): def decompile_patterns(ctx, gir):
ctx.print("patterns [") ctx.print("patterns [")
@decompiler("pattern", cdata=True) @decompiler("pattern", cdata=True)
def decompile_pattern(ctx, gir, cdata): def decompile_pattern(ctx, gir, cdata):
ctx.print(f'"{cdata}",') ctx.print(f"{escape_quote(cdata)},")
@decompiler("suffixes") @decompiler("suffixes")
def decompile_suffixes(ctx, gir): def decompile_suffixes(ctx, gir):
ctx.print("suffixes [") ctx.print("suffixes [")
@decompiler("suffix", cdata=True) @decompiler("suffix", cdata=True)
def decompile_suffix(ctx, gir, cdata): 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 # SPDX-License-Identifier: LGPL-3.0-or-later
from .attributes import BaseAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .common import * 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" tag_name = "property"
@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 # there isn't really a way to validate these
return None return ValueTypeCtx(None)
@validate("name") @validate("name")
def unique_in_parent(self): def unique_in_parent(self):
@ -39,21 +59,20 @@ class LayoutProperty(BaseAttribute):
) )
layout_prop = Group( class ExtLayout(AstNode):
LayoutProperty,
Statement(
UseIdent("name"),
":",
VALUE_HOOKS.expected("a value"),
)
)
class Layout(AstNode):
grammar = Sequence( grammar = Sequence(
Keyword("layout"), 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") @validate("layout")
@ -64,17 +83,18 @@ class Layout(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate layout block") self.validate_unique_in_parent("Duplicate layout block")
@docs("layout")
def ref_docs(self):
return get_docs_section("Syntax ExtLayout")
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Widget"), applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def layout_completer(ast_node, match_variables): def layout_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}")
"layout", CompletionItemKind.Snippet,
snippet="layout {\n $0\n}"
)
@decompiler("layout") @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

@ -19,11 +19,11 @@
import typing as T import typing as T
from blueprintcompiler.language.values import Value from blueprintcompiler.language.values import StringValue
from .attributes import BaseAttribute
from .gobject_object import Object, ObjectContent
from .common import * from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import RESERVED_IDS
class Menu(AstNode): class Menu(AstNode):
@ -35,164 +35,239 @@ class Menu(AstNode):
def id(self) -> str: def id(self) -> str:
return self.tokens["id"] return self.tokens["id"]
@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 @property
def tag(self) -> str: def tag(self) -> str:
return self.tokens["tag"] return self.tokens["tag"]
@property
def items(self) -> T.List[T.Union["Menu", "MenuAttribute"]]:
return self.children
class MenuAttribute(BaseAttribute): @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" tag_name = "attribute"
@property @property
def value_type(self): def name(self) -> str:
return None return self.tokens["name"]
@property @property
def value(self) -> Value: def value(self) -> StringValue:
return self.children[Value][0] return self.children[StringValue][0]
@property
menu_contents = Sequence() def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
menu_section = Group( self.name,
Menu, SymbolKind.Field,
[ self.range,
"section", (
UseLiteral("tag", "section"), self.group.tokens["name"].range
Optional(UseIdent("id")), if self.group.tokens["name"]
menu_contents else self.range
] ),
self.value.range.text,
) )
menu_submenu = Group( @context(ValueTypeCtx)
Menu, def value_type(self) -> ValueTypeCtx:
[ return ValueTypeCtx(None)
"submenu",
UseLiteral("tag", "submenu"), @validate("name")
Optional(UseIdent("id")), def unique(self):
menu_contents self.validate_unique_in_parent(
] f"Duplicate attribute '{self.name}'", lambda x: x.name == self.name
) )
menu_child = AnyOf()
menu_attribute = Group( menu_attribute = Group(
MenuAttribute, MenuAttribute,
[ [
UseIdent("name"), UseIdent("name"),
":", ":",
VALUE_HOOKS.expected("a value"), Err(StringValue, "Expected string or translated string"),
Match(";").expected(), 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 = Group(
Menu, Menu,
[ [
"item", Keyword("item"),
UseLiteral("tag", "item"), UseLiteral("tag", "item"),
Optional(UseIdent("id")),
Match("{").expected(), Match("{").expected(),
Until(menu_attribute, "}"), Until(menu_attribute, "}"),
] ],
) )
menu_item_shorthand = Group( menu_item_shorthand = Group(
Menu, Menu,
[ [
"item", Keyword("item"),
UseLiteral("tag", "item"), UseLiteral("tag", "item"),
UseLiteral("shorthand", True),
"(", "(",
Group( Group(
MenuAttribute, MenuAttribute,
[UseLiteral("name", "label"), VALUE_HOOKS], [UseLiteral("name", "label"), StringValue],
), ),
Optional([ Optional(
[
",", ",",
Optional([ Optional(
[
Group( Group(
MenuAttribute, MenuAttribute,
[UseLiteral("name", "action"), VALUE_HOOKS], [UseLiteral("name", "action"), StringValue],
), ),
Optional([ Optional(
[
",", ",",
Group( Group(
MenuAttribute, MenuAttribute,
[UseLiteral("name", "icon"), VALUE_HOOKS], [UseLiteral("name", "icon"), StringValue],
), ),
])
])
]),
Match(")").expected(),
] ]
),
]
),
]
),
Match(")").expected(),
],
) )
menu_contents.children = [ menu_child.children = [
Match("{"),
Until(AnyOf(
menu_section, menu_section,
menu_submenu, menu_submenu,
menu_item_shorthand, menu_item_shorthand,
menu_item, menu_item,
menu_attribute,
), "}"),
] ]
menu: Group = Group( menu: Group = Group(
Menu, Menu,
[ [
"menu", Keyword("menu"),
UseLiteral("tag", "menu"), UseLiteral("tag", "menu"),
Optional(UseIdent("id")), 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 from .ui import UI
@completer( @completer(
applies_in=[UI], applies_in=[UI],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def menu_completer(ast_node, match_variables): def menu_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}")
"menu", CompletionItemKind.Snippet,
snippet="menu {\n $0\n}"
)
@completer( @completer(
applies_in=[Menu], applies_in=[Menu],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def menu_content_completer(ast_node, match_variables): def menu_content_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion(
"submenu", CompletionItemKind.Snippet, "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}"
snippet="submenu {\n $0\n}"
) )
yield Completion( yield Completion(
"section", CompletionItemKind.Snippet, "section", CompletionItemKind.Snippet, snippet="section {\n $0\n}"
snippet="section {\n $0\n}"
) )
yield Completion("item", CompletionItemKind.Snippet, snippet="item {\n $0\n}")
yield Completion( yield Completion(
"item", CompletionItemKind.Snippet, "item (shorthand)",
snippet="item {\n $0\n}" CompletionItemKind.Snippet,
) snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")',
yield Completion(
"item (shorthand)", CompletionItemKind.Snippet,
snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")'
) )
yield Completion( yield Completion("label", CompletionItemKind.Snippet, snippet="label: $0;")
"label", CompletionItemKind.Snippet, yield Completion("action", CompletionItemKind.Snippet, snippet='action: "$0";')
snippet='label: $0;' yield Completion("icon", CompletionItemKind.Snippet, snippet='icon: "$0";')
)
yield Completion(
"action", CompletionItemKind.Snippet,
snippet='action: "$0";'
)
yield Completion(
"icon", CompletionItemKind.Snippet,
snippet='icon: "$0";'
)
@decompiler("menu") @decompiler("menu")
@ -202,6 +277,7 @@ def decompile_menu(ctx, gir, id=None):
else: else:
ctx.print("menu {") ctx.print("menu {")
@decompiler("submenu") @decompiler("submenu")
def decompile_submenu(ctx, gir, id=None): def decompile_submenu(ctx, gir, id=None):
if id: if id:
@ -209,13 +285,15 @@ def decompile_submenu(ctx, gir, id=None):
else: else:
ctx.print("submenu {") ctx.print("submenu {")
@decompiler("item")
@decompiler("item", parent_tag="menu")
def decompile_item(ctx, gir, id=None): def decompile_item(ctx, gir, id=None):
if id: if id:
ctx.print(f"item {id} {{") ctx.print(f"item {id} {{")
else: else:
ctx.print("item {") ctx.print("item {")
@decompiler("section") @decompiler("section")
def decompile_section(ctx, gir, id=None): def decompile_section(ctx, gir, id=None):
if id: 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,29 +18,58 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .common import * from .common import *
from .contexts import ScopeCtx
from .gobject_object import ObjectContent, validate_parent_type
class Widget(AstNode): class Widget(AstNode):
grammar = UseIdent("name") 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") @validate("name")
def obj_widget(self): 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") type = self.root.gir.get_type("Widget", "Gtk")
if object is None: if object is None:
raise CompileError( raise CompileError(
f"Could not find object with ID {self.tokens['name']}", 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): elif object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError( raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}" 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"Object '{self.name}' is listed twice", lambda x: x.name == self.name
)
class Widgets(AstNode):
class ExtSizeGroupWidgets(AstNode):
grammar = [ grammar = [
Keyword("widgets"), Keyword("widgets"),
"[", "[",
@ -48,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") @validate("widgets")
def container_is_size_group(self): def container_is_size_group(self):
validate_parent_type(self, "Gtk", "SizeGroup", "size group properties") validate_parent_type(self, "Gtk", "SizeGroup", "size group properties")
@ -56,11 +94,25 @@ class Widgets(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate widgets block") self.validate_unique_in_parent("Duplicate widgets block")
@docs("widgets")
def ref_docs(self):
return get_docs_section("Syntax ExtSizeGroupWidgets")
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "SizeGroup"), applies_in_subclass=("Gtk", "SizeGroup"),
matches=new_statement_patterns, 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]") 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,21 +18,29 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # 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 .common import *
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue
class Item(AstNode): class Item(AstNode):
grammar = VALUE_HOOKS grammar = StringValue
@property @property
def value_type(self): def child(self) -> StringValue:
return StringType() return self.children[StringValue][0]
@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 = [ grammar = [
Keyword("strings"), Keyword("strings"),
"[", "[",
@ -40,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): def container_is_string_list(self):
validate_parent_type(self, "Gtk", "StringList", "StringList items") validate_parent_type(self, "Gtk", "StringList", "StringList items")
@ -48,14 +65,37 @@ class Strings(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate strings block") self.validate_unique_in_parent("Duplicate strings block")
@docs("strings")
def ref_docs(self):
return get_docs_section("Syntax ExtStringListStrings")
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "StringList"), applies_in_subclass=("Gtk", "StringList"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def strings_completer(ast_node, match_variables): def strings_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]")
"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,15 +18,34 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .common import * from .common import *
from .gobject_object import ObjectContent, validate_parent_type
class StyleClass(AstNode): class StyleClass(AstNode):
grammar = UseQuoted("name") grammar = UseQuoted("name")
@property
def name(self) -> str:
return self.tokens["name"]
class Styles(AstNode): @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 ExtStyles(AstNode):
grammar = [ grammar = [
Keyword("styles"), Keyword("styles"),
"[", "[",
@ -34,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") @validate("styles")
def container_is_widget(self): def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "style classes") validate_parent_type(self, "Gtk", "Widget", "style classes")
@ -42,20 +70,25 @@ class Styles(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate styles block") self.validate_unique_in_parent("Duplicate styles block")
@docs("styles")
def ref_docs(self):
return get_docs_section("Syntax ExtStyles")
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Widget"), applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def style_completer(ast_node, match_variables): def style_completer(lsp, ast_node, match_variables):
yield Completion("styles", CompletionItemKind.Keyword, snippet="styles [\"$0\"]") yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]')
@decompiler("style") @decompiler("style")
def decompile_style(ctx, gir): def decompile_style(ctx, gir):
ctx.print(f"styles [") ctx.print(f"styles [")
@decompiler("class") @decompiler("class")
def decompile_style_class(ctx, gir, name): def decompile_style_class(ctx, gir, name):
ctx.print(f'"{name}",') ctx.print(f'"{name}",')

View file

@ -20,27 +20,63 @@
from functools import cached_property from functools import cached_property
from .gobject_object import Object
from .response_id import ResponseId
from .common import * from .common import *
from .gobject_object import Object
from .response_id import ExtResponse, decompile_response_type
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [
("Gtk", "Buildable"), ("Gtk", "Buildable"),
("Gio", "ListStore") ("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): class Child(AstNode):
grammar = [ grammar = [
Optional([ Optional(ChildAnnotation),
"[",
Optional(["internal-child", UseLiteral("internal_child", True)]),
UseIdent("child_type").expected("a child type"),
Optional(ResponseId),
"]",
]),
Object, Object,
] ]
@property
def annotation(self) -> T.Optional[ChildAnnotation]:
annotations = self.children[ChildAnnotation]
return annotations[0] if len(annotations) else None
@property @property
def object(self) -> Object: def object(self) -> Object:
return self.children[Object][0] return self.children[Object][0]
@ -53,32 +89,57 @@ class Child(AstNode):
if gir_class.assignable_to(parent_type): if gir_class.assignable_to(parent_type):
break break
else: else:
hints=["only Gio.ListStore or Gtk.Buildable implementors can have children"] hints = [
if "child" in gir_class.properties: "only Gio.ListStore or Gtk.Buildable implementors can have children"
hints.append("did you mean to assign this object to the 'child' property?") ]
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( raise CompileError(
f"{gir_class.full_name} doesn't have children", f"{gir_class.full_name} doesn't have children",
hints=hints, hints=hints,
) )
@cached_property @cached_property
def response_id(self) -> T.Optional[ResponseId]: def response_id(self) -> T.Optional[ExtResponse]:
"""Get action widget's response ID. """Get action widget's response ID.
If child is not action widget, returns `None`. If child is not action widget, returns `None`.
""" """
response_ids = self.children[ResponseId] if (
self.annotation is not None
if response_ids: and isinstance(self.annotation.child, ChildExtension)
return response_ids[0] and isinstance(self.annotation.child.child, ExtResponse)
):
return self.annotation.child.child
else: else:
return None return None
@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", 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
@decompiler("child")
def decompile_child(ctx, gir, type=None, internal_child=None):
if type is not None:
ctx.print(f"[{type}]") ctx.print(f"[{type}]")
elif internal_child is not None: elif internal_child := element["internal-child"]:
ctx.print(f"[internal-child {internal_child}]") ctx.print(f"[internal-child {internal_child}]")
return gir return gir

View file

@ -19,49 +19,91 @@
import typing as T import typing as T
from .gobject_object import Object, ObjectContent from blueprintcompiler.language.common import GirType
from ..gir import TemplateType
from .common import * from .common import *
from .types import ClassName from .gobject_object import Object, ObjectContent
from .types import ClassName, TemplateClassName
class Template(Object): class Template(Object):
grammar = [ grammar = [
"template", UseExact("id", "template"),
UseIdent("id").expected("template class name"), to_parse_node(TemplateClassName).expected("template type"),
Optional([ Optional(
[
Match(":"), Match(":"),
to_parse_node(ClassName).expected("parent class"), to_parse_node(ClassName).expected("parent class"),
]), ]
),
ObjectContent, ObjectContent,
] ]
@property @property
def id(self) -> str: def id(self) -> str:
return self.tokens["id"] return "template"
@property @property
def class_name(self) -> T.Optional[ClassName]: def signature(self) -> str:
if len(self.children[ClassName]): if self.parent_type and self.parent_type.gir_type:
return self.children[ClassName][0] return f"template {self.class_name.as_string} : {self.parent_type.gir_type.full_name}"
else:
return f"template {self.class_name.as_string}"
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.signature,
SymbolKind.Object,
self.range,
self.group.tokens["id"].range,
)
@property
def gir_class(self) -> GirType:
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: else:
return None return None
@property @validate()
def gir_class(self): def parent_only_if_extern(self):
# Templates might not have a parent class defined if not isinstance(self.class_name.gir_type, ExternType):
if class_name := self.class_name: if self.parent_type is not None:
return class_name.gir_type raise CompileError(
"Parent type may only be specified if the template type is extern"
)
@validate("id") @validate("id")
def unique_in_parent(self): 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])}",) 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") @decompiler("template")
def decompile_template(ctx: DecompileCtx, gir, klass, parent="Widget"): def decompile_template(ctx: DecompileCtx, gir, klass, parent=None):
gir_class = ctx.type_by_cname(parent) def class_name(cname: str) -> str:
if gir_class is None: if gir := ctx.type_by_cname(cname):
ctx.print(f"template {klass} : .{parent} {{") return decompile.full_name(gir)
else: else:
ctx.print(f"template {klass} : {decompile.full_name(gir_class)} {{") return "$" + cname
return gir_class
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,8 +24,12 @@ from .common import *
class GtkDirective(AstNode): class GtkDirective(AstNode):
grammar = Statement( grammar = Statement(
Match("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), Match("using").err(
Match("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), '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"), UseNumberText("version").expected("a version number for GTK"),
) )
@ -35,7 +39,9 @@ class GtkDirective(AstNode):
if version not in ["4.0"]: if version not in ["4.0"]:
err = CompileError("Only GTK 4 is supported") err = CompileError("Only GTK 4 is supported")
if version and version.startswith("4"): if version and version.startswith("4"):
err.hint("Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'.") err.hint(
"Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'."
)
else: else:
err.hint("Expected 'using Gtk 4.0;'") err.hint("Expected 'using Gtk 4.0;'")
raise err raise err
@ -51,14 +57,14 @@ class GtkDirective(AstNode):
hints=e.hints, hints=e.hints,
) )
@property @property
def gir_namespace(self): def gir_namespace(self):
# validate the GTK version first to make sure the more specific error # For better error handling, just assume it's 4.0
# message is emitted return gir.get_namespace("Gtk", "4.0")
self.gtk_version()
if self.tokens["version"] is not None: @docs()
return gir.get_namespace("Gtk", self.tokens["version"]) def ref_docs(self):
return get_docs_section("Syntax GtkDecl")
class Import(AstNode): class Import(AstNode):
@ -68,13 +74,36 @@ class Import(AstNode):
UseNumberText("version").expected("a version number"), 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") @validate("namespace", "version")
def namespace_exists(self): def namespace_exists(self):
gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) gir.get_namespace(self.namespace, self.version)
@validate()
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 @property
def gir_namespace(self): def gir_namespace(self):
try: try:
return gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) return gir.get_namespace(self.namespace, self.version)
except CompileError: except CompileError:
return None return None
@docs()
def ref_docs(self):
return get_docs_section("Syntax Using")

View file

@ -23,33 +23,25 @@ import typing as T
from .common import * from .common import *
class ResponseId(AstNode): class ExtResponse(AstNode):
"""Response ID of action widget.""" """Response ID of action widget."""
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")]
("Gtk", "Dialog"),
("Gtk", "InfoBar")
]
grammar = [ grammar = [
Keyword("action"),
Keyword("response"), Keyword("response"),
"=", "=",
AnyOf( AnyOf(
UseIdent("response_id"), UseIdent("response_id"),
UseNumber("response_id") [
Optional(UseExact("sign", "-")),
UseNumber("response_id"),
],
), ),
Optional([ Optional([Keyword("default"), UseLiteral("is_default", True)]),
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() @validate()
def parent_has_action_widgets(self) -> None: def parent_has_action_widgets(self) -> None:
"""Chech that parent widget has allowed type.""" """Chech that parent widget has allowed type."""
@ -61,7 +53,7 @@ class ResponseId(AstNode):
gir = self.root.gir 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) parent_type = gir.get_type(name, namespace)
if container_type.assignable_to(parent_type): if container_type.assignable_to(parent_type):
break break
@ -73,10 +65,10 @@ class ResponseId(AstNode):
@validate() @validate()
def widget_have_id(self) -> None: def widget_have_id(self) -> None:
"""Check that action widget have ID.""" """Check that action widget have ID."""
from .gobject_object import Object from .gtkbuilder_child import Child
_object = self.parent.children[Object][0] object = self.parent_by_type(Child).object
if _object.tokens["id"] is None: if object.id is None:
raise CompileError(f"Action widget must have ID") raise CompileError(f"Action widget must have ID")
@validate("response_id") @validate("response_id")
@ -89,28 +81,24 @@ class ResponseId(AstNode):
gir = self.root.gir gir = self.root.gir
response = self.tokens["response_id"] response = self.tokens["response_id"]
if isinstance(response, int): if self.tokens["sign"] == "-":
if response < 0: raise CompileError("Numeric response type can't be negative")
if isinstance(response, float):
raise CompileError( raise CompileError(
"Numeric response type can't be negative") "Response type must be GtkResponseType member or integer," " not float"
elif isinstance(response, float):
raise CompileError(
"Response type must be GtkResponseType member or integer,"
" not float"
) )
else: elif not isinstance(response, int):
responses = gir.get_type("ResponseType", "Gtk").members.keys() responses = gir.get_type("ResponseType", "Gtk").members.keys()
if response not in responses: if response not in responses:
raise CompileError( raise CompileError(f'Response type "{response}" doesn\'t exist')
f"Response type \"{response}\" doesn't exist")
@validate("default") @validate("default")
def no_multiple_default(self) -> None: def no_multiple_default(self) -> None:
"""Only one action widget in dialog can be default.""" """Only one action widget in dialog can be default."""
from .gtkbuilder_child import Child
from .gobject_object import Object from .gobject_object import Object
if not self.tokens["is_default"]: if not self.is_default:
return return
action_widgets = self.parent_by_type(Object).action_widgets action_widgets = self.parent_by_type(Object).action_widgets
@ -131,8 +119,46 @@ class ResponseId(AstNode):
@property @property
def widget_id(self) -> str: def widget_id(self) -> str:
"""Get action widget ID.""" """Get action widget ID."""
from .gobject_object import Object from .gtkbuilder_child import Child
_object: Object = self.parent.children[Object][0] object = self.parent_by_type(Child).object
return _object.tokens["id"] return object.id
@docs()
def ref_docs(self):
return get_docs_section("Syntax ExtResponse")
@docs("response_id")
def response_id_docs(self):
if enum := self.root.gir.get_type("ResponseType", "Gtk"):
if member := enum.members.get(self.response_id, None):
return member.doc
def decompile_response_type(parent_element, child_element):
obj_id = None
for obj in child_element.children:
if obj.tag == "object":
obj_id = obj["id"]
break
if obj_id is None:
return None
for child in parent_element.children:
if child.tag == "action-widgets":
for action_widget in child.children:
if action_widget.cdata == obj_id:
response_id = action_widget["response"]
is_default = (
" default" if decompile.truthy(action_widget["default"]) else ""
)
return f"[action response={response_id}{is_default}]"
return None
@decompiler("action-widgets", skip_children=True)
def decompile_action_widgets(ctx, gir):
# This is handled in the <child> decompiler and decompile_response_type above
pass

View file

@ -1,4 +1,4 @@
# attributes.py # translation_domain.py
# #
# Copyright 2022 James Westman <james@jwestman.net> # Copyright 2022 James Westman <james@jwestman.net>
# #
@ -17,22 +17,19 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .values import Value, TranslatedStringValue
from .common import * from .common import *
class BaseAttribute(AstNode): class TranslationDomain(AstNode):
""" A helper class for attribute syntax of the form `name: literal_value;`""" grammar = Statement(
"translation-domain",
tag_name: str = "" UseQuoted("domain"),
attr_name: str = "name" )
@property @property
def name(self): def domain(self):
return self.tokens["name"] return self.tokens["domain"]
@docs()
class BaseTypedAttribute(BaseAttribute): def ref_docs(self):
""" A BaseAttribute whose parent has a value_type property that can assist return get_docs_section("Syntax TranslationDomain")
in validation. """

View file

@ -18,9 +18,8 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T from ..gir import Class, ExternType, Interface
from .common import * from .common import *
from ..gir import Class, Interface
class TypeName(AstNode): class TypeName(AstNode):
@ -31,33 +30,67 @@ class TypeName(AstNode):
UseIdent("class_name"), UseIdent("class_name"),
], ],
[ [
".", AnyOf("$", [".", UseLiteral("old_extern", True)]),
UseIdent("class_name"), UseIdent("class_name"),
UseLiteral("ignore_gir", True), UseLiteral("extern", True),
], ],
UseIdent("class_name"), 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") @validate("class_name")
def type_exists(self): def type_exists(self):
if not self.tokens["ignore_gir"] and self.gir_ns is not None: if not self.tokens["extern"] and self.gir_ns is not None:
self.root.gir.validate_type(self.tokens["class_name"], self.tokens["namespace"]) self.root.gir.validate_type(
self.tokens["class_name"], self.tokens["namespace"]
)
@validate("namespace") @validate("namespace")
def gir_ns_exists(self): def gir_ns_exists(self):
if not self.tokens["ignore_gir"]: if not self.tokens["extern"]:
try:
self.root.gir.validate_ns(self.tokens["namespace"]) 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 @property
def gir_ns(self): def gir_ns(self) -> T.Optional[gir.Namespace]:
if not self.tokens["ignore_gir"]: if not self.tokens["extern"]:
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
return None
@property @property
def gir_type(self) -> T.Optional[gir.Class]: def gir_type(self) -> gir.GirType:
if self.tokens["class_name"] and not self.tokens["ignore_gir"]: if self.tokens["class_name"] and not self.tokens["extern"]:
return self.root.gir.get_type(self.tokens["class_name"], self.tokens["namespace"]) return self.root.gir.get_type(
return None self.tokens["class_name"], self.tokens["namespace"]
)
return gir.ExternType(self.tokens["class_name"])
@property @property
def glib_type_name(self) -> str: def glib_type_name(self) -> str:
@ -76,13 +109,28 @@ class TypeName(AstNode):
if self.gir_type: if self.gir_type:
return self.gir_type.doc 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): class ClassName(TypeName):
@validate("namespace", "class_name") @validate("namespace", "class_name")
def gir_class_exists(self): def gir_class_exists(self):
if self.gir_type is not None and not isinstance(self.gir_type, Class): 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): if isinstance(self.gir_type, Interface):
raise CompileError(f"{self.gir_type.full_name} is an interface, not a class") raise CompileError(
f"{self.gir_type.full_name} is an interface, not a class"
)
else: else:
raise CompileError(f"{self.gir_type.full_name} is not a class") raise CompileError(f"{self.gir_type.full_name} is not a class")
@ -93,6 +141,44 @@ class ConcreteClassName(ClassName):
if isinstance(self.gir_type, Class) and self.gir_type.abstract: if isinstance(self.gir_type, Class) and self.gir_type.abstract:
raise CompileError( raise CompileError(
f"{self.gir_type.full_name} can't be instantiated because it's abstract", 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}?"] 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,13 +17,17 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from functools import cached_property
from .. import gir from .. import gir
from .imports import GtkDirective, Import
from .gtkbuilder_template import Template
from .gobject_object import Object
from .gtk_menu import menu
from .common import * 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): class UI(AstNode):
@ -32,15 +36,19 @@ class UI(AstNode):
grammar = [ grammar = [
GtkDirective, GtkDirective,
ZeroOrMore(Import), ZeroOrMore(Import),
Until(AnyOf( Optional(TranslationDomain),
Until(
AnyOf(
Template, Template,
menu, menu,
Object, Object,
), Eof()), ),
Eof(),
),
] ]
@property @cached_property
def gir(self): def gir(self) -> gir.GirContext:
gir_ctx = gir.GirContext() gir_ctx = gir.GirContext()
self._gir_errors = [] self._gir_errors = []
@ -54,18 +62,85 @@ class UI(AstNode):
try: try:
if i.gir_namespace is not None: if i.gir_namespace is not None:
gir_ctx.add_namespace(i.gir_namespace) gir_ctx.add_namespace(i.gir_namespace)
else:
gir_ctx.not_found_namespaces.add(i.namespace)
except CompileError as e: except CompileError as e:
e.start = i.group.tokens["namespace"].start e.range = i.range
e.end = i.group.tokens["version"].end
self._gir_errors.append(e) self._gir_errors.append(e)
return gir_ctx return gir_ctx
@property
def using(self) -> T.List[Import]:
return self.children[Import]
@property @property
def objects_by_id(self): def gtk_decl(self) -> GtkDirective:
return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None } 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() @validate()
def gir_errors(self): def gir_errors(self):
@ -74,15 +149,6 @@ class UI(AstNode):
if len(self._gir_errors): if len(self._gir_errors):
raise MultipleErrors(self._gir_errors) raise MultipleErrors(self._gir_errors)
@validate() @validate()
def unique_ids(self): def unique_ids(self):
passed = {} self.context[ScopeCtx].validate_unique_ids()
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

View file

@ -19,83 +19,146 @@
import typing as T import typing as T
from blueprintcompiler.gir import ArrayType
from blueprintcompiler.lsp_utils import SemanticToken
from .common import * from .common import *
from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx
from .expression import Expression
from .gobject_object import Object
from .types import TypeName from .types import TypeName
class Value(AstNode): class Translated(AstNode):
pass
class TranslatedStringValue(Value):
grammar = AnyOf( grammar = AnyOf(
[ ["_", "(", UseQuoted("string"), ")"],
"_",
"(",
UseQuoted("value").expected("a quoted string"),
Match(")").expected(),
],
[ [
"C_", "C_",
"(", "(",
UseQuoted("context").expected("a quoted string"), UseQuoted("context"),
",", ",",
UseQuoted("value").expected("a quoted string"), UseQuoted("string"),
Optional(","), ")",
Match(")").expected(),
], ],
) )
@property @property
def string(self) -> str: def string(self) -> str:
return self.tokens["value"] return self.tokens["string"]
@property @property
def context(self) -> T.Optional[str]: def translate_context(self) -> T.Optional[str]:
return self.tokens["context"] return self.tokens["context"]
@validate()
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}"
)
class TypeValue(Value): @validate("context")
def context_double_quoted(self):
if self.translate_context is None:
return
if not str(self.group.tokens["context"]).startswith('"'):
raise CompileWarning("gettext may not recognize single-quoted strings")
@validate("string")
def string_double_quoted(self):
if not str(self.group.tokens["string"]).startswith('"'):
raise CompileWarning("gettext may not recognize single-quoted strings")
@docs()
def ref_docs(self):
return get_docs_section("Syntax Translated")
class TypeLiteral(AstNode):
grammar = [ grammar = [
"typeof", "typeof",
"(", AnyOf(
[
"<",
to_parse_node(TypeName).expected("type name"), to_parse_node(TypeName).expected("type name"),
Match(")").expected(), Match(">").expected(),
],
[
UseExact("lparen", "("),
to_parse_node(TypeName).expected("type name"),
UseExact("rparen", ")").expected("')'"),
],
),
] ]
@property @property
def type_name(self): def type(self):
return gir.TypeType()
@property
def type_name(self) -> TypeName:
return self.children[TypeName][0] return self.children[TypeName][0]
@validate() @validate()
def validate_for_type(self): def validate_for_type(self) -> None:
type = self.parent.value_type expected_type = self.context[ValueTypeCtx].value_type
if type is not None and not isinstance(type, gir.TypeType): if expected_type is not None and not isinstance(expected_type, gir.TypeType):
raise CompileError(f"Cannot convert GType to {type.full_name}") 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 QuotedValue(Value): class QuotedLiteral(AstNode):
grammar = UseQuoted("value") grammar = UseQuoted("value")
@property @property
def value(self) -> str: def value(self) -> str:
return self.tokens["value"] return self.tokens["value"]
@property
def type(self):
return gir.StringType()
@validate() @validate()
def validate_for_type(self): def validate_for_type(self) -> None:
type = self.parent.value_type expected_type = self.context[ValueTypeCtx].value_type
if isinstance(type, gir.IntType) or isinstance(type, gir.UIntType) or isinstance(type, gir.FloatType): 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") raise CompileError(f"Cannot convert string to number")
elif isinstance(type, gir.StringType): elif isinstance(expected_type, gir.StringType):
pass pass
elif isinstance(type, gir.Class) or isinstance(type, gir.Interface) or isinstance(type, gir.Boxed): elif (
isinstance(expected_type, gir.Class)
or isinstance(expected_type, gir.Interface)
or isinstance(expected_type, gir.Boxed)
):
parseable_types = [ parseable_types = [
"Gdk.Paintable", "Gdk.Paintable",
"Gdk.Texture", "Gdk.Texture",
"Gdk.Pixbuf", "Gdk.Pixbuf",
"GLib.File", "Gio.File",
"Gtk.ShortcutTrigger", "Gtk.ShortcutTrigger",
"Gtk.ShortcutAction", "Gtk.ShortcutAction",
"Gdk.RGBA", "Gdk.RGBA",
@ -103,56 +166,84 @@ class QuotedValue(Value):
"Gsk.Transform", "Gsk.Transform",
"GLib.Variant", "GLib.Variant",
] ]
if type.full_name not in parseable_types: if expected_type.full_name not in parseable_types:
hints = [] hints = []
if isinstance(type, gir.TypeType): if isinstance(expected_type, gir.TypeType):
hints.append(f"use the typeof operator: 'typeof({self.tokens('value')})'") hints.append(f"use the typeof operator: 'typeof({self.value})'")
raise CompileError(f"Cannot convert string to {type.full_name}", hints=hints) raise CompileError(
f"Cannot convert string to {expected_type.full_name}", hints=hints
)
elif type is not None: elif expected_type is not None:
raise CompileError(f"Cannot convert string to {type.full_name}") raise CompileError(f"Cannot convert string to {expected_type.full_name}")
class NumberValue(Value): class NumberLiteral(AstNode):
grammar = UseNumber("value") 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 @property
def value(self) -> T.Union[int, float]: def value(self) -> T.Union[int, float]:
if self.tokens["sign"] == "-":
return -self.tokens["value"]
else:
return self.tokens["value"] return self.tokens["value"]
@validate() @validate()
def validate_for_type(self): def validate_for_type(self) -> None:
type = self.parent.value_type expected_type = self.context[ValueTypeCtx].value_type
if isinstance(type, gir.IntType): if isinstance(expected_type, gir.IntType):
try: if not isinstance(self.value, int):
int(self.tokens["value"]) raise CompileError(
except: f"Cannot convert {self.group.tokens['value']} to integer"
raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer") )
elif isinstance(type, gir.UIntType): elif isinstance(expected_type, gir.UIntType):
try: if self.value < 0:
int(self.tokens["value"]) raise CompileError(
if int(self.tokens["value"]) < 0: f"Cannot convert -{self.group.tokens['value']} to unsigned integer"
raise Exception() )
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer")
elif isinstance(type, gir.FloatType): elif not isinstance(expected_type, gir.FloatType) and expected_type is not None:
try: raise CompileError(f"Cannot convert number to {expected_type.full_name}")
float(self.tokens["value"])
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to float")
elif type is not None:
raise CompileError(f"Cannot convert number to {type.full_name}")
class Flag(AstNode): class Flag(AstNode):
grammar = UseIdent("value") 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() @docs()
def docs(self): def docs(self):
type = self.parent.parent.value_type type = self.context[ValueTypeCtx].value_type
if not isinstance(type, Enumeration): if not isinstance(type, Enumeration):
return return
if member := type.members.get(self.tokens["value"]): if member := type.members.get(self.tokens["value"]):
@ -160,72 +251,298 @@ class Flag(AstNode):
@validate() @validate()
def validate_for_type(self): def validate_for_type(self):
type = self.parent.parent.value_type expected_type = self.context[ValueTypeCtx].value_type
if isinstance(type, gir.Bitfield) and self.tokens["value"] not in type.members: if (
isinstance(expected_type, gir.Bitfield)
and self.tokens["value"] not in expected_type.members
):
raise CompileError( raise CompileError(
f"{self.tokens['value']} is not a member of {type.full_name}", f"{self.tokens['value']} is not a member of {expected_type.full_name}",
did_you_mean=(self.tokens['value'], type.members.keys()), did_you_mean=(self.tokens["value"], expected_type.members.keys()),
) )
class FlagsValue(Value):
grammar = [Flag, "|", Delimited(Flag, "|")]
@validate() @validate()
def parent_is_bitfield(self): def unique(self):
type = self.parent.value_type self.validate_unique_in_parent(
if type is not None and not isinstance(type, gir.Bitfield): f"Duplicate flag '{self.name}'", lambda x: x.name == self.name
raise CompileError(f"{type.full_name} is not a bitfield type") )
class IdentValue(Value): class Flags(AstNode):
grammar = UseIdent("value") grammar = [Flag, "|", Flag, ZeroOrMore(["|", Flag])]
@property
def flags(self) -> T.List[Flag]:
return self.children
@validate() @validate()
def validate_for_type(self): def validate_for_type(self) -> None:
type = self.parent.value_type expected_type = self.context[ValueTypeCtx].value_type
if expected_type is not None and not isinstance(expected_type, gir.Bitfield):
if isinstance(type, gir.Enumeration): raise CompileError(f"{expected_type.full_name} is not a bitfield type")
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}"
)
@docs() @docs()
def docs(self): def ref_docs(self):
type = self.parent.value_type return get_docs_section("Syntax Flags")
if isinstance(type, gir.Enumeration):
if member := type.members.get(self.tokens["value"]):
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 return member.doc
else: else:
return type.doc return expected_type.doc
elif isinstance(type, gir.GirNode): elif self.ident == "null" and self.context[ValueTypeCtx].allow_null:
return type.doc 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]: 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"] token = self.group.tokens["value"]
yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) 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,59 +18,78 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import json
import sys
import traceback
import typing as T 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 .completions import complete
from .errors import PrintableError, CompileError, MultipleErrors from .errors import CompileError, MultipleErrors
from .lsp_utils import * from .lsp_utils import *
from . import tokenizer, parser, utils, xml_reader from .outputs.xml import XmlOutput
from .tokenizer import Token
def printerr(*args, **kwargs): def printerr(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs) print(*args, file=sys.stderr, **kwargs)
def command(json_method): def command(json_method: str):
def decorator(func): def decorator(func):
func._json_method = json_method func._json_method = json_method
return func return func
return decorator return decorator
class OpenFile: class OpenFile:
def __init__(self, uri, text, version): def __init__(self, uri: str, text: str, version: int) -> None:
self.uri = uri self.uri = uri
self.text = text self.text = text
self.version = version self.version = version
self.ast = None self.ast: T.Optional[AstNode] = None
self.tokens = None self.tokens: T.Optional[list[Token]] = None
self._update() self._update()
def apply_changes(self, changes): def apply_changes(self, changes) -> None:
for change in changes: for change in changes:
start = utils.pos_to_idx(change["range"]["start"]["line"], change["range"]["start"]["character"], self.text) if "range" not in change:
end = utils.pos_to_idx(change["range"]["end"]["line"], change["range"]["end"]["character"], self.text) 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.text = self.text[:start] + change["text"] + self.text[end:]
self._update() self._update()
def _update(self): def _update(self) -> None:
self.diagnostics = [] self.diagnostics: list[CompileError] = []
try: try:
self.tokens = tokenizer.tokenize(self.text) self.tokens = tokenizer.tokenize(self.text)
self.ast, errors, warnings = parser.parse(self.tokens) self.ast, errors, warnings = parser.parse(self.tokens)
self.diagnostics += warnings self.diagnostics += warnings
if errors is not None: if errors is not None:
self.diagnostics += errors.errors self.diagnostics += errors.errors
self.diagnostics += self.ast.errors
except MultipleErrors as e: except MultipleErrors as e:
self.diagnostics += e.errors self.diagnostics += e.errors
except CompileError as e: except CompileError as e:
self.diagnostics.append(e) self.diagnostics.append(e)
def calc_semantic_tokens(self) -> T.List[int]: def calc_semantic_tokens(self) -> T.List[int]:
if self.ast is None:
return []
tokens = list(self.ast.get_semantic_tokens()) tokens = list(self.ast.get_semantic_tokens())
token_lists = [ token_lists = [
[ [
@ -78,13 +97,15 @@ class OpenFile:
token.end - token.start, # length token.end - token.start, # length
token.type, token.type,
0, # token modifiers 0, # token modifiers
] for token in tokens] ]
for token in tokens
]
# convert line, column numbers to deltas # convert line, column numbers to deltas
for i, token_list in enumerate(token_lists[1:]): for a, b in zip(token_lists[-2::-1], token_lists[:0:-1]):
token_list[0] -= token_lists[i][0] b[0] -= a[0]
if token_list[0] == 0: if b[0] == 0:
token_list[1] -= token_lists[i][1] b[1] -= a[1]
# flatten the list # flatten the list
return [x for y in token_lists for x in y] return [x for y in token_lists for x in y]
@ -95,7 +116,9 @@ class LanguageServer:
def __init__(self): def __init__(self):
self.client_capabilities = {} self.client_capabilities = {}
self.client_supports_completion_choice = False
self._open_files: T.Dict[str, OpenFile] = {} self._open_files: T.Dict[str, OpenFile] = {}
self._exited = False
def run(self): def run(self):
# Read <doc> tags from gir files. During normal compilation these are # Read <doc> tags from gir files. During normal compilation these are
@ -103,7 +126,7 @@ class LanguageServer:
xml_reader.PARSE_GIR.add("doc") xml_reader.PARSE_GIR.add("doc")
try: try:
while True: while not self._exited:
line = "" line = ""
content_len = -1 content_len = -1
while content_len == -1 or (line != "\n" and line != "\r\n"): while content_len == -1 or (line != "\n" and line != "\r\n"):
@ -125,33 +148,54 @@ class LanguageServer:
except Exception as e: except Exception as e:
printerr(traceback.format_exc()) printerr(traceback.format_exc())
def _send(self, data): def _send(self, data):
data["jsonrpc"] = "2.0" data["jsonrpc"] = "2.0"
line = json.dumps(data, separators=(",", ":")) + "\r\n" line = json.dumps(data, separators=(",", ":"))
printerr("output: " + line) 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.write(
f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}"
)
sys.stdout.flush() sys.stdout.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): def _send_response(self, id, result):
self._send({ self._send(
{
"id": id, "id": id,
"result": result, "result": result,
}) }
)
def _send_notification(self, method, params): def _send_notification(self, method, params):
self._send({ self._send(
{
"method": method, "method": method,
"params": params, "params": params,
}) }
)
@command("initialize") @command("initialize")
def initialize(self, id, params): def initialize(self, id, params):
from . import main from . import main
self.client_capabilities = params.get("capabilities") self.client_capabilities = params.get("capabilities", {})
self._send_response(id, { self.client_supports_completion_choice = params.get("clientInfo", {}).get(
"name"
) in ["Visual Studio Code", "VSCodium"]
self._send_response(
id,
{
"capabilities": { "capabilities": {
"textDocumentSync": { "textDocumentSync": {
"openClose": True, "openClose": True,
@ -160,18 +204,31 @@ class LanguageServer:
"semanticTokensProvider": { "semanticTokensProvider": {
"legend": { "legend": {
"tokenTypes": ["enumMember"], "tokenTypes": ["enumMember"],
"tokenModifiers": [],
}, },
"full": True, "full": True,
}, },
"completionProvider": {}, "completionProvider": {},
"codeActionProvider": {}, "codeActionProvider": {},
"hoverProvider": True, "hoverProvider": True,
"documentSymbolProvider": True,
"definitionProvider": True,
"documentFormattingProvider": True,
}, },
"serverInfo": { "serverInfo": {
"name": "Blueprint", "name": "Blueprint",
"version": main.VERSION, "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") @command("textDocument/didOpen")
def didOpen(self, id, params): def didOpen(self, id, params):
@ -198,14 +255,23 @@ class LanguageServer:
@command("textDocument/hover") @command("textDocument/hover")
def hover(self, id, params): def hover(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]] 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: if docs:
self._send_response(id, { self._send_response(
id,
{
"contents": { "contents": {
"kind": "markdown", "kind": "markdown",
"value": docs, "value": docs,
} }
}) },
)
else: else:
self._send_response(id, None) self._send_response(id, None)
@ -217,75 +283,218 @@ class LanguageServer:
self._send_response(id, []) self._send_response(id, [])
return return
idx = utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text) idx = utils.pos_to_idx(
completions = complete(open_file.ast, open_file.tokens, idx) params["position"]["line"], params["position"]["character"], open_file.text
self._send_response(id, [completion.to_json(True) for completion in completions]) )
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") @command("textDocument/semanticTokens/full")
def semantic_tokens(self, id, params): def semantic_tokens(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]] open_file = self._open_files[params["textDocument"]["uri"]]
self._send_response(id, { self._send_response(
id,
{
"data": open_file.calc_semantic_tokens(), "data": open_file.calc_semantic_tokens(),
}) },
)
@command("textDocument/codeAction") @command("textDocument/codeAction")
def code_actions(self, id, params): def code_actions(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]] 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 = Range(
range_end = utils.pos_to_idx(params["range"]["end"]["line"], params["range"]["end"]["character"], open_file.text) 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 = [ actions = [
{ {
"title": action.title, "title": action.title,
"kind": "quickfix", "kind": "quickfix",
"diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, diagnostic)], "diagnostics": [self._create_diagnostic(open_file.uri, diagnostic)],
"edit": { "edit": {
"changes": { "changes": {
open_file.uri: [{ open_file.uri: [
"range": utils.idxs_to_range(diagnostic.start, diagnostic.end, open_file.text), {
"newText": action.replace_with "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 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 for action in diagnostic.actions
] ]
self._send_response(id, 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): def _send_file_updates(self, open_file: OpenFile):
self._send_notification("textDocument/publishDiagnostics", { self._send_notification(
"textDocument/publishDiagnostics",
{
"uri": open_file.uri, "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, text, uri, err): def _create_diagnostic(self, uri: str, err: CompileError):
message = err.message message = err.message
assert err.range is not None
for hint in err.hints: for hint in err.hints:
message += '\nhint: ' + hint message += "\nhint: " + hint
result = { result = {
"range": utils.idxs_to_range(err.start, err.end, text), "range": err.range.to_json(),
"message": message, "message": message,
"severity": DiagnosticSeverity.Warning if isinstance(err, CompileWarning) else DiagnosticSeverity.Error, "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: if len(err.references) > 0:
result["relatedInformation"] = [ result["relatedInformation"] = [
{ {
"location": { "location": {
"uri": uri, "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 for ref in err.references
] ]
@ -297,4 +506,3 @@ for name in dir(LanguageServer):
item = getattr(LanguageServer, name) item = getattr(LanguageServer, name)
if callable(item) and hasattr(item, "_json_method"): if callable(item) and hasattr(item, "_json_method"):
LanguageServer.commands[item._json_method] = item LanguageServer.commands[item._json_method] = item

View file

@ -18,11 +18,14 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from dataclasses import dataclass
import enum import enum
import json
import os
import typing as T import typing as T
from dataclasses import dataclass, field
from .errors import * from .errors import *
from .tokenizer import Range
from .utils import * from .utils import *
@ -31,13 +34,16 @@ class TextDocumentSyncKind(enum.IntEnum):
Full = 1 Full = 1
Incremental = 2 Incremental = 2
class CompletionItemTag(enum.IntEnum): class CompletionItemTag(enum.IntEnum):
Deprecated = 1 Deprecated = 1
class InsertTextFormat(enum.IntEnum): class InsertTextFormat(enum.IntEnum):
PlainText = 1 PlainText = 1
Snippet = 2 Snippet = 2
class CompletionItemKind(enum.IntEnum): class CompletionItemKind(enum.IntEnum):
Text = 1 Text = 1
Method = 2 Method = 2
@ -66,15 +72,21 @@ class CompletionItemKind(enum.IntEnum):
TypeParameter = 25 TypeParameter = 25
class ErrorCode(enum.IntEnum):
RequestFailed = -32803
@dataclass @dataclass
class Completion: class Completion:
label: str label: str
kind: CompletionItemKind kind: CompletionItemKind
signature: T.Optional[str] = None signature: T.Optional[str] = None
deprecated: bool = False deprecated: bool = False
sort_text: T.Optional[str] = None
docs: T.Optional[str] = None docs: T.Optional[str] = None
text: T.Optional[str] = None text: T.Optional[str] = None
snippet: T.Optional[str] = None snippet: T.Optional[str] = None
detail: T.Optional[str] = None
def to_json(self, snippets: bool): def to_json(self, snippets: bool):
insert_text = self.text or self.label insert_text = self.text or self.label
@ -87,14 +99,21 @@ class Completion:
"label": self.label, "label": self.label,
"kind": self.kind, "kind": self.kind,
"tags": [CompletionItemTag.Deprecated] if self.deprecated else None, "tags": [CompletionItemTag.Deprecated] if self.deprecated else None,
"detail": self.signature, # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemLabelDetails
"documentation": { "labelDetails": ({"detail": self.signature} if self.signature else None),
"documentation": (
{
"kind": "markdown", "kind": "markdown",
"value": self.docs, "value": self.docs,
} if self.docs else None, }
if self.docs
else None
),
"deprecated": self.deprecated, "deprecated": self.deprecated,
"sortText": self.sort_text,
"insertText": insert_text, "insertText": insert_text,
"insertTextFormat": insert_text_format, "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} return {k: v for k, v in result.items() if v is not None}
@ -110,9 +129,100 @@ class DiagnosticSeverity(enum.IntEnum):
Hint = 4 Hint = 4
class DiagnosticTag(enum.IntEnum):
Unnecessary = 1
Deprecated = 2
@dataclass @dataclass
class SemanticToken: class SemanticToken:
start: int start: int
end: int end: int
type: SemanticTokenType 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,36 +18,106 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import argparse
import difflib
import os
import sys
import typing as T 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 .lsp import LanguageServer
from . import parser, tokenizer, decompiler, interactive_port
from .utils import Colors
from .outputs import XmlOutput from .outputs import XmlOutput
from .utils import Colors
VERSION = "uninstalled" VERSION = "uninstalled"
LIBDIR = None LIBDIR = None
class BlueprintApp: class BlueprintApp:
def main(self): def main(self):
self.parser = argparse.ArgumentParser() self.parser = argparse.ArgumentParser()
self.subparsers = self.parser.add_subparsers(metavar="command") self.subparsers = self.parser.add_subparsers(metavar="command")
self.parser.set_defaults(func=self.cmd_help) 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("--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("output_dir", metavar="output-dir")
batch_compile.add_argument("input_dir", metavar="input-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) 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 = 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) self.add_subcommand("help", "Show this message", self.cmd_help)
@ -65,18 +135,19 @@ class BlueprintApp:
except: except:
report_bug() report_bug()
def add_subcommand(self, name: str, help: str, func):
def add_subcommand(self, name, help, func):
parser = self.subparsers.add_parser(name, help=help) parser = self.subparsers.add_parser(name, help=help)
parser.set_defaults(func=func) parser.set_defaults(func=func)
return parser return parser
def cmd_help(self, opts): def cmd_help(self, opts):
self.parser.print_help() self.parser.print_help()
def cmd_compile(self, opts): 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() data = opts.input.read()
try: try:
xml, warnings = self._compile(data) xml, warnings = self._compile(data)
@ -90,17 +161,24 @@ class BlueprintApp:
with open(opts.output, "w") as file: with open(opts.output, "w") as file:
file.write(xml) file.write(xml)
except PrintableError as e: except PrintableError as e:
e.pretty_print(opts.input.name, data) e.pretty_print(opts.input.name, data, stream=sys.stderr)
sys.exit(1) sys.exit(1)
def cmd_batch_compile(self, opts): 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: for file in opts.inputs:
data = file.read() data = file.read()
file_abs = os.path.abspath(file.name)
input_dir_abs = os.path.abspath(opts.input_dir)
try: try:
if not os.path.commonpath([file.name, opts.input_dir]): if not os.path.commonpath([file_abs, input_dir_abs]):
print(f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}") print(
f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}"
)
sys.exit(1) sys.exit(1)
xml, warnings = self._compile(data) xml, warnings = self._compile(data)
@ -111,9 +189,8 @@ class BlueprintApp:
path = os.path.join( path = os.path.join(
opts.output_dir, opts.output_dir,
os.path.relpath( os.path.relpath(
os.path.splitext(file.name)[0] + ".ui", os.path.splitext(file.name)[0] + ".ui", opts.input_dir
opts.input_dir ),
)
) )
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as file: with open(path, "w") as file:
@ -122,24 +199,150 @@ class BlueprintApp:
e.pretty_print(file.name, data) e.pretty_print(file.name, data)
sys.exit(1) 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): def cmd_lsp(self, opts):
langserv = LanguageServer() langserv = LanguageServer()
langserv.run() langserv.run()
def cmd_port(self, opts): def cmd_port(self, opts):
interactive_port.run(opts) interactive_port.run(opts)
def _compile(self, data: str) -> T.Tuple[str, T.List[CompileError]]:
def _compile(self, data: str) -> T.Tuple[str, T.List[PrintableError]]:
tokens = tokenizer.tokenize(data) tokens = tokenizer.tokenize(data)
ast, errors, warnings = parser.parse(tokens) ast, errors, warnings = parser.parse(tokens)
if errors: if errors:
raise errors raise errors
if len(ast.errors): if ast is None:
raise MultipleErrors(ast.errors) raise CompilerBugError()
formatter = XmlOutput() formatter = XmlOutput()

View file

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

View file

@ -1,25 +1,26 @@
import typing as T import typing as T
from .. import OutputFormat
from ...language import * from ...language import *
from .. import OutputFormat
from .xml_emitter import XmlEmitter from .xml_emitter import XmlEmitter
class XmlOutput(OutputFormat): class XmlOutput(OutputFormat):
def emit(self, ui: UI) -> str: def emit(self, ui: UI, indent=2, generated_notice=True) -> str:
xml = XmlEmitter() xml = XmlEmitter(indent, generated_notice)
self._emit_ui(ui, xml) self._emit_ui(ui, xml)
return xml.result return xml.result
def _emit_ui(self, ui: UI, xml: XmlEmitter): 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") xml.start_tag("interface")
for x in ui.children: self._emit_gtk_directive(ui.gtk_decl, xml)
if isinstance(x, GtkDirective):
self._emit_gtk_directive(x, xml) for x in ui.contents:
elif isinstance(x, Import): if isinstance(x, Template):
pass
elif isinstance(x, Template):
self._emit_template(x, xml) self._emit_template(x, xml)
elif isinstance(x, Object): elif isinstance(x, Object):
self._emit_object(x, xml) self._emit_object(x, xml)
@ -34,7 +35,9 @@ class XmlOutput(OutputFormat):
xml.put_self_closing("requires", lib="gtk", version=gtk.gir_namespace.version) xml.put_self_closing("requires", lib="gtk", version=gtk.gir_namespace.version)
def _emit_template(self, template: Template, xml: XmlEmitter): def _emit_template(self, template: Template, xml: XmlEmitter):
xml.start_tag("template", **{"class": template.id}, parent=template.class_name) xml.start_tag(
"template", **{"class": template.gir_class}, parent=template.parent_type
)
self._emit_object_or_template(template, xml) self._emit_object_or_template(template, xml)
xml.end_tag() xml.end_tag()
@ -47,7 +50,9 @@ class XmlOutput(OutputFormat):
self._emit_object_or_template(obj, xml) self._emit_object_or_template(obj, xml)
xml.end_tag() xml.end_tag()
def _emit_object_or_template(self, obj: T.Union[Object, Template], xml: XmlEmitter): def _emit_object_or_template(
self, obj: T.Union[Object, Template, ExtListItemFactory], xml: XmlEmitter
):
for child in obj.content.children: for child in obj.content.children:
if isinstance(child, Property): if isinstance(child, Property):
self._emit_property(child, xml) self._emit_property(child, xml)
@ -74,62 +79,92 @@ class XmlOutput(OutputFormat):
def _emit_menu(self, menu: Menu, xml: XmlEmitter): def _emit_menu(self, menu: Menu, xml: XmlEmitter):
xml.start_tag(menu.tag, id=menu.id) xml.start_tag(menu.tag, id=menu.id)
for child in menu.children: for child in menu.items:
if isinstance(child, Menu): if isinstance(child, Menu):
self._emit_menu(child, xml) self._emit_menu(child, xml)
elif isinstance(child, MenuAttribute): elif isinstance(child, MenuAttribute):
self._emit_attribute("attribute", "name", child.name, child.value, xml) xml.start_tag(
"attribute",
name=child.name,
**self._translated_string_attrs(child.value.child),
)
xml.put_text(child.value.string)
xml.end_tag()
else: else:
raise CompilerBugError() raise CompilerBugError()
xml.end_tag() xml.end_tag()
def _emit_property(self, property: Property, xml: XmlEmitter): def _emit_property(self, property: Property, xml: XmlEmitter):
values = property.children[Value] value = property.value
value = values[0] if len(values) == 1 else None
bind_flags = [] props: T.Dict[str, T.Optional[str]] = {
if property.tokens["bind_source"] and not property.tokens["no_sync_create"]: "name": property.name,
bind_flags.append("sync-create")
if property.tokens["inverted"]:
bind_flags.append("invert-boolean")
if property.tokens["bidirectional"]:
bind_flags.append("bidirectional")
bind_flags_str = "|".join(bind_flags) or None
props = {
"name": property.tokens["name"],
"bind-source": property.tokens["bind_source"],
"bind-property": property.tokens["bind_property"],
"bind-flags": bind_flags_str,
} }
if isinstance(value, TranslatedStringValue): if isinstance(value, Value):
xml.start_tag("property", **props, **self._translated_string_attrs(value)) child = value.child
xml.put_text(value.string)
if isinstance(child, Translated):
xml.start_tag(
"property", **props, **self._translated_string_attrs(child)
)
xml.put_text(child.string)
xml.end_tag() xml.end_tag()
elif len(property.children[Object]) == 1:
xml.start_tag("property", **props)
self._emit_object(property.children[Object][0], xml)
xml.end_tag()
elif value is None:
if property.tokens["binding"]:
xml.start_tag("binding", **props)
self._emit_expression(property.children[Expr][0], xml)
xml.end_tag()
else:
xml.put_self_closing("property", **props)
else: else:
xml.start_tag("property", **props) xml.start_tag("property", **props)
self._emit_value(value, xml) self._emit_value(value, xml)
xml.end_tag() 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( def _translated_string_attrs(
self, translated: TranslatedStringValue self, translated: T.Optional[T.Union[QuotedLiteral, Translated]]
) -> T.Dict[str, T.Optional[str]]: ) -> T.Dict[str, T.Optional[str]]:
return { if translated is None:
"translatable": "true", return {}
"context": translated.context, elif isinstance(translated, QuotedLiteral):
} return {}
else:
return {"translatable": "yes", "context": translated.translate_context}
def _emit_signal(self, signal: Signal, xml: XmlEmitter): def _emit_signal(self, signal: Signal, xml: XmlEmitter):
name = signal.name name = signal.name
@ -139,72 +174,117 @@ class XmlOutput(OutputFormat):
"signal", "signal",
name=name, name=name,
handler=signal.handler, handler=signal.handler,
swapped=signal.is_swapped or None, swapped=signal.is_swapped,
object=signal.object_id, 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): def _emit_child(self, child: Child, xml: XmlEmitter):
child_type = internal_child = None child_type = internal_child = None
if child.annotation is not None:
if child.tokens["internal_child"]: annotation = child.annotation.child
internal_child = child.tokens["child_type"] 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: else:
child_type = child.tokens["child_type"] raise CompilerBugError()
xml.start_tag("child", type=child_type, internal_child=internal_child) xml.start_tag("child", type=child_type, internal_child=internal_child)
self._emit_object(child.object, xml) self._emit_object(child.object, xml)
xml.end_tag() xml.end_tag()
def _emit_value(self, value: Value, xml: XmlEmitter): def _emit_literal(self, literal: Literal, xml: XmlEmitter):
if isinstance(value, IdentValue): value = literal.value
if isinstance(value.parent.value_type, gir.Enumeration): if isinstance(value, IdentLiteral):
xml.put_text( value_type = value.context[ValueTypeCtx].value_type
value.parent.value_type.members[value.tokens["value"]].nick 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: else:
xml.put_text(value.tokens["value"]) xml.put_text(self._object_id(value, value.ident))
elif isinstance(value, QuotedValue) or isinstance(value, NumberValue): elif isinstance(value, TypeLiteral):
xml.put_text(value.value)
elif isinstance(value, FlagsValue):
xml.put_text("|".join([flag.tokens["value"] for flag in value.children]))
elif isinstance(value, TranslatedStringValue):
raise CompilerBugError("translated values must be handled in the parent")
elif isinstance(value, TypeValue):
xml.put_text(value.type_name.glib_type_name) xml.put_text(value.type_name.glib_type_name)
else: else:
raise CompilerBugError() 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_expression(self, expression: Expr, xml: XmlEmitter): def _emit_value(self, value: Value, xml: XmlEmitter):
self._emit_expression_part(expression.children[-1], xml) if isinstance(value.child, Literal):
self._emit_literal(value.child, xml)
def _emit_expression_part(self, expression, xml: XmlEmitter): elif isinstance(value.child, Flags):
if isinstance(expression, IdentExpr): xml.put_text(
self._emit_ident_expr(expression, xml) "|".join([str(flag.value or flag.name) for flag in value.child.flags])
elif isinstance(expression, LookupOp): )
self._emit_lookup_op(expression, xml)
elif isinstance(expression, Expr):
self._emit_expression(expression, xml)
else: else:
raise CompilerBugError() raise CompilerBugError()
def _emit_ident_expr(self, expr: IdentExpr, xml: XmlEmitter): 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") xml.start_tag("constant")
xml.put_text(expr.ident) else:
xml.start_tag("constant", type=expr.type)
self._emit_literal(expr.literal, xml)
xml.end_tag() xml.end_tag()
def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter): def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter):
xml.start_tag("lookup", name=expr.property_name) xml.start_tag("lookup", name=expr.property_name, type=expr.lhs.type)
self._emit_expression_part(expr.lhs, xml) self._emit_expression_part(expr.lhs, xml)
xml.end_tag() 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( def _emit_attribute(
self, tag: str, attr: str, name: str, value: Value, xml: XmlEmitter self,
tag: str,
attr: str,
name: str,
value: T.Union[Value, StringValue],
xml: XmlEmitter,
): ):
attrs = {attr: name} attrs = {attr: name}
if isinstance(value, TranslatedStringValue): if isinstance(value.child, Translated):
xml.start_tag(tag, **attrs, **self._translated_string_attrs(value)) xml.start_tag(tag, **attrs, **self._translated_string_attrs(value.child))
xml.put_text(value.string) 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() xml.end_tag()
else: else:
xml.start_tag(tag, **attrs) xml.start_tag(tag, **attrs)
@ -212,63 +292,135 @@ class XmlOutput(OutputFormat):
xml.end_tag() xml.end_tag()
def _emit_extensions(self, extension, xml: XmlEmitter): def _emit_extensions(self, extension, xml: XmlEmitter):
if isinstance(extension, A11y): if isinstance(extension, ExtAccessibility):
xml.start_tag("accessibility") xml.start_tag("accessibility")
for child in extension.children: for property in extension.properties:
for val in property.values:
self._emit_attribute( self._emit_attribute(
child.tag_name, "name", child.name, child.children[Value][0], xml property.tag_name, "name", property.name, val, xml
) )
xml.end_tag() 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): elif isinstance(extension, Filters):
xml.start_tag(extension.tokens["tag_name"]) xml.start_tag(extension.tokens["tag_name"])
for child in extension.children: for prop in extension.children:
xml.start_tag(child.tokens["tag_name"]) xml.start_tag(prop.tokens["tag_name"])
xml.put_text(child.tokens["name"]) xml.put_text(prop.tokens["name"])
xml.end_tag() xml.end_tag()
xml.end_tag() xml.end_tag()
elif isinstance(extension, Items): elif isinstance(extension, ExtComboBoxItems):
xml.start_tag("items") xml.start_tag("items")
for child in extension.children: for prop in extension.children:
self._emit_attribute( self._emit_attribute("item", "id", prop.name, prop.value, xml)
"item", "id", child.name, child.children[Value][0], xml
)
xml.end_tag() xml.end_tag()
elif isinstance(extension, Layout): elif isinstance(extension, ExtLayout):
xml.start_tag("layout") xml.start_tag("layout")
for child in extension.children: for prop in extension.children:
self._emit_attribute( self._emit_attribute("property", "name", prop.name, prop.value, xml)
"property", "name", child.name, child.children[Value][0], xml
)
xml.end_tag() xml.end_tag()
elif isinstance(extension, Strings): 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") xml.start_tag("items")
for child in extension.children: for string in extension.children:
value = child.children[Value][0] value = string.child
if isinstance(value, TranslatedStringValue): xml.start_tag("item", **self._translated_string_attrs(value.child))
xml.start_tag("item", **self._translated_string_attrs(value))
xml.put_text(value.string) xml.put_text(value.string)
xml.end_tag() xml.end_tag()
else:
xml.start_tag("item")
self._emit_value(value, xml)
xml.end_tag()
xml.end_tag() xml.end_tag()
elif isinstance(extension, Styles): 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") xml.start_tag("style")
for child in extension.children: for style in extension.children:
xml.put_self_closing("class", name=child.tokens["name"]) xml.put_self_closing("class", name=style.name)
xml.end_tag() xml.end_tag()
elif isinstance(extension, Widgets): elif isinstance(extension, ExtSizeGroupWidgets):
xml.start_tag("widgets") xml.start_tag("widgets")
for child in extension.children: for prop in extension.children:
xml.put_self_closing("widget", name=child.tokens["name"]) xml.put_self_closing("widget", name=prop.name)
xml.end_tag() xml.end_tag()
else: else:
raise CompilerBugError() 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

@ -18,7 +18,6 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T import typing as T
from xml.sax import saxutils from xml.sax import saxutils
from blueprintcompiler.gir import GirType from blueprintcompiler.gir import GirType
@ -26,13 +25,24 @@ from blueprintcompiler.language.types import ClassName
class XmlEmitter: class XmlEmitter:
def __init__(self, indent=2): def __init__(self, indent=2, generated_notice=True):
self.indent = indent self.indent = indent
self.result = '<?xml version="1.0" encoding="UTF-8"?>' 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._tag_stack = []
self._needs_newline = False self._needs_newline = False
def start_tag(self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None]): def start_tag(
self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None, float]
):
self._indent() self._indent()
self.result += f"<{tag}" self.result += f"<{tag}"
for key, val in attrs.items(): for key, val in attrs.items():
@ -62,6 +72,11 @@ class XmlEmitter:
self.result += saxutils.escape(str(text)) self.result += saxutils.escape(str(text))
self._needs_newline = False 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): def _indent(self):
if self.indent is not None: if self.indent is not None:
self.result += "\n" + " " * (self.indent * len(self._tag_stack)) self.result += "\n" + " " * (self.indent * len(self._tag_stack))

View file

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

View file

@ -19,19 +19,29 @@
from .errors import MultipleErrors, PrintableError from .errors import MultipleErrors, PrintableError
from .language import OBJECT_CONTENT_HOOKS, UI, Template
from .parse_tree import * from .parse_tree import *
from .tokenizer import TokenType from .tokenizer import TokenType
from .language import 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.""" """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) AnyOf(UI).parse(ctx)
ast_node = ctx.last_group.to_ast() if ctx.last_group else None assert ctx.last_group is not None
errors = MultipleErrors(ctx.errors) if len(ctx.errors) else None ast_node = ctx.last_group.to_ast()
warnings = ctx.warnings
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 # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import re import re
import typing as T
from dataclasses import dataclass
from enum import Enum from enum import Enum
from .errors import CompileError from . import utils
class TokenType(Enum): class TokenType(Enum):
@ -38,49 +39,59 @@ class TokenType(Enum):
_tokens = [ _tokens = [
(TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"), (TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"),
(TokenType.QUOTED, r'"(\\"|[^"\n])*"'), (TokenType.QUOTED, r'"(\\(.|\n)|[^\\"\n])*"'),
(TokenType.QUOTED, r"'(\\'|[^'\n])*'"), (TokenType.QUOTED, r"'(\\(.|\n)|[^\\'\n])*'"),
(TokenType.NUMBER, r"0x[A-Za-z0-9_]+"), (TokenType.NUMBER, r"0x[A-Za-z0-9_]+"),
(TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"), (TokenType.NUMBER, r"[\d_]+(\.[\d_]+)?"),
(TokenType.NUMBER, r"[-+]?\.[\d_]+"), (TokenType.NUMBER, r"\.[\d_]+"),
(TokenType.WHITESPACE, r"\s+"), (TokenType.WHITESPACE, r"\s+"),
(TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"),
(TokenType.COMMENT, r"\/\/[^\n]*"), (TokenType.COMMENT, r"\/\/[^\n]*"),
(TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), (TokenType.OP, r"\$|<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"),
(TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"), (TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"),
] ]
_TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens] _TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens]
class Token: class Token:
def __init__(self, type, start, end, string): def __init__(self, type: TokenType, start: int, end: int, string: str):
self.type = type self.type = type
self.start = start self.start = start
self.end = end self.end = end
self.string = string self.string = string
def __str__(self): def __str__(self) -> str:
return self.string[self.start : self.end] 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: if self.type != TokenType.NUMBER:
return None raise CompilerBugError()
string = str(self).replace("_", "") string = str(self).replace("_", "")
try: try:
if string.startswith("0x"): if string.startswith("0x"):
return int(string, 16) return int(string, 16)
elif "." in string:
return float(string)
else: else:
return float(string.replace("_", "")) return int(string)
except: except:
raise CompileError(f"{str(self)} is not a valid number literal", self.start, self.end) raise CompileError(f"{str(self)} is not a valid number literal", self.range)
def _tokenize(ui_ml: str): def _tokenize(ui_ml: str):
from .errors import CompileError
i = 0 i = 0
while i < len(ui_ml): while i < len(ui_ml):
matched = False matched = False
for (type, regex) in _TOKENS: for type, regex in _TOKENS:
match = regex.match(ui_ml, i) match = regex.match(ui_ml, i)
if match is not None: if match is not None:
@ -90,10 +101,55 @@ def _tokenize(ui_ml: str):
break break
if not matched: 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) yield Token(TokenType.EOF, i, i, ui_ml)
def tokenize(data: str) -> T.List[Token]: def tokenize(data: str) -> T.List[Token]:
return list(_tokenize(data)) 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

@ -17,15 +17,15 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import math
import mmap
import os
import sys import sys
import typing as T import typing as T
import math
from ctypes import * from ctypes import *
import mmap, os
from .errors import CompilerBugError from .errors import CompilerBugError
BLOB_TYPE_STRUCT = 3 BLOB_TYPE_STRUCT = 3
BLOB_TYPE_BOXED = 4 BLOB_TYPE_BOXED = 4
BLOB_TYPE_ENUM = 5 BLOB_TYPE_ENUM = 5
@ -58,14 +58,21 @@ TYPE_UNICHAR = 21
class Field: class Field:
def __init__(self, offset, type, shift=0, mask=None): def __init__(self, offset: int, type: str, shift=0, mask=None):
self._offset = offset self._offset = offset
self._type = type self._type = type
if not mask or sys.byteorder == "little":
self._shift = shift 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._mask = (1 << mask) - 1 if mask else None
self._name = f"{offset}__{type}__{shift}__{mask}" self._name = f"{offset}__{type}__{shift}__{mask}"
def __get__(self, typelib, _objtype=None): def __get__(self, typelib: "Typelib", _objtype=None):
if typelib is None: if typelib is None:
return self return self
@ -118,6 +125,7 @@ class Typelib:
HEADER_FUNCTION_BLOB_SIZE = Field(0x3E, "u16") HEADER_FUNCTION_BLOB_SIZE = Field(0x3E, "u16")
HEADER_CALLBACK_BLOB_SIZE = Field(0x40, "u16") HEADER_CALLBACK_BLOB_SIZE = Field(0x40, "u16")
HEADER_SIGNAL_BLOB_SIZE = Field(0x42, "u16") HEADER_SIGNAL_BLOB_SIZE = Field(0x42, "u16")
HEADER_ARG_BLOB_SIZE = Field(0x46, "u16")
HEADER_PROPERTY_BLOB_SIZE = Field(0x48, "u16") HEADER_PROPERTY_BLOB_SIZE = Field(0x48, "u16")
HEADER_FIELD_BLOB_SIZE = Field(0x4A, "u16") HEADER_FIELD_BLOB_SIZE = Field(0x4A, "u16")
HEADER_VALUE_BLOB_SIZE = Field(0x4C, "u16") HEADER_VALUE_BLOB_SIZE = Field(0x4C, "u16")
@ -132,19 +140,32 @@ class Typelib:
DIR_ENTRY_OFFSET = Field(0x8, "pointer") DIR_ENTRY_OFFSET = Field(0x8, "pointer")
DIR_ENTRY_NAMESPACE = Field(0x8, "string") DIR_ENTRY_NAMESPACE = Field(0x8, "string")
ATTR_OFFSET = Field(0x0, "u32") ARG_NAME = Field(0x0, "string")
ATTR_NAME = Field(0x0, "string") ARG_TYPE = Field(0xC, "u32")
ATTR_VALUE = Field(0x0, "string")
INTERFACE_TYPE_INTERFACE = Field(0x2, "dir_entry") 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") 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_GTYPE_NAME = Field(0x8, "string")
ENUM_N_VALUES = Field(0x10, "u16") ENUM_N_VALUES = Field(0x10, "u16")
ENUM_N_METHODS = Field(0x12, "u16") ENUM_N_METHODS = Field(0x12, "u16")
ENUM_VALUES = Field(0x18, "offset") ENUM_VALUES = Field(0x18, "offset")
INTERFACE_DEPRECATED = Field(0x2, "u16", 0, 1)
INTERFACE_GTYPE_NAME = Field(0x8, "string") INTERFACE_GTYPE_NAME = Field(0x8, "string")
INTERFACE_N_PREREQUISITES = Field(0x12, "u16") INTERFACE_N_PREREQUISITES = Field(0x12, "u16")
INTERFACE_N_PROPERTIES = Field(0x14, "u16") INTERFACE_N_PROPERTIES = Field(0x14, "u16")
@ -160,7 +181,7 @@ class Typelib:
OBJ_FINAL = Field(0x02, "u16", 3, 1) OBJ_FINAL = Field(0x02, "u16", 3, 1)
OBJ_GTYPE_NAME = Field(0x08, "string") OBJ_GTYPE_NAME = Field(0x08, "string")
OBJ_PARENT = Field(0x10, "dir_entry") OBJ_PARENT = Field(0x10, "dir_entry")
OBJ_GTYPE_STRUCT = Field(0x14, "string") OBJ_GTYPE_STRUCT = Field(0x12, "string")
OBJ_N_INTERFACES = Field(0x14, "u16") OBJ_N_INTERFACES = Field(0x14, "u16")
OBJ_N_FIELDS = Field(0x16, "u16") OBJ_N_FIELDS = Field(0x16, "u16")
OBJ_N_PROPERTIES = Field(0x18, "u16") OBJ_N_PROPERTIES = Field(0x18, "u16")
@ -178,50 +199,55 @@ class Typelib:
PROP_CONSTRUCT_ONLY = Field(0x4, "u32", 4, 1) PROP_CONSTRUCT_ONLY = Field(0x4, "u32", 4, 1)
PROP_TYPE = Field(0xC, "u32") 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_NAME = Field(0x4, "string")
VALUE_VALUE = Field(0x8, "i32") VALUE_VALUE = Field(0x8, "i32")
def __init__(self, typelib_file, offset): def __init__(self, typelib_file, offset: int):
self._typelib_file = typelib_file self._typelib_file = typelib_file
self._offset = offset self._offset = offset
def __getitem__(self, index): def __getitem__(self, index: int):
return Typelib(self._typelib_file, self._offset + index) return Typelib(self._typelib_file, self._offset + index)
def attr(self, name): def attr(self, name):
return self.header.attr(self._offset, name) return self.header.attr(self._offset, name)
@property @property
def header(self): def header(self) -> "TypelibHeader":
return TypelibHeader(self._typelib_file) return TypelibHeader(self._typelib_file)
@property @property
def u8(self): def u8(self) -> int:
"""Gets the 8-bit unsigned int at this location.""" """Gets the 8-bit unsigned int at this location."""
return self._int(1, False) return self._int(1, False)
@property @property
def u16(self): def u16(self) -> int:
"""Gets the 16-bit unsigned int at this location.""" """Gets the 16-bit unsigned int at this location."""
return self._int(2, False) return self._int(2, False)
@property @property
def u32(self): def u32(self) -> int:
"""Gets the 32-bit unsigned int at this location.""" """Gets the 32-bit unsigned int at this location."""
return self._int(4, False) return self._int(4, False)
@property @property
def i8(self): def i8(self) -> int:
"""Gets the 8-bit unsigned int at this location.""" """Gets the 8-bit unsigned int at this location."""
return self._int(1, True) return self._int(1, True)
@property @property
def i16(self): def i16(self) -> int:
"""Gets the 16-bit unsigned int at this location.""" """Gets the 16-bit unsigned int at this location."""
return self._int(2, True) return self._int(2, True)
@property @property
def i32(self): def i32(self) -> int:
"""Gets the 32-bit unsigned int at this location.""" """Gets the 32-bit unsigned int at this location."""
return self._int(4, True) return self._int(4, True)
@ -235,20 +261,22 @@ class Typelib:
if loc == 0: if loc == 0:
return None return None
end = loc end = self._typelib_file.find(b"\0", loc)
while self._typelib_file[end] != 0:
end += 1
return self._typelib_file[loc:end].decode("utf-8") return self._typelib_file[loc:end].decode("utf-8")
def _int(self, size, signed): def _int(self, size, signed) -> int:
return int.from_bytes(self._typelib_file[self._offset:self._offset + size], sys.byteorder) return int.from_bytes(
self._typelib_file[self._offset : self._offset + size],
sys.byteorder,
signed=signed,
)
class TypelibHeader(Typelib): class TypelibHeader(Typelib):
def __init__(self, typelib_file): def __init__(self, typelib_file):
super().__init__(typelib_file, 0) super().__init__(typelib_file, 0)
def dir_entry(self, index): def dir_entry(self, index) -> T.Optional[Typelib]:
if index == 0: if index == 0:
return None return None
else: else:

View file

@ -18,18 +18,20 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T import typing as T
from dataclasses import dataclass
class Colors: class Colors:
RED = '\033[91m' RED = "\033[91m"
GREEN = '\033[92m' GREEN = "\033[92m"
YELLOW = '\033[33m' YELLOW = "\033[33m"
FAINT = '\033[2m' PURPLE = "\033[35m"
BOLD = '\033[1m' FAINT = "\033[2m"
BLUE = '\033[34m' BOLD = "\033[1m"
UNDERLINE = '\033[4m' BLUE = "\033[34m"
NO_UNDERLINE = '\033[24m' UNDERLINE = "\033[4m"
CLEAR = '\033[0m' NO_UNDERLINE = "\033[24m"
CLEAR = "\033[0m"
def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]: 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 cost = 1
else: else:
cost = 2 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] return distances[m - 1][n - 1]
@ -70,15 +76,16 @@ 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]: def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]:
if idx == 0 or len(text) == 0: if idx == 0 or len(text) == 0:
return (0, 0) return (0, 0)
sp = text[:idx].splitlines(keepends=True) line_num = text.count("\n", 0, idx) + 1
line_num = len(sp) col_num = idx - text.rfind("\n", 0, idx) - 1
col_num = len(sp[-1])
return (line_num - 1, col_num) return (line_num - 1, col_num)
def pos_to_idx(line: int, col: int, text: str) -> int: def pos_to_idx(line: int, col: int, text: str) -> int:
lines = text.splitlines(keepends=True) lines = text.splitlines(keepends=True)
return sum([len(line) for line in lines[:line]]) + col return sum([len(line) for line in lines[:line]]) + col
def idxs_to_range(start: int, end: int, text: str): def idxs_to_range(start: int, end: int, text: str):
start_l, start_c = idx_to_pos(start, text) start_l, start_c = idx_to_pos(start, text)
end_l, end_c = idx_to_pos(end, 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, "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,22 +18,34 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from collections import defaultdict from collections import defaultdict
from functools import cached_property from functools import cached_property
import typing as T
from xml import sax from xml import sax
# To speed up parsing, we ignore all tags except these # To speed up parsing, we ignore all tags except these
PARSE_GIR = set([ PARSE_GIR = set(
"repository", "namespace", "class", "interface", "property", "glib:signal", [
"include", "implements", "type", "parameter", "parameters", "enumeration", "repository",
"member", "bitfield", "namespace",
]) "class",
"interface",
"property",
"glib:signal",
"include",
"implements",
"type",
"parameter",
"parameters",
"enumeration",
"member",
"bitfield",
]
)
class Element: 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.tag = tag
self.attrs = attrs self.attrs = attrs
self.children: T.List["Element"] = [] self.children: T.List["Element"] = []
@ -41,16 +53,12 @@ class Element:
@cached_property @cached_property
def cdata(self): def cdata(self):
return ''.join(self.cdata_chunks) return "".join(self.cdata_chunks)
def get_elements(self, name) -> T.List["Element"]: def get_elements(self, name: str) -> T.List["Element"]:
return [ return [child for child in self.children if child.tag == name]
child
for child in self.children
if child.tag == name
]
def __getitem__(self, key): def __getitem__(self, key: str):
return self.attrs.get(key) return self.attrs.get(key)
@ -83,3 +91,9 @@ def parse(filename):
parser.setContentHandler(handler) parser.setContentHandler(handler)
parser.parse(filename) parser.parse(filename)
return handler.root return handler.root
def parse_string(xml):
handler = Handler()
parser = sax.parseString(xml, handler)
return handler.root

View file

@ -1,8 +1,13 @@
FROM fedora:latest FROM fedora:latest
RUN dnf install -y meson python3-pip gtk4-devel gobject-introspection-devel \ RUN dnf install -y meson gcc g++ python3-pip gobject-introspection-devel \
libadwaita-devel python3-devel python3-gobject git python3-devel python3-gobject git diffutils xorg-x11-server-Xvfb \
RUN pip3 install furo mypy sphinx coverage 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. # 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 pip install pythonfuzz --extra-index-url https://gitlab.com/api/v4/projects/19904939/packages/pypi/simple

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

View file

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

View file

@ -1,463 +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.
Type Literals
~~~~~~~~~~~~~
Some properties have the type `GObject.Type <https://docs.gtk.org/gobject/alias.Type.html>`_, that is, their value is
itself a description of a type in the GObject type system. An example is
`Gio.ListStore:item-type <https://docs.gtk.org/gio/property.ListStore.item-type.html>`_. Such properties can be
specified with the ``typeof()`` operator:
.. code-block::
Gio.ListStore {
item-type: typeof(.MyDataModel);
}
.. note::
Though it looks like a function, ``typeof()`` is actually a special operator. This is an important syntactic
distinction because the argument to ``typeof()`` is interpreted at the grammar level as a type, not a value or
expression.
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 {}
}
Gtk.Label
~~~~~~~~~
Blueprint does not have syntax for `attributes` on `Gtk.Label`.
Instead, you should use `platform classes <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/style-classes.html>`_, `CSS <https://docs.gtk.org/gtk4/css-properties.html>`_, `Pango Markup <https://docs.gtk.org/Pango/pango_markup.html>`_ or `Gtk.Label.set_attributes <https://docs.gtk.org/gtk4/method.Label.set_attributes.html>`_.
Examples
.. code-block::
Gtk.Label {
label: "Hello, World!";
styles ["title-1"]
}
.. code-block::
Gtk.Label {
label: "<b>Hello, World!</b>";
use-markup: true;
}

View file

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

View file

@ -18,15 +18,15 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces.
setup setup
translations translations
flatpak flatpak
examples reference/index
packaging packaging
.. code-block:: .. code-block:: blueprint
using Gtk 4.0; using Gtk 4.0;
template MyAppWindow : ApplicationWindow { template $MyAppWindow: ApplicationWindow {
default-width: 600; default-width: 600;
default-height: 300; default-height: 300;
title: _("Hello, Blueprint!"); title: _("Hello, Blueprint!");
@ -35,7 +35,7 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces.
HeaderBar {} HeaderBar {}
Label { Label {
label: bind MyAppWindow.main_text; label: bind template.main_text;
} }
} }
@ -53,15 +53,13 @@ Features
- **Concise syntax.** No more clumsy XML! Blueprint is designed from the ground - **Concise syntax.** No more clumsy XML! Blueprint is designed from the ground
up to match GTK's widget model, including templates, child types, signal up to match GTK's widget model, including templates, child types, signal
handlers, and menus. handlers, and menus.
- **Easy to learn.** The syntax should be very familiar to most people. Scroll - **Easy to learn.** The syntax should be very familiar to most people. Take a look at the :doc:`reference <reference/index>` to get started.
through the :doc:`examples page <examples>` for a quick overview of the whole
language.
- **Modern tooling.** Blueprint ships a `Language Server <https://microsoft.github.io/language-server-protocol/>`_ for IDE integration. - **Modern tooling.** Blueprint ships a `Language Server <https://microsoft.github.io/language-server-protocol/>`_ for IDE integration.
Links Links
----- -----
- `Source code <https://gitlab.gnome.org/jwestman/blueprint-compiler>`_ - `Source code <https://gitlab.gnome.org/GNOME/blueprint-compiler>`_
- `Workbench <https://github.com/sonnyp/Workbench>`_ lets you try, preview and export Blueprint - `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 - `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 thetek42 <https://github.com/thetek42/vim-blueprint-syntax>`_
@ -83,33 +81,68 @@ Built with Blueprint
- `AdwSteamGtk <https://github.com/Foldex/AdwSteamGtk>`_ - `AdwSteamGtk <https://github.com/Foldex/AdwSteamGtk>`_
- `Blurble <https://gitlab.gnome.org/World/Blurble>`_ - `Blurble <https://gitlab.gnome.org/World/Blurble>`_
- `Bottles <https://github.com/bottlesdevs/Bottles>`_ - `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/>`_ - `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>`_ - `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>`_ - `Extension Manager <https://github.com/mjakeman/extension-manager>`_
- `Eyedropper <https://github.com/FineFindus/eyedropper>`_
- `favagtk <https://gitlab.gnome.org/johannesjh/favagtk>`_ - `favagtk <https://gitlab.gnome.org/johannesjh/favagtk>`_
- `Feeds <https://gitlab.gnome.org/World/gfeeds>`_ - `Feeds <https://gitlab.gnome.org/World/gfeeds>`_
- `File Shredder <https://github.com/ADBeveridge/raider>`_ - `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>`_ - `Geopard <https://github.com/ranfdev/Geopard>`_
- `Giara <https://gitlab.gnome.org/World/giara>`_ - `Giara <https://gitlab.gnome.org/World/giara>`_
- `Girens <https://gitlab.gnome.org/tijder/girens>`_ - `Girens <https://gitlab.gnome.org/tijder/girens>`_
- `Gradience <https://github.com/GradienceTeam/Gradience>`_ - `Gradience <https://github.com/GradienceTeam/Gradience>`_
- `Graphs <https://gitlab.gnome.org/World/Graphs>`_
- `Health <https://gitlab.gnome.org/World/Health>`_ - `Health <https://gitlab.gnome.org/World/Health>`_
- `HydraPaper <https://gitlab.com/gabmus/HydraPaper>`_ - `HydraPaper <https://gitlab.com/gabmus/HydraPaper>`_
- `Identity <https://gitlab.gnome.org/YaLTeR/identity>`_ - `Identity <https://gitlab.gnome.org/YaLTeR/identity>`_
- `Jogger <https://codeberg.org/baarkerlounger/jogger>`_
- `Junction <https://github.com/sonnyp/Junction/>`_ - `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>`_ - `Login Manager Settings <https://github.com/realmazharhussain/gdm-settings>`_
- `Maniatic Launcher <https://github.com/santiagocezar/maniatic-launcher/>`_ - `Maniatic Launcher <https://github.com/santiagocezar/maniatic-launcher/>`_
- `Master Key <https://gitlab.com/guillermop/master-key/>`_
- `Misson Center <https://github.com/flathub/io.missioncenter.MissionCenter>`_
- `NewCaw <https://github.com/CodedOre/NewCaw>`_ - `NewCaw <https://github.com/CodedOre/NewCaw>`_
- `Paper <https://gitlab.com/posidon_software/paper>`_ - `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>`_ - `Passes <https://github.com/pablo-s/passes>`_
- `Pipeline <https://gitlab.com/schmiddi-on-mobile/pipeline>`_
- `Playhouse <https://github.com/sonnyp/Playhouse>`_ - `Playhouse <https://github.com/sonnyp/Playhouse>`_
- `Plitki <https://github.com/YaLTeR/plitki>`_ - `Plitki <https://github.com/YaLTeR/plitki>`_
- `Raider <https://github.com/ADBeveridge/raider>`_ - `Raider <https://github.com/ADBeveridge/raider>`_
- `Retro <https://github.com/sonnyp/Retro>`_ - `Retro <https://github.com/sonnyp/Retro>`_
- `Solanum <https://gitlab.gnome.org/World/Solanum>`_ - `Solanum <https://gitlab.gnome.org/World/Solanum>`_
- `Sudoku Solver <https://gitlab.com/cyberphantom52/sudoku-solver>`_
- `Swatch <https://gitlab.gnome.org/GabMus/swatch>`_ - `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/>`_ - `Tangram <https://github.com/sonnyp/Tangram/>`_
- `Text Pieces <https://github.com/liferooter/textpieces>`_ - `Text Pieces <https://github.com/liferooter/textpieces>`_
- `Upscaler <https://gitlab.gnome.org/World/Upscaler>`_
- `Video Trimmer <https://gitlab.gnome.org/YaLTeR/video-trimmer>`_ - `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>`_ - `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>`_ - `Workbench <https://github.com/sonnyp/Workbench>`_

View file

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

View file

@ -13,7 +13,7 @@ GObject Introspection
Blueprint files can import GObject Introspection namespaces like this: Blueprint files can import GObject Introspection namespaces like this:
.. code-block:: .. code-block:: blueprint
using Gtk 4.0; using Gtk 4.0;
using Adw 1; using Adw 1;

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 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``, from source. You can install it using ``meson _build`` and ``ninja -C _build install``,
or you can leave it uninstalled. or you can leave it uninstalled.
@ -29,7 +29,7 @@ blueprint-compiler works as a meson subproject.
[wrap-git] [wrap-git]
directory = blueprint-compiler directory = blueprint-compiler
url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git
revision = main revision = main
depth = 1 depth = 1

View file

@ -5,7 +5,7 @@ Translations
Blueprint files can be translated with xgettext. To mark a string as translated, Blueprint files can be translated with xgettext. To mark a string as translated,
use the following syntax: use the following syntax:
.. code-block:: .. code-block:: blueprint
_("translated string") _("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') i18n.gettext('package name', preset: 'glib')
You must use double quotes for the translated strings in order for gettext to recognize them. Newer versions of blueprint will warn you if you use single quotes.
Contexts Contexts
-------- --------
@ -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_`` contexts, might be different in another language! To disambiguate, use ``C_``
instead of ``_`` and add a context string as the first argument: instead of ``_`` and add a context string as the first argument:
.. code-block:: .. code-block:: blueprint
C_("shortcuts window", "Quit") 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', project('blueprint-compiler',
version: '0.6.0', version: '0.16.0',
) )
subdir('docs')
prefix = get_option('prefix') prefix = get_option('prefix')
datadir = join_paths(prefix, get_option('datadir')) datadir = join_paths(prefix, get_option('datadir'))
py = import('python').find_installation('python3') py = import('python').find_installation('python3')
subdir('docs')
configure_file( configure_file(
input: 'blueprint-compiler.pc.in', input: 'blueprint-compiler.pc.in',
output: 'blueprint-compiler.pc', output: 'blueprint-compiler.pc',

View file

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

View file

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

View file

@ -0,0 +1,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,15 +1,22 @@
import os, sys import os
import sys
from pythonfuzz.main import PythonFuzz from pythonfuzz.main import PythonFuzz
from blueprintcompiler.outputs.xml import XmlOutput from blueprintcompiler.outputs.xml import XmlOutput
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from blueprintcompiler import tokenizer, parser, decompiler, gir from blueprintcompiler import decompiler, gir, parser, tokenizer, utils
from blueprintcompiler.completions import complete 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.tokenizer import Token, TokenType, tokenize
from blueprintcompiler import utils
@PythonFuzz @PythonFuzz
def fuzz(buf): def fuzz(buf):
@ -20,7 +27,7 @@ def fuzz(buf):
ast, errors, warnings = parser.parse(tokens) ast, errors, warnings = parser.parse(tokens)
xml = XmlOutput() xml = XmlOutput()
if errors is None and len(ast.errors) == 0: if errors is None and ast is not None:
xml.emit(ast) xml.emit(ast)
except CompilerBugError as e: except CompilerBugError as e:
raise e raise e
@ -29,6 +36,7 @@ def fuzz(buf):
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
if __name__ == "__main__": if __name__ == "__main__":
# Make sure Gtk 4.0 is accessible, otherwise every test will fail on that # Make sure Gtk 4.0 is accessible, otherwise every test will fail on that
# and nothing interesting will be tested # and nothing interesting will be tested

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,4 +1,4 @@
using Gtk 4.0; using Gtk 4.0;
template MyWidget : Gtk.Widget {} template $MyWidget : Gtk.Widget {}
Gtk.Widget {} Gtk.Widget {}

View file

@ -1 +1 @@
4,13,15,Action widget must have ID 4,6,22,Action widget must have ID

View file

@ -1 +1 @@
4,13,11,Gtk.Box doesn't have action widgets 4,6,18,Gtk.Box doesn't have action widgets

View file

@ -1 +1 @@
4,24,4,Numeric response type can't be negative 4,25,3,Numeric response type can't be negative

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