Compare commits

...

165 commits
v0.8.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
284 changed files with 5790 additions and 1189 deletions

View file

@ -3,11 +3,12 @@ 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:
- black --check --diff blueprintcompiler tests - black --check --diff ./ tests
- mypy --python-version=3.9 blueprintcompiler - isort --check --diff --profile black ./ tests
- mypy --python-version=3.9 blueprintcompiler/
- G_DEBUG=fatal-warnings xvfb-run coverage run -m unittest - G_DEBUG=fatal-warnings xvfb-run coverage run -m unittest
- coverage report - coverage report
- coverage html - coverage html
@ -18,7 +19,7 @@ build:
- 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 3077f669fc9c8e3ceb4da85e6bda680c297c58a2 - git checkout 5f9e155c1333e84e6f683cdb26b02a5925fd8db3
- ./test.sh - ./test.sh
- cd .. - cd ..
coverage: '/TOTAL.*\s([.\d]+)%/' coverage: '/TOTAL.*\s([.\d]+)%/'
@ -32,7 +33,7 @@ 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
script: script:
- meson _build - meson _build

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)

149
NEWS.md
View file

@ -1,3 +1,152 @@
# 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 # v0.8.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,12 +17,13 @@
# #
# 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") TType = T.TypeVar("TType")
@ -37,12 +38,10 @@ class Children:
return iter(self._children) return iter(self._children)
@T.overload @T.overload
def __getitem__(self, key: T.Type[TType]) -> T.List[TType]: def __getitem__(self, key: T.Type[TType]) -> T.List[TType]: ...
...
@T.overload @T.overload
def __getitem__(self, key: int) -> "AstNode": def __getitem__(self, key: int) -> "AstNode": ...
...
def __getitem__(self, key): def __getitem__(self, key):
if isinstance(key, int): if isinstance(key, int):
@ -54,6 +53,18 @@ class Children:
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") TCtx = T.TypeVar("TCtx")
TAttr = T.TypeVar("TAttr") TAttr = T.TypeVar("TAttr")
@ -102,6 +113,10 @@ class AstNode:
def context(self): def context(self):
return Ctx(self) return Ctx(self)
@cached_property
def ranges(self):
return Ranges(self.group.ranges)
@cached_property @cached_property
def root(self): def root(self):
if self.parent is None: if self.parent is None:
@ -109,6 +124,10 @@ class AstNode:
else: else:
return self.parent.root return self.parent.root
@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: def parent_by_type(self, type: T.Type[TType]) -> TType:
if self.parent is None: if self.parent is None:
raise CompilerBugError() raise CompilerBugError()
@ -141,6 +160,11 @@ class AstNode:
yield e yield e
if e.fatal: if e.fatal:
return return
except MultipleErrors as e:
for error in e.errors:
yield error
if error.fatal:
return
for child in self.children: for child in self.children:
yield from child._get_errors() yield from child._get_errors()
@ -160,22 +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 validate_unique_in_parent(self, error, check=None): def get_reference(self, idx: int) -> T.Optional[LocationLink]:
for child in self.children:
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 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
@ -186,23 +234,36 @@ class AstNode:
error, error,
references=[ references=[
ErrorReference( ErrorReference(
child.group.start, child.range,
child.group.end,
"previous declaration was here", "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:
@ -211,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

View file

@ -19,25 +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( def _complete(
ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int lsp, ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int
) -> T.Iterator[Completion]: ) -> T.Iterator[Completion]:
for child in ast_node.children: for child in ast_node.children:
if child.group.start <= idx and ( if child.group.start <= idx and (
idx < child.group.end or (idx == child.group.end and child.incomplete) idx < child.group.end or (idx == child.group.end and child.incomplete)
): ):
yield from _complete(child, tokens, idx, token_idx) yield from _complete(lsp, child, tokens, idx, token_idx)
return return
prev_tokens: T.List[Token] = [] prev_tokens: T.List[Token] = []
@ -50,11 +50,11 @@ def _complete(
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( def complete(
ast_node: AstNode, tokens: T.List[Token], idx: int lsp, ast_node: AstNode, tokens: T.List[Token], idx: int
) -> T.Iterator[Completion]: ) -> T.Iterator[Completion]:
token_idx = 0 token_idx = 0
# find the current token # find the current token
@ -67,19 +67,21 @@ def complete(
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:
@ -97,55 +99,131 @@ def namespace(ast_node, match_variables):
[(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 and not isinstance(ast_node.gir_class, gir.ExternType): 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.BaseAttribute], applies_in=[language.Property, language.A11yProperty],
matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]],
) )
def prop_value_completer(ast_node, match_variables): def prop_value_completer(lsp, ast_node, match_variables):
if isinstance(ast_node.value_type, gir.Enumeration): if (vt := ast_node.value_type) is not None:
for name, member in ast_node.value_type.members.items(): if isinstance(vt.value_type, gir.Enumeration):
yield Completion(name, CompletionItemKind.EnumMember, docs=member.doc) for name, member in vt.value_type.members.items():
yield Completion(
name,
CompletionItemKind.EnumMember,
docs=member.doc,
detail=member.detail,
)
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)
@completer( @completer(
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 and not isinstance(ast_node.gir_class, gir.ExternType): 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:
@ -156,14 +234,17 @@ def signal_completer(ast_node, match_variables):
.lower() .lower()
) )
yield Completion( yield Completion(
signal, signal_name,
CompletionItemKind.Property, CompletionItemKind.Event,
snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;", sort_text=f"1 {signal_name}",
snippet=f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;",
docs=signal.doc,
detail=signal.detail,
) )
@completer(applies_in=[language.UI], matches=new_statement_patterns) @completer(applies_in=[language.UI], matches=new_statement_patterns)
def template_completer(ast_node, match_variables): def template_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion(
"template", "template",
CompletionItemKind.Snippet, CompletionItemKind.Snippet,

View file

@ -20,9 +20,8 @@
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, "{")],
@ -32,27 +31,16 @@ new_statement_patterns = [
] ]
def applies_to(*ast_types):
"""Decorator describing which AST nodes the completer should apply in."""
def decorator(func):
for c in ast_types:
c.completers.append(func)
return func
return decorator
def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None):
def decorator(func): def decorator(func):
def inner(prev_tokens: T.List[Token], ast_node): 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( type = ast_node.root.gir.get_type(
applies_in_subclass[1], applies_in_subclass[0] applies_in_subclass[1], applies_in_subclass[0]
) )
if ast_node.gir_class and not ast_node.gir_class.assignable_to(type): 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
@ -78,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, parse_string 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,25 +51,32 @@ class LineType(Enum):
class DecompileCtx: class DecompileCtx:
def __init__(self) -> None: def __init__(self, parent_gir: T.Optional[GirContext] = None) -> None:
self.sub_decompiler = parent_gir is not None
self._result: str = "" self._result: str = ""
self.gir = GirContext() self.gir = parent_gir or GirContext()
self._indent: int = 0
self._blocks_need_end: T.List[str] = [] self._blocks_need_end: T.List[str] = []
self._last_line_type: LineType = LineType.NONE self._last_line_type: LineType = LineType.NONE
self.template_class: T.Optional[str] = None self._obj_type_stack: list[T.Optional[GirType]] = []
self._node_stack: list[Element] = []
self.gir.add_namespace(get_namespace("Gtk", "4.0")) self.gir.add_namespace(get_namespace("Gtk", "4.0"))
@property @property
def result(self) -> str: def result(self) -> str:
imports = "\n".join( imports = ""
[
f"using {ns} {namespace.version};" if not self.sub_decompiler:
for ns, namespace in self.gir.namespaces.items() import_lines = sorted(
] [
) f"using {ns} {namespace.version};"
return imports + "\n" + self._result for ns, namespace in self.gir.namespaces.items()
if ns != "Gtk"
]
)
imports += "\n".join(["using Gtk 4.0;", *import_lines])
return formatter.format(imports + self._result)
def type_by_cname(self, cname: str) -> T.Optional[GirType]: 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):
@ -88,46 +95,86 @@ class DecompileCtx:
def start_block(self) -> None: def start_block(self) -> None:
self._blocks_need_end.append("") self._blocks_need_end.append("")
self._obj_type_stack.append(None)
def end_block(self) -> None: 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()
@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: 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: str, newline: bool = True) -> None:
if line == "}" or line == "]": self._result += 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: str, value: str, type: GirType) -> None: # Converts a value from an XML element to a blueprint string
# based on the given type. Returns a tuple of translator comments
# (if any) and the decompiled syntax.
def decompile_value(
self,
value: str,
type: T.Optional[GirType],
translatable: T.Optional[T.Tuple[str, str, str]] = None,
) -> T.Tuple[str, str]:
def get_enum_name(value): def get_enum_name(value):
for member in type.members.values(): for member in type.members.values():
if ( if (
@ -138,13 +185,18 @@ class DecompileCtx:
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"))
@ -158,67 +210,82 @@ class DecompileCtx:
self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger") self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger")
) )
): ):
self.print(f'{name}: "{escape_quote(value)}";') return "", escape_quote(value)
elif value == self.template_class: elif value == self.template_class:
self.print(f"{name}: template;") return "", "template"
elif type.assignable_to( elif type.assignable_to(
self.gir.namespaces["Gtk"].lookup_type("GObject.Object") self.gir.namespaces["Gtk"].lookup_type("GObject.Object")
): ) or isinstance(type, Interface):
self.print(f"{name}: {value};") 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:
return "", f"typeof<${value}>"
else: else:
self.print(f'{name}: "{escape_quote(value)}";') return "", escape_quote(value)
def _decompile_element( def decompile_element(
ctx: DecompileCtx, gir: T.Optional[GirContext], xml: Element ctx: DecompileCtx, gir: T.Optional[GirContext], xml: Element
) -> None: ) -> 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: T.Dict[str, T.Optional[str]] = { decompiler = decompilers[0]
canon(name): value for name, value in xml.attrs.items()
}
if decompiler._cdata:
if len(xml.children):
args["cdata"] = None
else:
args["cdata"] = xml.cdata
if decompiler._element:
args = [ctx, gir, xml]
kwargs: T.Dict[str, T.Optional[str]] = {}
else:
args = [ctx, gir]
kwargs = {canon(name): value for name, value in xml.attrs.items()}
if decompiler._cdata:
if len(xml.children):
kwargs["cdata"] = None
else:
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)
except TypeError as e:
raise UnsupportedError(tag=xml.tag)
if not decompiler._skip_children:
for child in xml.children:
decompile_element(ctx, gir, child)
ctx.end_block() ctx.end_block()
ctx._node_stack.pop()
except UnsupportedError as e: except UnsupportedError as e:
raise e raise e
except TypeError as e:
raise UnsupportedError(tag=xml.tag)
def decompile(data: str) -> str: 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): def decompile_string(data: str) -> str:
ctx = DecompileCtx() ctx = DecompileCtx()
xml = parse_string(data) xml = parse_string(data)
_decompile_element(ctx, None, xml) decompile_element(ctx, None, xml)
return ctx.result return ctx.result
@ -231,10 +298,10 @@ def canon(string: str) -> str:
def truthy(string: str) -> bool: def truthy(string: str) -> bool:
return string.lower() in ["yes", "true", "t", "y", "1"] return string is not None and string.lower() in ["yes", "true", "t", "y", "1"]
def full_name(gir) -> str: def full_name(gir: GirType) -> str:
return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name
@ -245,26 +312,45 @@ def lookup_by_cname(gir, cname: str) -> T.Optional[GirType]:
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
@ -283,18 +369,20 @@ def decompile_translatable(
translatable: T.Optional[str], translatable: T.Optional[str],
context: T.Optional[str], context: T.Optional[str],
comments: T.Optional[str], comments: T.Optional[str],
) -> T.Tuple[T.Optional[str], str]: ) -> T.Tuple[str, str]:
if translatable is not None and truthy(translatable): if translatable is not None and truthy(translatable):
if comments is not None: if comments is None:
comments = ""
else:
comments = comments.replace("/*", " ").replace("*/", " ") comments = comments.replace("/*", " ").replace("*/", " ")
comments = f"/* Translators: {comments} */" comments = f"/* Translators: {comments} */"
if context is not None: if context is not None:
return comments, f'C_("{escape_quote(context)}", "{escape_quote(string)}")' return comments, f"C_({escape_quote(context)}, {escape_quote(string)})"
else: else:
return comments, f'_("{escape_quote(string)}")' return comments, f"_({escape_quote(string)})"
else: else:
return comments, f'"{escape_quote(string)}"' return "", f"{escape_quote(string)}"
@decompiler("property", cdata=True) @decompiler("property", cdata=True)
@ -311,11 +399,8 @@ def decompile_property(
context=None, context=None,
): ):
name = name.replace("_", "-") name = name.replace("_", "-")
if comments is not None:
ctx.print(f"/* Translators: {comments} */")
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 = ""
@ -326,7 +411,11 @@ def decompile_property(
flags += " inverted" flags += " inverted"
if "bidirectional" in bind_flags: if "bidirectional" in bind_flags:
flags += " bidirectional" flags += " bidirectional"
ctx.print(f"{name}: bind-property {bind_source}.{bind_property}{flags};")
if bind_source == ctx.template_class:
bind_source = "template"
ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
elif truthy(translatable): elif truthy(translatable):
comments, translatable = decompile_translatable( comments, translatable = decompile_translatable(
cdata, translatable, context, comments cdata, translatable, context, comments
@ -335,9 +424,20 @@ def decompile_property(
ctx.print(comments) ctx.print(comments)
ctx.print(f"{name}: {translatable};") 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

View file

@ -17,10 +17,13 @@
# #
# 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
@ -34,8 +37,7 @@ class PrintableError(Exception):
@dataclass @dataclass
class ErrorReference: class ErrorReference:
start: int range: Range
end: int
message: str message: str
@ -48,8 +50,7 @@ class CompileError(PrintableError):
def __init__( def __init__(
self, self,
message: str, message: str,
start: T.Optional[int] = None, range: T.Optional[Range] = None,
end: T.Optional[int] = None,
did_you_mean: T.Optional[T.Tuple[str, T.List[str]]] = None, did_you_mean: T.Optional[T.Tuple[str, T.List[str]]] = None,
hints: T.Optional[T.List[str]] = None, hints: T.Optional[T.List[str]] = None,
actions: T.Optional[T.List["CodeAction"]] = None, actions: T.Optional[T.List["CodeAction"]] = None,
@ -59,8 +60,7 @@ class CompileError(PrintableError):
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 []
@ -90,25 +90,56 @@ class CompileError(PrintableError):
self.hint("Are your dependencies up to date?") self.hint("Are your dependencies up to date?")
def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None: def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None:
assert self.start is not None assert self.range is not None
line_num, col_num = utils.idx_to_pos(self.start + 1, code) line_num, col_num = utils.idx_to_pos(self.range.start + 1, code)
line = code.splitlines(True)[line_num] 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
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( stream.write(
f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR} 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
@ -126,20 +157,29 @@ class CompileWarning(CompileError):
color = Colors.YELLOW color = Colors.YELLOW
class DeprecatedWarning(CompileWarning):
pass
class UnusedWarning(CompileWarning):
pass
class UpgradeWarning(CompileWarning): class UpgradeWarning(CompileWarning):
category = "upgrade" category = "upgrade"
color = Colors.PURPLE color = Colors.PURPLE
class UnexpectedTokenError(CompileError): class UnexpectedTokenError(CompileError):
def __init__(self, start, end) -> None: 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):
@ -179,7 +219,7 @@ def report_bug(): # pragma: no cover
f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** 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}"""
) )

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"

View file

@ -17,17 +17,31 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from functools import cached_property import os
import sys
import typing as T import typing as T
import os, sys from functools import cached_property
import gi # type: ignore import gi # type: ignore
gi.require_version("GIRepository", "2.0") try:
from gi.repository import GIRepository # type: ignore gi.require_version("GIRepository", "3.0")
from gi.repository import GIRepository # type: ignore
_repo = GIRepository.Repository()
except ValueError:
# We can remove this once we can bump the minimum dependencies
# to glib 2.80 and pygobject 3.52
# dependency('glib-2.0', version: '>= 2.80.0')
# dependency('girepository-2.0', version: '>= 2.80.0')
gi.require_version("GIRepository", "2.0")
from gi.repository import GIRepository # type: ignore
_repo = GIRepository.Repository
from .errors import CompileError, CompilerBugError
from . import typelib, xml_reader from . import typelib, xml_reader
from .errors import CompileError, CompilerBugError
from .lsp_utils import CodeAction
_namespace_cache: T.Dict[str, "Namespace"] = {} _namespace_cache: T.Dict[str, "Namespace"] = {}
_xml_cache = {} _xml_cache = {}
@ -40,7 +54,7 @@ def add_typelib_search_path(path: str):
def get_namespace(namespace: str, version: str) -> "Namespace": def get_namespace(namespace: str, version: str) -> "Namespace":
search_paths = [*GIRepository.Repository.get_search_path(), *_user_search_paths] search_paths = [*_repo.get_search_path(), *_user_search_paths]
filename = f"{namespace}-{version}.typelib" filename = f"{namespace}-{version}.typelib"
@ -64,6 +78,32 @@ def get_namespace(namespace: str, version: str) -> "Namespace":
return _namespace_cache[filename] return _namespace_cache[filename]
_available_namespaces: list[tuple[str, str]] = []
def get_available_namespaces() -> T.List[T.Tuple[str, str]]:
if len(_available_namespaces):
return _available_namespaces
search_paths: list[str] = [
*_repo.get_search_path(),
*_user_search_paths,
]
for search_path in search_paths:
try:
filenames = os.listdir(search_path)
except FileNotFoundError:
continue
for filename in filenames:
if filename.endswith(".typelib"):
namespace, version = filename.removesuffix(".typelib").rsplit("-", 1)
_available_namespaces.append((namespace, version))
return _available_namespaces
def get_xml(namespace: str, version: str): def get_xml(namespace: str, version: str):
search_paths = [] search_paths = []
@ -91,6 +131,23 @@ def get_xml(namespace: str, version: str):
return _xml_cache[filename] return _xml_cache[filename]
ONLINE_DOCS = {
"Adw-1": "https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/",
"Gdk-4.0": "https://docs.gtk.org/gdk4/",
"GdkPixbuf-2.0": "https://docs.gtk.org/gdk-pixbuf/",
"Gio-2.0": "https://docs.gtk.org/gio/",
"GLib-2.0": "https://docs.gtk.org/glib/",
"GModule-2.0": "https://docs.gtk.org/gmodule/",
"GObject-2.0": "https://docs.gtk.org/gobject/",
"Gsk-4.0": "https://docs.gtk.org/gsk4/",
"Gtk-4.0": "https://docs.gtk.org/gtk4/",
"GtkSource-5": "https://gnome.pages.gitlab.gnome.org/gtksourceview/gtksourceview5",
"Pango-1.0": "https://docs.gtk.org/Pango/",
"Shumate-1.0": "https://gnome.pages.gitlab.gnome.org/libshumate/",
"WebKit2-4.1": "https://webkitgtk.org/reference/webkit2gtk/stable/",
}
class GirType: class GirType:
@property @property
def doc(self) -> T.Optional[str]: def doc(self) -> T.Optional[str]:
@ -118,6 +175,14 @@ class GirType:
def incomplete(self) -> bool: def incomplete(self) -> bool:
return False return False
@property
def deprecated(self) -> bool:
return False
@property
def deprecated_doc(self) -> T.Optional[str]:
return None
class ExternType(GirType): class ExternType(GirType):
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
@ -147,6 +212,10 @@ class ArrayType(GirType):
def assignable_to(self, other: GirType) -> bool: def assignable_to(self, other: GirType) -> bool:
return isinstance(other, ArrayType) and self._inner.assignable_to(other._inner) return isinstance(other, ArrayType) and self._inner.assignable_to(other._inner)
@property
def inner(self) -> GirType:
return self._inner
@property @property
def name(self) -> str: def name(self) -> str:
return self._inner.name + "[]" return self._inner.name + "[]"
@ -235,6 +304,8 @@ TNode = T.TypeVar("TNode", bound="GirNode")
class GirNode: class GirNode:
xml_tag: str
def __init__(self, container: T.Optional["GirNode"], tl: typelib.Typelib) -> None: def __init__(self, container: T.Optional["GirNode"], tl: typelib.Typelib) -> None:
self.container = container self.container = container
self.tl = tl self.tl = tl
@ -251,7 +322,8 @@ class GirNode:
def xml(self): def xml(self):
for el in self.container.xml.children: for el in self.container.xml.children:
if el.attrs.get("name") == self.name: if el.attrs.get("name") == self.name:
return el if el.tag == self.xml_tag:
return el
@cached_property @cached_property
def glib_type_name(self) -> str: def glib_type_name(self) -> str:
@ -276,6 +348,17 @@ class GirNode:
def available_in(self) -> str: def available_in(self) -> str:
return self.xml.get("version") return self.xml.get("version")
@cached_property
def detail(self) -> T.Optional[str]:
try:
el = self.xml.get_elements("doc")
if len(el) == 1:
return el[0].cdata.strip().partition("\n")[0]
else:
return None
except:
return None
@cached_property @cached_property
def doc(self) -> T.Optional[str]: def doc(self) -> T.Optional[str]:
sections = [] sections = []
@ -290,10 +373,17 @@ class GirNode:
except: except:
# Not a huge deal, but if you want docs in the language server you # Not a huge deal, but if you want docs in the language server you
# should ensure .gir files are installed # should ensure .gir files are installed
pass sections.append("Documentation is not installed")
if self.online_docs:
sections.append(f"[Online documentation]({self.online_docs})")
return "\n\n---\n\n".join(sections) return "\n\n---\n\n".join(sections)
@property
def online_docs(self) -> T.Optional[str]:
return None
@property @property
def signature(self) -> T.Optional[str]: def signature(self) -> T.Optional[str]:
return None return None
@ -302,8 +392,17 @@ class GirNode:
def type(self) -> GirType: def type(self) -> GirType:
raise NotImplementedError() raise NotImplementedError()
@property
def deprecated_doc(self) -> T.Optional[str]:
try:
return self.xml.get_elements("doc-deprecated")[0].cdata.strip()
except:
return None
class Property(GirNode): class Property(GirNode):
xml_tag = "property"
def __init__(self, klass: T.Union["Class", "Interface"], tl: typelib.Typelib): def __init__(self, klass: T.Union["Class", "Interface"], tl: typelib.Typelib):
super().__init__(klass, tl) super().__init__(klass, tl)
@ -317,7 +416,7 @@ class Property(GirNode):
@cached_property @cached_property
def signature(self): def signature(self):
return f"{self.full_name} {self.container.name}.{self.name}" return f"{self.type.full_name} {self.container.name}:{self.name}"
@property @property
def writable(self) -> bool: def writable(self) -> bool:
@ -327,31 +426,94 @@ class Property(GirNode):
def construct_only(self) -> bool: def construct_only(self) -> bool:
return self.tl.PROP_CONSTRUCT_ONLY == 1 return self.tl.PROP_CONSTRUCT_ONLY == 1
@property
def online_docs(self) -> T.Optional[str]:
if ns := self.get_containing(Namespace).online_docs:
assert self.container is not None
return f"{ns}property.{self.container.name}.{self.name}.html"
else:
return None
class Parameter(GirNode): @property
def deprecated(self) -> bool:
return self.tl.PROP_DEPRECATED == 1
class Argument(GirNode):
def __init__(self, container: GirNode, tl: typelib.Typelib) -> None: def __init__(self, container: GirNode, tl: typelib.Typelib) -> None:
super().__init__(container, tl) super().__init__(container, tl)
@cached_property
def name(self) -> str:
return self.tl.ARG_NAME
@cached_property
def type(self) -> GirType:
return self.get_containing(Repository)._resolve_type_id(self.tl.ARG_TYPE)
class Signature(GirNode):
def __init__(self, container: GirNode, tl: typelib.Typelib) -> None:
super().__init__(container, tl)
@cached_property
def args(self) -> T.List[Argument]:
n_arguments = self.tl.SIGNATURE_N_ARGUMENTS
blob_size = self.tl.header.HEADER_ARG_BLOB_SIZE
result = []
for i in range(n_arguments):
entry = self.tl.SIGNATURE_ARGUMENTS[i * blob_size]
result.append(Argument(self, entry))
return result
@cached_property
def return_type(self) -> T.Optional[GirType]:
if self.tl.SIGNATURE_RETURN_TYPE == 0:
return None
else:
return self.get_containing(Repository)._resolve_type_id(
self.tl.SIGNATURE_RETURN_TYPE
)
class Signal(GirNode): class Signal(GirNode):
xml_tag = "glib:signal"
def __init__( def __init__(
self, klass: T.Union["Class", "Interface"], tl: typelib.Typelib self, klass: T.Union["Class", "Interface"], tl: typelib.Typelib
) -> None: ) -> None:
super().__init__(klass, tl) super().__init__(klass, tl)
# if parameters := xml.get_elements('parameters'):
# self.params = [Parameter(self, child) for child in parameters[0].get_elements('parameter')] @cached_property
# else: def gir_signature(self) -> Signature:
# self.params = [] return Signature(self, self.tl.SIGNAL_SIGNATURE)
@property @property
def signature(self): def signature(self):
# TODO: fix args = ", ".join(
# args = ", ".join([f"{p.type_name} {p.name}" for p in self.params]) [f"{a.type.full_name} {a.name}" for a in self.gir_signature.args]
args = "" )
return f"signal {self.container.name}.{self.name} ({args})" result = f"signal {self.container.full_name}::{self.name} ({args})"
if self.gir_signature.return_type is not None:
result += f" -> {self.gir_signature.return_type.full_name}"
return result
@property
def online_docs(self) -> T.Optional[str]:
if ns := self.get_containing(Namespace).online_docs:
assert self.container is not None
return f"{ns}signal.{self.container.name}.{self.name}.html"
else:
return None
@property
def deprecated(self) -> bool:
return self.tl.SIGNAL_DEPRECATED == 1
class Interface(GirNode, GirType): class Interface(GirNode, GirType):
xml_tag = "interface"
def __init__(self, ns: "Namespace", tl: typelib.Typelib): def __init__(self, ns: "Namespace", tl: typelib.Typelib):
super().__init__(ns, tl) super().__init__(ns, tl)
@ -402,8 +564,21 @@ class Interface(GirNode, GirType):
return True return True
return False return False
@property
def online_docs(self) -> T.Optional[str]:
if ns := self.get_containing(Namespace).online_docs:
return f"{ns}iface.{self.name}.html"
else:
return None
@property
def deprecated(self) -> bool:
return self.tl.INTERFACE_DEPRECATED == 1
class Class(GirNode, GirType): class Class(GirNode, GirType):
xml_tag = "class"
def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None:
super().__init__(ns, tl) super().__init__(ns, tl)
@ -514,6 +689,17 @@ class Class(GirNode, GirType):
for impl in self.implements: for impl in self.implements:
yield from impl.signals.values() yield from impl.signals.values()
@property
def online_docs(self) -> T.Optional[str]:
if ns := self.get_containing(Namespace).online_docs:
return f"{ns}class.{self.name}.html"
else:
return None
@property
def deprecated(self) -> bool:
return self.tl.OBJ_DEPRECATED == 1
class TemplateType(GirType): class TemplateType(GirType):
def __init__(self, name: str, parent: T.Optional[GirType]): def __init__(self, name: str, parent: T.Optional[GirType]):
@ -553,7 +739,7 @@ class TemplateType(GirType):
# we don't know the template type's interfaces, assume yes # we don't know the template type's interfaces, assume yes
return True return True
elif self.parent is None or isinstance(self.parent, ExternType): elif self.parent is None or isinstance(self.parent, ExternType):
return isinstance(other, Class) return isinstance(other, Class) or isinstance(other, ExternType)
else: else:
return self.parent.assignable_to(other) return self.parent.assignable_to(other)
@ -570,6 +756,8 @@ class TemplateType(GirType):
class EnumMember(GirNode): class EnumMember(GirNode):
xml_tag = "member"
def __init__(self, enum: "Enumeration", tl: typelib.Typelib) -> None: def __init__(self, enum: "Enumeration", tl: typelib.Typelib) -> None:
super().__init__(enum, tl) super().__init__(enum, tl)
@ -595,6 +783,8 @@ class EnumMember(GirNode):
class Enumeration(GirNode, GirType): class Enumeration(GirNode, GirType):
xml_tag = "enumeration"
def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None:
super().__init__(ns, tl) super().__init__(ns, tl)
@ -616,8 +806,21 @@ class Enumeration(GirNode, GirType):
def assignable_to(self, type: GirType) -> bool: def assignable_to(self, type: GirType) -> bool:
return type == self return type == self
@property
def online_docs(self) -> T.Optional[str]:
if ns := self.get_containing(Namespace).online_docs:
return f"{ns}enum.{self.name}.html"
else:
return None
@property
def deprecated(self) -> bool:
return self.tl.ENUM_DEPRECATED == 1
class Boxed(GirNode, GirType): class Boxed(GirNode, GirType):
xml_tag = "glib:boxed"
def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None:
super().__init__(ns, tl) super().__init__(ns, tl)
@ -628,8 +831,21 @@ class Boxed(GirNode, GirType):
def assignable_to(self, type) -> bool: def assignable_to(self, type) -> bool:
return type == self return type == self
@property
def online_docs(self) -> T.Optional[str]:
if ns := self.get_containing(Namespace).online_docs:
return f"{ns}boxed.{self.name}.html"
else:
return None
@property
def deprecated(self) -> bool:
return self.tl.STRUCT_DEPRECATED == 1
class Bitfield(Enumeration): class Bitfield(Enumeration):
xml_tag = "bitfield"
def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None:
super().__init__(ns, tl) super().__init__(ns, tl)
@ -638,29 +854,35 @@ class Namespace(GirNode):
def __init__(self, repo: "Repository", tl: typelib.Typelib) -> None: def __init__(self, repo: "Repository", tl: typelib.Typelib) -> None:
super().__init__(repo, tl) super().__init__(repo, tl)
self.entries: T.Dict[str, GirType] = {} @cached_property
def entries(self) -> T.Mapping[str, GirType]:
entries: dict[str, GirType] = {}
n_local_entries: int = self.tl.HEADER_N_ENTRIES
directory: typelib.Typelib = self.tl.HEADER_DIRECTORY
blob_size: int = self.tl.header.HEADER_ENTRY_BLOB_SIZE
n_local_entries: int = tl.HEADER_N_ENTRIES
directory: typelib.Typelib = tl.HEADER_DIRECTORY
for i in range(n_local_entries): for i in range(n_local_entries):
entry = directory[i * tl.HEADER_ENTRY_BLOB_SIZE] entry = directory[i * blob_size]
entry_name: str = entry.DIR_ENTRY_NAME entry_name: str = entry.DIR_ENTRY_NAME
entry_type: int = entry.DIR_ENTRY_BLOB_TYPE entry_type: int = entry.DIR_ENTRY_BLOB_TYPE
entry_blob: typelib.Typelib = entry.DIR_ENTRY_OFFSET entry_blob: typelib.Typelib = entry.DIR_ENTRY_OFFSET
if entry_type == typelib.BLOB_TYPE_ENUM: if entry_type == typelib.BLOB_TYPE_ENUM:
self.entries[entry_name] = Enumeration(self, entry_blob) entries[entry_name] = Enumeration(self, entry_blob)
elif entry_type == typelib.BLOB_TYPE_FLAGS: elif entry_type == typelib.BLOB_TYPE_FLAGS:
self.entries[entry_name] = Bitfield(self, entry_blob) entries[entry_name] = Bitfield(self, entry_blob)
elif entry_type == typelib.BLOB_TYPE_OBJECT: elif entry_type == typelib.BLOB_TYPE_OBJECT:
self.entries[entry_name] = Class(self, entry_blob) entries[entry_name] = Class(self, entry_blob)
elif entry_type == typelib.BLOB_TYPE_INTERFACE: elif entry_type == typelib.BLOB_TYPE_INTERFACE:
self.entries[entry_name] = Interface(self, entry_blob) entries[entry_name] = Interface(self, entry_blob)
elif ( elif (
entry_type == typelib.BLOB_TYPE_BOXED entry_type == typelib.BLOB_TYPE_BOXED
or entry_type == typelib.BLOB_TYPE_STRUCT or entry_type == typelib.BLOB_TYPE_STRUCT
): ):
self.entries[entry_name] = Boxed(self, entry_blob) entries[entry_name] = Boxed(self, entry_blob)
return entries
@cached_property @cached_property
def xml(self): def xml(self):
@ -686,22 +908,22 @@ class Namespace(GirNode):
if isinstance(entry, Class) if isinstance(entry, Class)
} }
@cached_property
def interfaces(self) -> T.Mapping[str, Interface]:
return {
name: entry
for name, entry in self.entries.items()
if isinstance(entry, Interface)
}
def get_type(self, name) -> T.Optional[GirType]: def get_type(self, name) -> T.Optional[GirType]:
"""Gets a type (class, interface, enum, etc.) from this namespace.""" """Gets a type (class, interface, enum, etc.) from this namespace."""
return self.entries.get(name) return self.entries.get(name)
def get_type_by_cname(self, cname: str) -> T.Optional[GirType]: def get_type_by_cname(self, cname: str) -> T.Optional[GirType]:
"""Gets a type from this namespace by its C name.""" """Gets a type from this namespace by its C name."""
for basic in _BASIC_TYPES.values():
if basic.glib_type_name == cname:
return basic()
for item in self.entries.values(): for item in self.entries.values():
if hasattr(item, "cname") and item.cname == cname: if (
hasattr(item, "cname")
and item.cname is not None
and item.cname == cname
):
return item return item
return None return None
@ -709,13 +931,12 @@ class Namespace(GirNode):
"""Looks up a type in the scope of this namespace (including in the """Looks up a type in the scope of this namespace (including in the
namespace's dependencies).""" namespace's dependencies)."""
if type_name in _BASIC_TYPES: ns, name = type_name.split(".", 1)
return _BASIC_TYPES[type_name]() return self.get_containing(Repository).get_type(name, ns)
elif "." in type_name:
ns, name = type_name.split(".", 1) @property
return self.get_containing(Repository).get_type(name, ns) def online_docs(self) -> T.Optional[str]:
else: return ONLINE_DOCS.get(f"{self.name}-{self.version}")
return self.get_type(type_name)
class Repository(GirNode): class Repository(GirNode):
@ -730,7 +951,7 @@ class Repository(GirNode):
self.includes = { self.includes = {
name: get_namespace(name, version) for name, version in deps name: get_namespace(name, version) for name, version in deps
} }
except: except: # pragma: no cover
raise CompilerBugError(f"Failed to load dependencies.") raise CompilerBugError(f"Failed to load dependencies.")
else: else:
self.includes = {} self.includes = {}
@ -738,12 +959,6 @@ class Repository(GirNode):
def get_type(self, name: str, ns: str) -> T.Optional[GirType]: def get_type(self, name: str, ns: str) -> T.Optional[GirType]:
return self.lookup_namespace(ns).get_type(name) return self.lookup_namespace(ns).get_type(name)
def get_type_by_cname(self, name: str) -> T.Optional[GirType]:
for ns in [self.namespace, *self.includes.values()]:
if type := ns.get_type_by_cname(name):
return type
return None
def lookup_namespace(self, ns: str): def lookup_namespace(self, ns: str):
"""Finds a namespace among this namespace's dependencies.""" """Finds a namespace among this namespace's dependencies."""
if ns == self.namespace.name: if ns == self.namespace.name:
@ -761,8 +976,8 @@ class Repository(GirNode):
return self.lookup_namespace(ns).get_type(dir_entry.DIR_ENTRY_NAME) return self.lookup_namespace(ns).get_type(dir_entry.DIR_ENTRY_NAME)
def _resolve_type_id(self, type_id: int) -> GirType: def _resolve_type_id(self, type_id: int) -> GirType:
if type_id & 0xFFFFFF == 0: if type_id & (0xFFFFFF if sys.byteorder == "little" else 0xFFFFFF00) == 0:
type_id = (type_id >> 27) & 0x1F type_id = ((type_id >> 27) if sys.byteorder == "little" else type_id) & 0x1F
# simple type # simple type
if type_id == typelib.TYPE_BOOLEAN: if type_id == typelib.TYPE_BOOLEAN:
return BoolType() return BoolType()
@ -845,9 +1060,11 @@ class GirContext:
ns = ns or "Gtk" ns = ns or "Gtk"
if ns not in self.namespaces and ns not in self.not_found_namespaces: if ns not in self.namespaces and ns not in self.not_found_namespaces:
all_available = list(set(ns for ns, _version in get_available_namespaces()))
raise CompileError( raise CompileError(
f"Namespace {ns} was not imported", f"Namespace {ns} was not imported",
did_you_mean=(ns, self.namespaces.keys()), did_you_mean=(ns, all_available),
) )
def validate_type(self, name: str, ns: str) -> None: def validate_type(self, name: str, ns: str) -> None:

View file

@ -18,16 +18,15 @@
# 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, CompilerBugError
from .utils import Colors from .utils import Colors
# A tool to interactively port projects to blueprints. # A tool to interactively port projects to blueprints.
@ -72,7 +71,7 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
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")
@ -137,7 +136,7 @@ def step1():
wrap.write( wrap.write(
f"""[wrap-git] 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
@ -302,9 +301,7 @@ def step5(in_files):
( (
Colors.GREEN Colors.GREEN
if line.startswith("+") if line.startswith("+")
else Colors.RED + Colors.FAINT else Colors.RED + Colors.FAINT if line.startswith("-") else ""
if line.startswith("-")
else ""
) )
+ line + line
+ Colors.CLEAR + Colors.CLEAR

View file

@ -1,12 +1,11 @@
from .gtk_list_item_factory import ExtListItemFactory
from .adw_message_dialog import ExtAdwMessageDialog
from .attributes import BaseAttribute
from .adw_breakpoint import ( from .adw_breakpoint import (
AdwBreakpointSetters,
AdwBreakpointSetter,
AdwBreakpointCondition, AdwBreakpointCondition,
AdwBreakpointSetter,
AdwBreakpointSetters,
) )
from .adw_response_dialog import ExtAdwResponseDialog
from .binding import Binding from .binding import Binding
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx from .contexts import ScopeCtx, ValueTypeCtx
from .expression import ( from .expression import (
CastExpr, CastExpr,
@ -20,27 +19,29 @@ from .expression import (
from .gobject_object import Object, ObjectContent from .gobject_object import Object, ObjectContent
from .gobject_property import Property from .gobject_property import Property
from .gobject_signal import Signal from .gobject_signal import Signal
from .gtk_a11y import ExtAccessibility from .gtk_a11y import A11yProperty, ExtAccessibility
from .gtk_combo_box_text import ExtComboBoxItems from .gtk_combo_box_text import ExtComboBoxItems
from .gtk_file_filter import ( from .gtk_file_filter import (
Filters,
ext_file_filter_mime_types, ext_file_filter_mime_types,
ext_file_filter_patterns, ext_file_filter_patterns,
ext_file_filter_suffixes, ext_file_filter_suffixes,
Filters,
) )
from .gtk_layout import ExtLayout from .gtk_layout import ExtLayout
from .gtk_menu import menu, Menu, MenuAttribute from .gtk_list_item_factory import ExtListItemFactory
from .gtk_menu import Menu, MenuAttribute, menu
from .gtk_scale import ExtScaleMarks from .gtk_scale import ExtScaleMarks
from .gtk_size_group import ExtSizeGroupWidgets from .gtk_size_group import ExtSizeGroupWidgets
from .gtk_string_list import ExtStringListStrings from .gtk_string_list import ExtStringListStrings
from .gtk_styles import ExtStyles from .gtk_styles import ExtStyles
from .gtkbuilder_child import Child, ChildType, ChildInternal, ChildExtension 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 .property_binding import PropertyBinding
from .ui import UI
from .types import ClassName from .types import ClassName
from .ui import UI
from .values import ( from .values import (
ArrayValue,
ExprValue,
Flag, Flag,
Flags, Flags,
IdentLiteral, IdentLiteral,
@ -54,15 +55,13 @@ from .values import (
Value, Value,
) )
from .common import *
OBJECT_CONTENT_HOOKS.children = [ OBJECT_CONTENT_HOOKS.children = [
Signal, Signal,
Property, Property,
AdwBreakpointCondition, AdwBreakpointCondition,
AdwBreakpointSetters, AdwBreakpointSetters,
ExtAccessibility, ExtAccessibility,
ExtAdwMessageDialog, ExtAdwResponseDialog,
ExtComboBoxItems, ExtComboBoxItems,
ext_file_filter_mime_types, ext_file_filter_mime_types,
ext_file_filter_patterns, ext_file_filter_patterns,

View file

@ -24,12 +24,36 @@ from .values import Value
class AdwBreakpointCondition(AstNode): class AdwBreakpointCondition(AstNode):
grammar = ["condition", "(", UseQuoted("condition"), Match(")").expected()] grammar = [
UseExact("kw", "condition"),
"(",
UseQuoted("condition"),
Match(")").expected(),
]
@property @property
def condition(self) -> str: def condition(self) -> str:
return self.tokens["condition"] 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() @validate()
def unique(self): def unique(self):
self.validate_unique_in_parent("Duplicate condition statement") self.validate_unique_in_parent("Duplicate condition statement")
@ -57,8 +81,8 @@ class AdwBreakpointSetter(AstNode):
return self.tokens["property"] return self.tokens["property"]
@property @property
def value(self) -> Value: def value(self) -> T.Optional[Value]:
return self.children[Value][0] return self.children[Value][0] if len(self.children[Value]) > 0 else None
@property @property
def gir_class(self) -> T.Optional[GirType]: def gir_class(self) -> T.Optional[GirType]:
@ -68,9 +92,42 @@ class AdwBreakpointSetter(AstNode):
return None return None
@property @property
def gir_property(self): def gir_property(self) -> T.Optional[gir.Property]:
if self.gir_class is not None and not isinstance(self.gir_class, ExternType): 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) 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) @context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx: def value_type(self) -> ValueTypeCtx:
@ -81,6 +138,20 @@ class AdwBreakpointSetter(AstNode):
return ValueTypeCtx(type, allow_null=True) 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") @validate("object")
def object_exists(self): def object_exists(self):
if self.object is None: if self.object is None:
@ -112,12 +183,25 @@ class AdwBreakpointSetter(AstNode):
class AdwBreakpointSetters(AstNode): class AdwBreakpointSetters(AstNode):
grammar = ["setters", Match("{").expected(), Until(AdwBreakpointSetter, "}")] grammar = [
Keyword("setters"),
Match("{").expected(),
Until(AdwBreakpointSetter, "}"),
]
@property @property
def setters(self) -> T.List[AdwBreakpointSetter]: def setters(self) -> T.List[AdwBreakpointSetter]:
return self.children[AdwBreakpointSetter] return self.children[AdwBreakpointSetter]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"setters",
SymbolKind.Struct,
self.range,
self.group.tokens["setters"].range,
)
@validate() @validate()
def container_is_breakpoint(self): def container_is_breakpoint(self):
validate_parent_type(self, "Adw", "Breakpoint", "breakpoint setters") validate_parent_type(self, "Adw", "Breakpoint", "breakpoint setters")
@ -125,3 +209,46 @@ class AdwBreakpointSetters(AstNode):
@validate() @validate()
def unique(self): def unique(self):
self.validate_unique_in_parent("Duplicate setters block") 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

@ -1,4 +1,4 @@
# adw_message_dialog.py # adw_response_dialog.py
# #
# Copyright 2023 James Westman <james@jwestman.net> # Copyright 2023 James Westman <james@jwestman.net>
# #
@ -18,46 +18,80 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from ..decompiler import truthy, decompile_translatable from ..decompiler import decompile_translatable, truthy
from .common import * from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue from .values import StringValue
class Response(AstNode): 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 = [ grammar = [
UseIdent("id"), UseIdent("id"),
Match(":").expected(), Match(":").expected(),
to_parse_node(StringValue).expected("a string or translatable string"), to_parse_node(StringValue).expected("a string or translatable string"),
ZeroOrMore( ZeroOrMore(ExtAdwResponseDialogFlag),
AnyOf(Keyword("destructive"), Keyword("suggested"), Keyword("disabled"))
),
] ]
@property @property
def id(self) -> str: def id(self) -> str:
return self.tokens["id"] return self.tokens["id"]
@property
def flags(self) -> T.List[ExtAdwResponseDialogFlag]:
return self.children[ExtAdwResponseDialogFlag]
@property @property
def appearance(self) -> T.Optional[str]: def appearance(self) -> T.Optional[str]:
if "destructive" in self.tokens: if any(flag.flag == "destructive" for flag in self.flags):
return "destructive" return "destructive"
if "suggested" in self.tokens: elif any(flag.flag == "suggested" for flag in self.flags):
return "suggested" return "suggested"
return None else:
return None
@property @property
def enabled(self) -> bool: def enabled(self) -> bool:
return "disabled" not in self.tokens return not any(flag.flag == "disabled" for flag in self.flags)
@property @property
def value(self) -> StringValue: def value(self) -> StringValue:
return self.children[0] return self.children[0]
@context(ValueTypeCtx) @property
def value_type(self) -> ValueTypeCtx: def document_symbol(self) -> DocumentSymbol:
return ValueTypeCtx(StringType()) return DocumentSymbol(
self.id,
SymbolKind.Field,
self.range,
self.group.tokens["id"].range,
self.value.range.text,
)
@validate("id") @validate("id")
def unique_in_parent(self): def unique_in_parent(self):
@ -67,33 +101,60 @@ class Response(AstNode):
) )
class ExtAdwMessageDialog(AstNode): class ExtAdwResponseDialog(AstNode):
grammar = [ grammar = [
Keyword("responses"), Keyword("responses"),
Match("[").expected(), Match("[").expected(),
Delimited(Response, ","), Delimited(ExtAdwResponseDialogResponse, ","),
"]", "]",
] ]
@property @property
def responses(self) -> T.List[Response]: def responses(self) -> T.List[ExtAdwResponseDialogResponse]:
return self.children return self.children
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"responses",
SymbolKind.Array,
self.range,
self.group.tokens["responses"].range,
)
@validate("responses") @validate("responses")
def container_is_message_dialog(self): def container_is_message_dialog_or_alert_dialog(self):
validate_parent_type(self, "Adw", "MessageDialog", "responses") try:
validate_parent_type(self, "Adw", "MessageDialog", "responses")
except:
validate_parent_type(self, "Adw", "AlertDialog", "responses")
@validate("responses") @validate("responses")
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate responses block") self.validate_unique_in_parent("Duplicate responses block")
@docs()
def ref_docs(self):
return get_docs_section("Syntax ExtAdwMessageDialog")
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Adw", "MessageDialog"), applies_in_subclass=("Adw", "MessageDialog"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def style_completer(ast_node, match_variables): 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( yield Completion(
"responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]"
) )

View file

@ -20,19 +20,64 @@
from dataclasses import dataclass from dataclasses import dataclass
from .common import * from .common import *
from .expression import Expression, LookupOp, LiteralExpr 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): class Binding(AstNode):
grammar = [ grammar = [
Keyword("bind"), AnyOf(Keyword("bind"), UseExact("bind", "bind-property")),
Expression, Expression,
ZeroOrMore(BindingFlag),
] ]
@property @property
def expression(self) -> Expression: def expression(self) -> Expression:
return self.children[Expression][0] return self.children[Expression][0]
@property
def flags(self) -> T.List[BindingFlag]:
return self.children[BindingFlag]
@property @property
def simple_binding(self) -> T.Optional["SimpleBinding"]: def simple_binding(self) -> T.Optional["SimpleBinding"]:
if isinstance(self.expression.last, LookupOp): if isinstance(self.expression.last, LookupOp):
@ -40,14 +85,39 @@ class Binding(AstNode):
from .values import IdentLiteral from .values import IdentLiteral
if isinstance(self.expression.last.lhs.literal.value, IdentLiteral): if isinstance(self.expression.last.lhs.literal.value, IdentLiteral):
flags = [x.flag for x in self.flags]
return SimpleBinding( return SimpleBinding(
self.expression.last.lhs.literal.value.ident, self.expression.last.lhs.literal.value.ident,
self.expression.last.property_name, self.expression.last.property_name,
no_sync_create="no-sync-create" in flags,
bidirectional="bidirectional" in flags,
inverted="inverted" in flags,
) )
return None 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 @dataclass
class SimpleBinding: class SimpleBinding:
source: str source: str
property_name: 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,36 +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, context
from ..errors import (
CompileError,
MultipleErrors,
UpgradeWarning,
CompileWarning,
CodeAction,
)
from ..completions_utils import *
from .. import decompiler as decompile from .. import decompiler as decompile
from .. import gir
from ..ast_utils import AstNode, context, docs, validate
from ..completions_utils import *
from ..decompiler import ( from ..decompiler import (
DecompileCtx, DecompileCtx,
decompile_translatable,
decompiler, decompiler,
escape_quote, escape_quote,
truthy, truthy,
decompile_translatable, )
from ..errors import (
CodeAction,
CompileError,
CompileWarning,
DeprecatedWarning,
MultipleErrors,
UnusedWarning,
UpgradeWarning,
) )
from ..gir import ( from ..gir import (
StringType,
BoolType, BoolType,
IntType,
FloatType,
GirType,
Enumeration, Enumeration,
ExternType, ExternType,
FloatType,
GirType,
IntType,
StringType,
)
from ..lsp_utils import (
Completion,
CompletionItemKind,
DocumentSymbol,
LocationLink,
SemanticToken,
SemanticTokenType,
SymbolKind,
get_docs_section,
) )
from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType
from ..parse_tree import * from ..parse_tree import *
OBJECT_CONTENT_HOOKS = AnyOf() OBJECT_CONTENT_HOOKS = AnyOf()
LITERAL = AnyOf() LITERAL = AnyOf()

View file

@ -30,6 +30,7 @@ from .gtkbuilder_template import Template
class ValueTypeCtx: class ValueTypeCtx:
value_type: T.Optional[GirType] value_type: T.Optional[GirType]
allow_null: bool = False allow_null: bool = False
must_infer_type: bool = False
@dataclass @dataclass
@ -38,8 +39,8 @@ class ScopeCtx:
@cached_property @cached_property
def template(self): def template(self):
from .ui import UI
from .gtk_list_item_factory import ExtListItemFactory from .gtk_list_item_factory import ExtListItemFactory
from .ui import UI
if isinstance(self.node, UI): if isinstance(self.node, UI):
return self.node.template return self.node.template
@ -69,8 +70,7 @@ class ScopeCtx:
): ):
raise CompileError( raise CompileError(
f"Duplicate object ID '{obj.tokens['id']}'", f"Duplicate object ID '{obj.tokens['id']}'",
token.start, token.range,
token.end,
) )
passed[obj.tokens["id"]] = obj passed[obj.tokens["id"]] = obj
@ -79,3 +79,9 @@ class ScopeCtx:
for child in node.children: for child in node.children:
if child.context[ScopeCtx] is self: if child.context[ScopeCtx] is self:
yield from self._iter_recursive(child) yield from self._iter_recursive(child)
@dataclass
class ExprValueCtx:
"""Indicates that the context is an expression literal, where the
"item" keyword may be used."""

View file

@ -18,11 +18,10 @@
# 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 .contexts import ScopeCtx, ValueTypeCtx
from .types import TypeName from .types import TypeName
from .gtkbuilder_template import Template
expr = Sequence() expr = Sequence()
@ -39,10 +38,6 @@ class ExprBase(AstNode):
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
raise NotImplementedError() raise NotImplementedError()
@property
def type_complete(self) -> bool:
return True
@property @property
def rhs(self) -> T.Optional["ExprBase"]: def rhs(self) -> T.Optional["ExprBase"]:
if isinstance(self.parent, Expression): if isinstance(self.parent, Expression):
@ -66,10 +61,6 @@ class Expression(ExprBase):
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
return self.last.type return self.last.type
@property
def type_complete(self) -> bool:
return self.last.type_complete
class InfixExpr(ExprBase): class InfixExpr(ExprBase):
@property @property
@ -90,6 +81,16 @@ class LiteralExpr(ExprBase):
or self.root.is_legacy_template(self.literal.value.ident) or self.root.is_legacy_template(self.literal.value.ident)
) )
@property
def is_this(self) -> bool:
from .values import IdentLiteral
return (
not self.is_object
and isinstance(self.literal.value, IdentLiteral)
and self.literal.value.ident == "item"
)
@property @property
def literal(self): def literal(self):
from .values import Literal from .values import Literal
@ -100,14 +101,14 @@ class LiteralExpr(ExprBase):
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
return self.literal.value.type return self.literal.value.type
@property @validate()
def type_complete(self) -> bool: def item_validations(self):
from .values import IdentLiteral if self.is_this:
if not isinstance(self.rhs, CastExpr):
raise CompileError('"item" must be cast to its object type')
if isinstance(self.literal.value, IdentLiteral): if not isinstance(self.rhs.rhs, LookupOp):
if object := self.context[ScopeCtx].objects.get(self.literal.value.ident): raise CompileError('"item" can only be used for looking up properties')
return not object.gir_class.incomplete
return True
class LookupOp(InfixExpr): class LookupOp(InfixExpr):
@ -115,7 +116,7 @@ class LookupOp(InfixExpr):
@context(ValueTypeCtx) @context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx: def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(None) return ValueTypeCtx(None, must_infer_type=True)
@property @property
def property_name(self) -> str: def property_name(self) -> str:
@ -131,9 +132,24 @@ class LookupOp(InfixExpr):
return None 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") @validate("property")
def property_exists(self): def property_exists(self):
if self.lhs.type is None: 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( raise CompileError(
f"Could not determine the type of the preceding expression", f"Could not determine the type of the preceding expression",
hints=[ hints=[
@ -157,10 +173,28 @@ class LookupOp(InfixExpr):
did_you_mean=(self.property_name, self.lhs.type.properties.keys()), 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): class CastExpr(InfixExpr):
grammar = [ grammar = [
"as", Keyword("as"),
AnyOf( AnyOf(
["<", TypeName, Match(">").expected()], ["<", TypeName, Match(">").expected()],
[ [
@ -179,10 +213,6 @@ class CastExpr(InfixExpr):
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
return self.children[TypeName][0].gir_type return self.children[TypeName][0].gir_type
@property
def type_complete(self) -> bool:
return True
@validate() @validate()
def cast_makes_sense(self): def cast_makes_sense(self):
if self.type is None or self.lhs.type is None: if self.type is None or self.lhs.type is None:
@ -206,6 +236,10 @@ class CastExpr(InfixExpr):
], ],
) )
@docs("as")
def ref_docs(self):
return get_docs_section("Syntax CastExpression")
class ClosureArg(AstNode): class ClosureArg(AstNode):
grammar = Expression grammar = Expression
@ -255,8 +289,96 @@ class ClosureExpr(ExprBase):
if not self.tokens["extern"]: if not self.tokens["extern"]:
raise CompileError(f"{self.closure_name} is not a builtin function") 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 = [
AnyOf(ClosureExpr, LiteralExpr, ["(", Expression, ")"]), AnyOf(ClosureExpr, LiteralExpr, ["(", Expression, ")"]),
ZeroOrMore(AnyOf(LookupOp, CastExpr)), ZeroOrMore(AnyOf(LookupOp, CastExpr)),
] ]
@decompiler("lookup", skip_children=True, cdata=True)
def decompile_lookup(
ctx: DecompileCtx,
gir: gir.GirContext,
cdata: str,
name: str,
type: T.Optional[str] = None,
):
if ctx.parent_node is not None and ctx.parent_node.tag == "property":
ctx.print("expr ")
if type is None:
type = ""
elif t := ctx.type_by_cname(type):
type = decompile.full_name(t)
else:
type = "$" + type
assert ctx.current_node is not None
constant = None
if len(ctx.current_node.children) == 0:
constant = cdata
elif (
len(ctx.current_node.children) == 1
and ctx.current_node.children[0].tag == "constant"
):
constant = ctx.current_node.children[0].cdata
if constant is not None:
if constant == ctx.template_class:
ctx.print("template." + name)
elif constant == "":
ctx.print(f"item as <{type}>.{name}")
else:
ctx.print(constant + "." + name)
return
else:
for child in ctx.current_node.children:
decompile.decompile_element(ctx, gir, child)
ctx.print(f" as <{type}>.{name}")
@decompiler("constant", cdata=True)
def decompile_constant(
ctx: DecompileCtx, gir: gir.GirContext, cdata: str, type: T.Optional[str] = None
):
if ctx.parent_node is not None and ctx.parent_node.tag == "property":
ctx.print("expr ")
if type is None:
if cdata == ctx.template_class:
ctx.print("template")
else:
ctx.print(cdata)
else:
_, string = ctx.decompile_value(cdata, ctx.type_by_cname(type))
ctx.print(string)
@decompiler("closure", skip_children=True)
def decompile_closure(ctx: DecompileCtx, gir: gir.GirContext, function: str, type: str):
if ctx.parent_node is not None and ctx.parent_node.tag == "property":
ctx.print("expr ")
if t := ctx.type_by_cname(type):
type = decompile.full_name(t)
else:
type = "$" + type
ctx.print(f"${function}(")
assert ctx.current_node is not None
for i, node in enumerate(ctx.current_node.children):
decompile_element(ctx, gir, node)
assert ctx.current_node is not None
if i < len(ctx.current_node.children) - 1:
ctx.print(", ")
ctx.end_block_with(f") as <{type}>")

View file

@ -21,12 +21,25 @@
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 ExtResponse from .response_id import ExtResponse
from .types import ClassName, ConcreteClassName from .types import ClassName, ConcreteClassName
RESERVED_IDS = {
RESERVED_IDS = {"this", "self", "template", "true", "false", "null", "none"} "this",
"self",
"template",
"true",
"false",
"null",
"none",
"item",
"expr",
"typeof",
}
class ObjectContent(AstNode): class ObjectContent(AstNode):
@ -56,6 +69,25 @@ class Object(AstNode):
def content(self) -> ObjectContent: def content(self) -> ObjectContent:
return self.children[ObjectContent][0] return self.children[ObjectContent][0]
@property
def signature(self) -> str:
if self.id:
return f"{self.class_name.gir_type.full_name} {self.id}"
elif t := self.class_name.gir_type:
return f"{t.full_name}"
else:
return f"{self.class_name.as_string}"
@property
def document_symbol(self) -> T.Optional[DocumentSymbol]:
return DocumentSymbol(
self.class_name.as_string,
SymbolKind.Object,
self.range,
self.children[ClassName][0].range,
self.id,
)
@property @property
def gir_class(self) -> GirType: def gir_class(self) -> GirType:
if self.class_name is None: if self.class_name is None:
@ -89,12 +121,12 @@ def validate_parent_type(node, ns: str, name: str, err_msg: str):
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( raise CompileError(
f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}" f"{container_type.full_name} is not a {ns}.{name}, so it doesn't have {err_msg}"
) )
@decompiler("object") @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 = ( klass_name = (
decompile.full_name(gir_class) if gir_class is not None else "$" + klass decompile.full_name(gir_class) if gir_class is not None else "$" + klass
@ -103,4 +135,5 @@ def decompile_object(ctx, gir, klass, id=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,17 +18,15 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .gtkbuilder_template import Template from .binding import Binding
from .values import Value, ObjectValue
from .common import * from .common import *
from .contexts import ValueTypeCtx from .contexts import ValueTypeCtx
from .property_binding import PropertyBinding from .values import ArrayValue, ExprValue, ObjectValue, Value
from .binding import Binding
class Property(AstNode): class Property(AstNode):
grammar = Statement( grammar = Statement(
UseIdent("name"), ":", AnyOf(PropertyBinding, Binding, ObjectValue, Value) UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue)
) )
@property @property
@ -36,7 +34,7 @@ class Property(AstNode):
return self.tokens["name"] return self.tokens["name"]
@property @property
def value(self) -> T.Union[PropertyBinding, Binding, ObjectValue, Value]: def value(self) -> T.Union[Binding, ExprValue, ObjectValue, Value, ArrayValue]:
return self.children[0] return self.children[0]
@property @property
@ -44,14 +42,31 @@ class Property(AstNode):
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 and not isinstance(self.gir_class, ExternType): 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
def document_symbol(self) -> DocumentSymbol:
if isinstance(self.value, ObjectValue) or self.value is None:
detail = None
else:
detail = self.value.range.text
return DocumentSymbol(
self.name,
SymbolKind.Property,
self.range,
self.group.tokens["name"].range,
detail,
)
@validate() @validate()
def binding_valid(self): def binding_valid(self):
if ( if (
(isinstance(self.value, PropertyBinding) or isinstance(self.value, Binding)) isinstance(self.value, Binding)
and self.gir_property is not None and self.gir_property is not None
and self.gir_property.construct_only and self.gir_property.construct_only
): ):
@ -94,6 +109,17 @@ class Property(AstNode):
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):
if self.gir_property is not None: if self.gir_property is not None:

View file

@ -19,9 +19,52 @@
import typing as T import typing as T
from .gtkbuilder_template import Template
from .contexts import ScopeCtx
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):
@ -33,18 +76,15 @@ class Signal(AstNode):
UseIdent("detail_name").expected("a signal detail name"), UseIdent("detail_name").expected("a signal detail name"),
] ]
), ),
"=>", Keyword("=>"),
Mark("detail_start"),
Optional(["$", UseLiteral("extern", True)]), 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( ZeroOrMore(SignalFlag),
AnyOf( Mark("detail_end"),
[Keyword("swapped"), UseLiteral("swapped", True)],
[Keyword("after"), UseLiteral("after", True)],
)
),
) )
@property @property
@ -55,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"]
@ -64,22 +111,57 @@ 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 and not isinstance(self.gir_class, ExternType): 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") @validate("handler")
def old_extern(self): def old_extern(self):
if not self.tokens["extern"]: if not self.tokens["extern"]:
@ -111,18 +193,57 @@ class Signal(AstNode):
if self.context[ScopeCtx].objects.get(object_id) is None: if self.context[ScopeCtx].objects.get(object_id) is None:
raise CompileError(f"Could not find object with ID '{object_id}'") raise CompileError(f"Could not find object with ID '{object_id}'")
@validate("name")
def deprecated(self) -> None:
if self.gir_signal is not None and self.gir_signal.deprecated:
hints = []
if self.gir_signal.deprecated_doc:
hints.append(self.gir_signal.deprecated_doc)
raise DeprecatedWarning(
f"{self.gir_signal.signature} is deprecated",
hints=hints,
)
@docs("name") @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,12 +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 BaseAttribute
from .values import Value
from .common import * from .common import *
from .contexts import ValueTypeCtx from .contexts import ValueTypeCtx
from ..decompiler import escape_quote from .gobject_object import ObjectContent, validate_parent_type
from .values import Value
def get_property_types(gir): def get_property_types(gir):
@ -97,7 +97,18 @@ def get_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)
@ -106,11 +117,11 @@ def _get_docs(gir, name):
return gir_type.doc return gir_type.doc
class A11yProperty(BaseAttribute): class A11yProperty(AstNode):
grammar = Statement( grammar = Statement(
UseIdent("name"), UseIdent("name"),
":", ":",
Value, AnyOf(Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]),
) )
@property @property
@ -131,13 +142,23 @@ class A11yProperty(BaseAttribute):
return self.tokens["name"].replace("_", "-") return self.tokens["name"].replace("_", "-")
@property @property
def value(self) -> Value: def values(self) -> T.List[Value]:
return self.children[0] return list(self.children)
@context(ValueTypeCtx) @context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx: def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(get_types(self.root.gir).get(self.tokens["name"])) 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):
types = get_types(self.root.gir) types = get_types(self.root.gir)
@ -154,6 +175,20 @@ class A11yProperty(BaseAttribute):
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):
@ -171,6 +206,15 @@ class ExtAccessibility(AstNode):
def properties(self) -> T.List[A11yProperty]: def properties(self) -> T.List[A11yProperty]:
return self.children[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")
@ -179,12 +223,16 @@ class ExtAccessibility(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, snippet="accessibility {\n $0\n}" "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}"
) )
@ -194,26 +242,46 @@ def a11y_completer(ast_node, match_variables):
applies_in=[ExtAccessibility], 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( yield Completion(
name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type) 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,9 +18,8 @@
# 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 ValueTypeCtx from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue from .values import StringValue
@ -31,13 +30,34 @@ class Item(AstNode):
] ]
@property @property
def name(self) -> str: def name(self) -> T.Optional[str]:
return self.tokens["name"] return self.tokens["name"]
@property @property
def value(self) -> StringValue: def value(self) -> StringValue:
return self.children[StringValue][0] return self.children[StringValue][0]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.value.range.text,
SymbolKind.String,
self.range,
self.value.range,
self.name,
)
@validate("name")
def unique_in_parent(self):
if self.name is not None:
self.validate_unique_in_parent(
f"Duplicate item '{self.name}'", lambda x: x.name == self.name
)
@docs("name")
def ref_docs(self):
return get_docs_section("Syntax ExtComboBoxItems")
class ExtComboBoxItems(AstNode): class ExtComboBoxItems(AstNode):
grammar = [ grammar = [
@ -47,6 +67,15 @@ class ExtComboBoxItems(AstNode):
"]", "]",
] ]
@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")
@ -55,11 +84,41 @@ class ExtComboBoxItems(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("items", CompletionItemKind.Snippet, snippet="items [$0]") yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]")
@decompiler("items", parent_type="Gtk.ComboBoxText")
def decompile_items(ctx: DecompileCtx, gir: gir.GirContext):
ctx.print("items [")
@decompiler("item", parent_type="Gtk.ComboBoxText", cdata=True)
def decompile_item(
ctx: DecompileCtx,
gir: gir.GirContext,
cdata: str,
id: T.Optional[str] = None,
translatable="false",
comments=None,
context=None,
):
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
)
if comments:
ctx.print(comments)
if id:
ctx.print(f"{id}: ")
ctx.print(translatable)
ctx.print(",")

View file

@ -18,27 +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 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 self.validate_unique_in_parent(
# the instance, hence wrapping it like this. f"Duplicate {self.tokens['tag_name']} block",
@validate(self.tokens["tag_name"]) check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"],
def wrapped_validator(self): )
self.validate_unique_in_parent(
f"Duplicate {self.tokens['tag_name']} block",
check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"],
)
wrapped_validator(self) @docs("tag_name")
def ref_docs(self):
return get_docs_section("Syntax ExtFileFilter")
class FilterString(AstNode): class FilterString(AstNode):
@ -46,13 +53,28 @@ class FilterString(AstNode):
def item(self) -> str: def item(self) -> str:
return self.tokens["name"] 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(
@ -79,7 +101,7 @@ ext_file_filter_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( yield Completion(
"mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]' "mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]'
) )
@ -94,7 +116,7 @@ def decompile_mime_types(ctx, gir):
@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")
@ -104,7 +126,7 @@ def decompile_patterns(ctx, gir):
@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")
@ -114,4 +136,4 @@ def decompile_suffixes(ctx, gir):
@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,9 +18,9 @@
# 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 ValueTypeCtx from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type
from .values import Value from .values import Value
@ -36,6 +36,16 @@ class LayoutProperty(AstNode):
def value(self) -> Value: def value(self) -> Value:
return self.children[Value][0] 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) @context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx: def value_type(self) -> ValueTypeCtx:
# there isn't really a way to validate these # there isn't really a way to validate these
@ -56,6 +66,15 @@ class ExtLayout(AstNode):
Until(LayoutProperty, "}"), Until(LayoutProperty, "}"),
) )
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"layout",
SymbolKind.Struct,
self.range,
self.group.tokens["layout"].range,
)
@validate("layout") @validate("layout")
def container_is_widget(self): def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "layout properties") validate_parent_type(self, "Gtk", "Widget", "layout properties")
@ -64,13 +83,17 @@ class ExtLayout(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("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}") yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}")

View file

@ -1,13 +1,40 @@
from .gobject_object import ObjectContent, validate_parent_type import typing as T
from ..parse_tree import Keyword
from blueprintcompiler.errors import T
from blueprintcompiler.lsp_utils import DocumentSymbol
from ..ast_utils import AstNode, validate from ..ast_utils import AstNode, validate
from .common import * from .common import *
from .types import TypeName
from .contexts import ScopeCtx from .contexts import ScopeCtx
from .gobject_object import ObjectContent, validate_parent_type
from .types import TypeName
class ExtListItemFactory(AstNode): class ExtListItemFactory(AstNode):
grammar = [UseExact("id", "template"), Optional(TypeName), ObjectContent] grammar = [
UseExact("id", "template"),
Mark("typename_start"),
Optional(TypeName),
Mark("typename_end"),
ObjectContent,
]
@property
def id(self) -> str:
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 @property
def type_name(self) -> T.Optional[TypeName]: def type_name(self) -> T.Optional[TypeName]:
@ -18,9 +45,12 @@ class ExtListItemFactory(AstNode):
@property @property
def gir_class(self): def gir_class(self):
return self.root.gir.get_type("ListItem", "Gtk") if self.type_name is not None:
return self.type_name.gir_type
else:
return self.root.gir.get_type("ListItem", "Gtk")
@validate("template") @validate("id")
def container_is_builder_list(self): def container_is_builder_list(self):
validate_parent_type( validate_parent_type(
self, self,
@ -29,13 +59,24 @@ class ExtListItemFactory(AstNode):
"sub-templates", "sub-templates",
) )
@validate() @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): def type_is_list_item(self):
if self.type_name is not None: if self.type_name is not None:
if self.type_name.glib_type_name != "GtkListItem": if self.type_name.glib_type_name not in (
raise CompileError(f"Only Gtk.ListItem is allowed as a type here") "GtkListItem",
"GtkListHeader",
"GtkColumnViewRow",
"GtkColumnViewCell",
):
raise CompileError(
f"Only Gtk.ListItem, Gtk.ListHeader, Gtk.ColumnViewRow, or Gtk.ColumnViewCell is allowed as a type here"
)
@validate("template") @validate("id")
def type_name_upgrade(self): def type_name_upgrade(self):
if self.type_name is None: if self.type_name is None:
raise UpgradeWarning( raise UpgradeWarning(
@ -62,8 +103,9 @@ class ExtListItemFactory(AstNode):
@property @property
def action_widgets(self): def action_widgets(self):
""" # The sub-template shouldn't have its own actions, this is just here to satisfy XmlOutput._emit_object_or_template
The sub-template shouldn't have it`s own actions this is
just hear to satisfy XmlOutput._emit_object_or_template
"""
return None return None
@docs("id")
def ref_docs(self):
return get_docs_section("Syntax ExtListItemFactory")

View file

@ -35,6 +35,23 @@ 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"]
@ -53,6 +70,25 @@ class Menu(AstNode):
if self.id in RESERVED_IDS: if self.id in RESERVED_IDS:
raise CompileWarning(f"{self.id} may be a confusing object ID") 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): class MenuAttribute(AstNode):
tag_name = "attribute" tag_name = "attribute"
@ -65,10 +101,30 @@ class MenuAttribute(AstNode):
def value(self) -> StringValue: def value(self) -> StringValue:
return self.children[StringValue][0] return self.children[StringValue][0]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.Field,
self.range,
(
self.group.tokens["name"].range
if self.group.tokens["name"]
else self.range
),
self.value.range.text,
)
@context(ValueTypeCtx) @context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx: def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(None) return ValueTypeCtx(None)
@validate("name")
def unique(self):
self.validate_unique_in_parent(
f"Duplicate attribute '{self.name}'", lambda x: x.name == self.name
)
menu_child = AnyOf() menu_child = AnyOf()
@ -85,7 +141,7 @@ menu_attribute = Group(
menu_section = Group( menu_section = Group(
Menu, Menu,
[ [
"section", Keyword("section"),
UseLiteral("tag", "section"), UseLiteral("tag", "section"),
Optional(UseIdent("id")), Optional(UseIdent("id")),
Match("{").expected(), Match("{").expected(),
@ -96,7 +152,7 @@ menu_section = Group(
menu_submenu = Group( menu_submenu = Group(
Menu, Menu,
[ [
"submenu", Keyword("submenu"),
UseLiteral("tag", "submenu"), UseLiteral("tag", "submenu"),
Optional(UseIdent("id")), Optional(UseIdent("id")),
Match("{").expected(), Match("{").expected(),
@ -107,7 +163,7 @@ menu_submenu = Group(
menu_item = Group( menu_item = Group(
Menu, Menu,
[ [
"item", Keyword("item"),
UseLiteral("tag", "item"), UseLiteral("tag", "item"),
Match("{").expected(), Match("{").expected(),
Until(menu_attribute, "}"), Until(menu_attribute, "}"),
@ -117,8 +173,9 @@ menu_item = Group(
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,
@ -186,7 +243,7 @@ from .ui import UI
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("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}") yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}")
@ -194,7 +251,7 @@ def menu_completer(ast_node, match_variables):
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, snippet="submenu {\n $0\n}" "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}"
) )
@ -229,7 +286,7 @@ def decompile_submenu(ctx, gir, id=None):
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} {{")

View file

@ -17,8 +17,8 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import validate_parent_type, ObjectContent
from .common import * from .common import *
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue from .values import StringValue
@ -58,6 +58,24 @@ class ExtScaleMark(AstNode):
else: else:
return None 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") @docs("position")
def position_docs(self) -> T.Optional[str]: def position_docs(self) -> T.Optional[str]:
if member := self.root.gir.get_type("PositionType", "Gtk").members.get( if member := self.root.gir.get_type("PositionType", "Gtk").members.get(
@ -76,6 +94,10 @@ class ExtScaleMark(AstNode):
did_you_mean=(self.position, positions.keys()), did_you_mean=(self.position, positions.keys()),
) )
@docs("mark")
def ref_docs(self):
return get_docs_section("Syntax ExtScaleMarks")
class ExtScaleMarks(AstNode): class ExtScaleMarks(AstNode):
grammar = [ grammar = [
@ -88,6 +110,15 @@ class ExtScaleMarks(AstNode):
def marks(self) -> T.List[ExtScaleMark]: def marks(self) -> T.List[ExtScaleMark]:
return self.children return self.children
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"marks",
SymbolKind.Array,
self.range,
self.group.tokens["marks"].range,
)
@validate("marks") @validate("marks")
def container_is_size_group(self): def container_is_size_group(self):
validate_parent_type(self, "Gtk", "Scale", "scale marks") validate_parent_type(self, "Gtk", "Scale", "scale marks")
@ -96,20 +127,24 @@ class ExtScaleMarks(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate 'marks' block") self.validate_unique_in_parent("Duplicate 'marks' block")
@docs("marks")
def ref_docs(self):
return get_docs_section("Syntax ExtScaleMarks")
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Scale"), applies_in_subclass=("Gtk", "Scale"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def complete_marks(ast_node, match_variables): def complete_marks(lsp, ast_node, match_variables):
yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]") yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]")
@completer( @completer(
applies_in=[ExtScaleMarks], applies_in=[ExtScaleMarks],
) )
def complete_mark(ast_node, match_variables): def complete_mark(lsp, ast_node, match_variables):
yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),") yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),")

View file

@ -18,14 +18,33 @@
# 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 .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.context[ScopeCtx].objects.get(self.tokens["name"]) object = self.context[ScopeCtx].objects.get(self.tokens["name"])
@ -43,6 +62,12 @@ class Widget(AstNode):
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 ExtSizeGroupWidgets(AstNode): class ExtSizeGroupWidgets(AstNode):
grammar = [ grammar = [
@ -52,6 +77,15 @@ class ExtSizeGroupWidgets(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")
@ -60,11 +94,25 @@ class ExtSizeGroupWidgets(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,9 +18,9 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .common import *
from .gobject_object import ObjectContent, validate_parent_type from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue from .values import StringValue
from .common import *
class Item(AstNode): class Item(AstNode):
@ -30,6 +30,15 @@ class Item(AstNode):
def child(self) -> StringValue: def child(self) -> StringValue:
return self.children[StringValue][0] return self.children[StringValue][0]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.child.range.text,
SymbolKind.String,
self.range,
self.range,
)
class ExtStringListStrings(AstNode): class ExtStringListStrings(AstNode):
grammar = [ grammar = [
@ -39,7 +48,16 @@ class ExtStringListStrings(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")
@ -47,11 +65,37 @@ class ExtStringListStrings(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("strings", CompletionItemKind.Snippet, snippet="strings [$0]") yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]")
@decompiler("items", parent_type="Gtk.StringList")
def decompile_strings(ctx: DecompileCtx, gir: gir.GirContext):
ctx.print("strings [")
@decompiler("item", cdata=True, parent_type="Gtk.StringList")
def decompile_item(
ctx: DecompileCtx,
gir: gir.GirContext,
translatable="false",
comments=None,
context=None,
cdata=None,
):
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
)
if comments is not None:
ctx.print(comments)
ctx.print(translatable + ",")

View file

@ -18,8 +18,8 @@
# 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):
@ -29,6 +29,21 @@ class StyleClass(AstNode):
def name(self) -> str: def name(self) -> str:
return self.tokens["name"] return self.tokens["name"]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.String,
self.range,
self.range,
)
@validate("name")
def unique_in_parent(self):
self.validate_unique_in_parent(
f"Duplicate style class '{self.name}'", lambda x: x.name == self.name
)
class ExtStyles(AstNode): class ExtStyles(AstNode):
grammar = [ grammar = [
@ -38,6 +53,15 @@ class ExtStyles(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")
@ -46,13 +70,17 @@ class ExtStyles(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"]')

View file

@ -20,9 +20,9 @@
from functools import cached_property from functools import cached_property
from .gobject_object import Object
from .response_id import ExtResponse
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"),
@ -53,6 +53,10 @@ class ChildExtension(AstNode):
def child(self) -> ExtResponse: def child(self) -> ExtResponse:
return self.children[0] return self.children[0]
@docs()
def ref_docs(self):
return get_docs_section("Syntax ChildExtension")
class ChildAnnotation(AstNode): class ChildAnnotation(AstNode):
grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"] grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"]
@ -88,7 +92,7 @@ class Child(AstNode):
hints = [ hints = [
"only Gio.ListStore or Gtk.Buildable implementors can have children" "only Gio.ListStore or Gtk.Buildable implementors can have children"
] ]
if "child" in gir_class.properties: if hasattr(gir_class, "properties") and "child" in gir_class.properties:
hints.append( hints.append(
"did you mean to assign this object to the 'child' property?" "did you mean to assign this object to the 'child' property?"
) )
@ -112,11 +116,30 @@ class Child(AstNode):
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

@ -21,9 +21,9 @@ import typing as T
from blueprintcompiler.language.common import GirType from blueprintcompiler.language.common import GirType
from .gobject_object import Object, ObjectContent
from .common import *
from ..gir import TemplateType from ..gir import TemplateType
from .common import *
from .gobject_object import Object, ObjectContent
from .types import ClassName, TemplateClassName from .types import ClassName, TemplateClassName
@ -44,6 +44,22 @@ class Template(Object):
def id(self) -> str: def id(self) -> str:
return "template" return "template"
@property
def signature(self) -> str:
if self.parent_type and self.parent_type.gir_type:
return f"template {self.class_name.as_string} : {self.parent_type.gir_type.full_name}"
else:
return f"template {self.class_name.as_string}"
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.signature,
SymbolKind.Object,
self.range,
self.group.tokens["id"].range,
)
@property @property
def gir_class(self) -> GirType: def gir_class(self) -> GirType:
if isinstance(self.class_name.gir_type, ExternType): if isinstance(self.class_name.gir_type, ExternType):
@ -72,6 +88,10 @@ class Template(Object):
f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}", 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=None): def decompile_template(ctx: DecompileCtx, gir, klass, parent=None):
@ -81,8 +101,9 @@ def decompile_template(ctx: DecompileCtx, gir, klass, parent=None):
else: else:
return "$" + cname return "$" + cname
ctx.print(f"template {class_name(klass)} : {class_name(parent)} {{") if parent is None:
ctx.print(f"template {class_name(klass)} {{")
ctx.template_class = klass else:
ctx.print(f"template {class_name(klass)} : {class_name(parent)} {{")
return ctx.type_by_cname(klass) or ctx.type_by_cname(parent) return ctx.type_by_cname(klass) or ctx.type_by_cname(parent)

View file

@ -59,14 +59,12 @@ class GtkDirective(AstNode):
@property @property
def gir_namespace(self): def gir_namespace(self):
# validate the GTK version first to make sure the more specific error # For better error handling, just assume it's 4.0
# message is emitted return gir.get_namespace("Gtk", "4.0")
self.gtk_version()
if self.tokens["version"] is not None: @docs()
return gir.get_namespace("Gtk", self.tokens["version"]) def ref_docs(self):
else: return get_docs_section("Syntax GtkDecl")
# For better error handling, just assume it's 4.0
return gir.get_namespace("Gtk", "4.0")
class Import(AstNode): class Import(AstNode):
@ -86,11 +84,26 @@ class Import(AstNode):
@validate("namespace", "version") @validate("namespace", "version")
def namespace_exists(self): def namespace_exists(self):
gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) gir.get_namespace(self.namespace, self.version)
@validate()
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

@ -1,139 +0,0 @@
# property_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 .common import *
from .contexts import ScopeCtx
from .gobject_object import Object
class PropertyBindingFlag(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'", "")],
)
class PropertyBinding(AstNode):
grammar = AnyOf(
[
Keyword("bind-property"),
UseIdent("source"),
".",
UseIdent("property"),
ZeroOrMore(PropertyBindingFlag),
],
[
Keyword("bind"),
UseIdent("source"),
".",
UseIdent("property"),
PropertyBindingFlag,
ZeroOrMore(PropertyBindingFlag),
],
)
@property
def source(self) -> str:
return self.tokens["source"]
@property
def source_obj(self) -> T.Optional[Object]:
if self.root.is_legacy_template(self.source):
return self.root.template
return self.context[ScopeCtx].objects.get(self.source)
@property
def property_name(self) -> str:
return self.tokens["property"]
@property
def flags(self) -> T.List[PropertyBindingFlag]:
return self.children[PropertyBindingFlag]
@property
def inverted(self) -> bool:
return any([f.flag == "inverted" for f in self.flags])
@property
def bidirectional(self) -> bool:
return any([f.flag == "bidirectional" for f in self.flags])
@property
def no_sync_create(self) -> bool:
return any([f.flag == "no-sync-create" for f in self.flags])
@validate("source")
def source_object_exists(self) -> None:
if self.source_obj is None:
raise CompileError(
f"Could not find object with ID {self.source}",
did_you_mean=(self.source, self.context[ScopeCtx].objects.keys()),
)
@validate("property")
def property_exists(self) -> None:
if self.source_obj is None:
return
gir_class = self.source_obj.gir_class
if gir_class is None or gir_class.incomplete:
# Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself
return
if (
isinstance(gir_class, gir.Class)
and gir_class.properties.get(self.property_name) is None
):
raise CompileError(
f"{gir_class.full_name} does not have a property called {self.property_name}"
)
@validate("bind")
def old_bind(self):
if self.tokens["bind"]:
raise UpgradeWarning(
"Use 'bind-property', introduced in blueprint 0.8.0, to use binding flags",
actions=[CodeAction("Use 'bind-property'", "bind-property")],
)
@validate("source")
def legacy_template(self):
if self.root.is_legacy_template(self.source):
raise UpgradeWarning(
"Use 'template' instead of the class name (introduced in 0.8.0)",
actions=[CodeAction("Use 'template'", "template")],
)

View file

@ -123,3 +123,42 @@ class ExtResponse(AstNode):
object = self.parent_by_type(Child).object object = self.parent_by_type(Child).object
return object.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,16 +17,19 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
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()
def ref_docs(self):
return get_docs_section("Syntax TranslationDomain")

View file

@ -18,8 +18,8 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .common import *
from ..gir import Class, ExternType, Interface from ..gir import Class, ExternType, Interface
from .common import *
class TypeName(AstNode): class TypeName(AstNode):
@ -55,12 +55,33 @@ class TypeName(AstNode):
@validate("namespace") @validate("namespace")
def gir_ns_exists(self): def gir_ns_exists(self):
if not self.tokens["extern"]: if not self.tokens["extern"]:
self.root.gir.validate_ns(self.tokens["namespace"]) try:
self.root.gir.validate_ns(self.tokens["namespace"])
except CompileError as e:
ns = self.tokens["namespace"]
e.actions = [
self.root.import_code_action(n, version)
for n, version in gir.get_available_namespaces()
if n == ns
]
raise e
@validate()
def deprecated(self) -> None:
if self.gir_type and self.gir_type.deprecated:
hints = []
if self.gir_type.deprecated_doc:
hints.append(self.gir_type.deprecated_doc)
raise DeprecatedWarning(
f"{self.gir_type.full_name} is deprecated",
hints=hints,
)
@property @property
def gir_ns(self): def gir_ns(self) -> T.Optional[gir.Namespace]:
if not self.tokens["extern"]: 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) -> gir.GirType: def gir_type(self) -> gir.GirType:

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, Menu
from .common import * from .common import *
from .contexts import ScopeCtx 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,6 +36,7 @@ class UI(AstNode):
grammar = [ grammar = [
GtkDirective, GtkDirective,
ZeroOrMore(Import), ZeroOrMore(Import),
Optional(TranslationDomain),
Until( Until(
AnyOf( AnyOf(
Template, Template,
@ -42,7 +47,7 @@ class UI(AstNode):
), ),
] ]
@property @cached_property
def gir(self) -> gir.GirContext: def gir(self) -> gir.GirContext:
gir_ctx = gir.GirContext() gir_ctx = gir.GirContext()
self._gir_errors = [] self._gir_errors = []
@ -60,8 +65,7 @@ class UI(AstNode):
else: else:
gir_ctx.not_found_namespaces.add(i.namespace) 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
@ -74,6 +78,14 @@ class UI(AstNode):
def gtk_decl(self) -> GtkDirective: def gtk_decl(self) -> GtkDirective:
return self.children[GtkDirective][0] 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 @property
def contents(self) -> T.List[T.Union[Object, Template, Menu]]: def contents(self) -> T.List[T.Union[Object, Template, Menu]]:
return [ return [
@ -98,6 +110,34 @@ class UI(AstNode):
and self.template.class_name.glib_type_name == id 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) @context(ScopeCtx)
def scope_ctx(self) -> ScopeCtx: def scope_ctx(self) -> ScopeCtx:
return ScopeCtx(node=self) return ScopeCtx(node=self)

View file

@ -19,10 +19,14 @@
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 .types import TypeName from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx
from .expression import Expression
from .gobject_object import Object from .gobject_object import Object
from .contexts import ScopeCtx, ValueTypeCtx from .types import TypeName
class Translated(AstNode): class Translated(AstNode):
@ -54,6 +58,23 @@ class Translated(AstNode):
f"Cannot convert translated string to {expected_type.full_name}" f"Cannot convert translated string to {expected_type.full_name}"
) )
@validate("context")
def context_double_quoted(self):
if self.translate_context is None:
return
if not str(self.group.tokens["context"]).startswith('"'):
raise CompileWarning("gettext may not recognize single-quoted strings")
@validate("string")
def string_double_quoted(self):
if not str(self.group.tokens["string"]).startswith('"'):
raise CompileWarning("gettext may not recognize single-quoted strings")
@docs()
def ref_docs(self):
return get_docs_section("Syntax Translated")
class TypeLiteral(AstNode): class TypeLiteral(AstNode):
grammar = [ grammar = [
@ -99,6 +120,10 @@ class TypeLiteral(AstNode):
], ],
) )
@docs()
def ref_docs(self):
return get_docs_section("Syntax TypeLiteral")
class QuotedLiteral(AstNode): class QuotedLiteral(AstNode):
grammar = UseQuoted("value") grammar = UseQuoted("value")
@ -200,15 +225,22 @@ class Flag(AstNode):
return self.tokens["value"] return self.tokens["value"]
@property @property
def value(self) -> T.Optional[int]: def value(self) -> T.Optional[str]:
type = self.context[ValueTypeCtx].value_type type = self.context[ValueTypeCtx].value_type
if not isinstance(type, Enumeration): if not isinstance(type, Enumeration):
return None return None
elif member := type.members.get(self.name): elif member := type.members.get(self.name):
return member.value return member.nick
else: else:
return None 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.context[ValueTypeCtx].value_type type = self.context[ValueTypeCtx].value_type
@ -229,6 +261,12 @@ class Flag(AstNode):
did_you_mean=(self.tokens["value"], expected_type.members.keys()), did_you_mean=(self.tokens["value"], expected_type.members.keys()),
) )
@validate()
def unique(self):
self.validate_unique_in_parent(
f"Duplicate flag '{self.name}'", lambda x: x.name == self.name
)
class Flags(AstNode): class Flags(AstNode):
grammar = [Flag, "|", Flag, ZeroOrMore(["|", Flag])] grammar = [Flag, "|", Flag, ZeroOrMore(["|", Flag])]
@ -243,6 +281,10 @@ class Flags(AstNode):
if expected_type is not None and not isinstance(expected_type, gir.Bitfield): if expected_type is not None and not isinstance(expected_type, gir.Bitfield):
raise CompileError(f"{expected_type.full_name} is not a bitfield type") raise CompileError(f"{expected_type.full_name} is not a bitfield type")
@docs()
def ref_docs(self):
return get_docs_section("Syntax Flags")
class IdentLiteral(AstNode): class IdentLiteral(AstNode):
grammar = UseIdent("value") grammar = UseIdent("value")
@ -285,13 +327,18 @@ class IdentLiteral(AstNode):
actions=[CodeAction("Use 'template'", "template")], actions=[CodeAction("Use 'template'", "template")],
) )
elif expected_type is not None: elif expected_type is not None or self.context[ValueTypeCtx].must_infer_type:
object = self.context[ScopeCtx].objects.get(self.ident) object = self.context[ScopeCtx].objects.get(self.ident)
if object is None: if object is None:
if self.ident == "null": if self.ident == "null":
if not self.context[ValueTypeCtx].allow_null: if not self.context[ValueTypeCtx].allow_null:
raise CompileError("null is not permitted here") raise CompileError("null is not permitted here")
else: elif self.ident == "item":
if not self.context[ExprValueCtx]:
raise CompileError(
'"item" can only be used in an expression literal'
)
elif self.ident not in ["true", "false"]:
raise CompileError( raise CompileError(
f"Could not find object with ID {self.ident}", f"Could not find object with ID {self.ident}",
did_you_mean=( did_you_mean=(
@ -299,21 +346,31 @@ class IdentLiteral(AstNode):
self.context[ScopeCtx].objects.keys(), self.context[ScopeCtx].objects.keys(),
), ),
) )
elif object.gir_class and not object.gir_class.assignable_to(expected_type): elif (
expected_type is not None
and object.gir_class is not None
and not object.gir_class.assignable_to(expected_type)
):
raise CompileError( raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {expected_type.full_name}" f"Cannot assign {object.gir_class.full_name} to {expected_type.full_name}"
) )
@docs() @docs()
def docs(self) -> T.Optional[str]: def docs(self) -> T.Optional[str]:
type = self.context[ValueTypeCtx].value_type expected_type = self.context[ValueTypeCtx].value_type
if isinstance(type, gir.Enumeration): if isinstance(expected_type, gir.BoolType):
if member := type.members.get(self.ident): 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: else:
return None return None
@ -323,6 +380,16 @@ class IdentLiteral(AstNode):
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): class Literal(AstNode):
grammar = AnyOf( grammar = AnyOf(
@ -359,6 +426,35 @@ class ObjectValue(AstNode):
) )
class ExprValue(AstNode):
grammar = [Keyword("expr"), Expression]
@property
def expression(self) -> Expression:
return self.children[Expression][0]
@validate("expr")
def validate_for_type(self) -> None:
expected_type = self.parent.context[ValueTypeCtx].value_type
expr_type = self.root.gir.get_type("Expression", "Gtk")
if expected_type is not None and not expected_type.assignable_to(expr_type):
raise CompileError(
f"Cannot convert Gtk.Expression to {expected_type.full_name}"
)
@docs("expr")
def ref_docs(self):
return get_docs_section("Syntax ExprValue")
@context(ExprValueCtx)
def expr_literal(self):
return ExprValueCtx()
@context(ValueTypeCtx)
def value_type(self):
return ValueTypeCtx(None, must_infer_type=True)
class Value(AstNode): class Value(AstNode):
grammar = AnyOf(Translated, Flags, Literal) grammar = AnyOf(Translated, Flags, Literal)
@ -369,6 +465,68 @@ class Value(AstNode):
return self.children[0] 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): class StringValue(AstNode):
grammar = AnyOf(Translated, QuotedLiteral) grammar = AnyOf(Translated, QuotedLiteral)

View file

@ -18,14 +18,19 @@
# 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 .outputs.xml import XmlOutput from .outputs.xml import XmlOutput
from . import tokenizer, parser, utils, xml_reader, decompiler from .tokenizer import Token
def printerr(*args, **kwargs): def printerr(*args, **kwargs):
@ -41,16 +46,16 @@ def command(json_method: str):
class OpenFile: class OpenFile:
def __init__(self, uri: str, text: str, version: int): 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:
if "range" not in change: if "range" not in change:
self.text = change["text"] self.text = change["text"]
@ -68,8 +73,8 @@ class OpenFile:
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)
@ -97,10 +102,10 @@ class OpenFile:
] ]
# 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]
@ -111,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
@ -119,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"):
@ -143,7 +150,7 @@ class LanguageServer:
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( sys.stdout.write(
f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}" f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}"
@ -183,6 +190,9 @@ class LanguageServer:
from . import main from . import main
self.client_capabilities = params.get("capabilities", {}) self.client_capabilities = params.get("capabilities", {})
self.client_supports_completion_choice = params.get("clientInfo", {}).get(
"name"
) in ["Visual Studio Code", "VSCodium"]
self._send_response( self._send_response(
id, id,
{ {
@ -194,12 +204,16 @@ 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",
@ -208,6 +222,14 @@ class LanguageServer:
}, },
) )
@command("shutdown")
def shutdown(self, id, params):
self._send_response(id, None)
@command("exit")
def exit(self, id, params):
self._exited = True
@command("textDocument/didOpen") @command("textDocument/didOpen")
def didOpen(self, id, params): def didOpen(self, id, params):
doc = params.get("textDocument") doc = params.get("textDocument")
@ -264,11 +286,43 @@ class LanguageServer:
idx = utils.pos_to_idx( idx = utils.pos_to_idx(
params["position"]["line"], params["position"]["character"], open_file.text params["position"]["line"], params["position"]["character"], open_file.text
) )
completions = complete(open_file.ast, open_file.tokens, idx) completions = complete(self, open_file.ast, open_file.tokens, idx)
self._send_response( self._send_response(
id, [completion.to_json(True) for completion in completions] 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") @command("textDocument/x-blueprint-compile")
def compile(self, id, params): def compile(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]] open_file = self._open_files[params["textDocument"]["uri"]]
@ -280,7 +334,7 @@ class LanguageServer:
xml = None xml = None
try: try:
output = XmlOutput() output = XmlOutput()
xml = output.emit(open_file.ast) xml = output.emit(open_file.ast, indent=2, generated_notice=False)
except: except:
printerr(traceback.format_exc()) printerr(traceback.format_exc())
self._send_error(id, ErrorCode.RequestFailed, "Could not compile document") self._send_error(id, ErrorCode.RequestFailed, "Could not compile document")
@ -291,16 +345,19 @@ class LanguageServer:
def decompile(self, id, params): def decompile(self, id, params):
text = params.get("text") text = params.get("text")
blp = None blp = None
if text.strip() == "":
try: blp = ""
blp = decompiler.decompile_string(text) printerr("Decompiled to empty blueprint because input was empty")
except decompiler.UnsupportedError as e: else:
self._send_error(id, ErrorCode.RequestFailed, e.message) try:
return blp = decompiler.decompile_string(text)
except: except decompiler.UnsupportedError as e:
printerr(traceback.format_exc()) self._send_error(id, ErrorCode.RequestFailed, e.message)
self._send_error(id, ErrorCode.RequestFailed, "Invalid input") return
return except:
printerr(traceback.format_exc())
self._send_error(id, ErrorCode.RequestFailed, "Invalid input")
return
self._send_response(id, {"blp": blp}) self._send_response(id, {"blp": blp})
@ -319,14 +376,17 @@ class LanguageServer:
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( range = Range(
params["range"]["start"]["line"], utils.pos_to_idx(
params["range"]["start"]["character"], params["range"]["start"]["line"],
open_file.text, params["range"]["start"]["character"],
) open_file.text,
range_end = utils.pos_to_idx( ),
params["range"]["end"]["line"], utils.pos_to_idx(
params["range"]["end"]["character"], params["range"]["end"]["line"],
params["range"]["end"]["character"],
open_file.text,
),
open_file.text, open_file.text,
) )
@ -334,15 +394,15 @@ class LanguageServer:
{ {
"title": action.title, "title": action.title,
"kind": "quickfix", "kind": "quickfix",
"diagnostics": [ "diagnostics": [self._create_diagnostic(open_file.uri, diagnostic)],
self._create_diagnostic(open_file.text, open_file.uri, diagnostic)
],
"edit": { "edit": {
"changes": { "changes": {
open_file.uri: [ open_file.uri: [
{ {
"range": utils.idxs_to_range( "range": (
diagnostic.start, diagnostic.end, open_file.text action.edit_range.to_json()
if action.edit_range
else diagnostic.range.to_json()
), ),
"newText": action.replace_with, "newText": action.replace_with,
} }
@ -351,46 +411,88 @@ class LanguageServer:
}, },
} }
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( self._send_notification(
"textDocument/publishDiagnostics", "textDocument/publishDiagnostics",
{ {
"uri": open_file.uri, "uri": open_file.uri,
"diagnostics": [ "diagnostics": [
self._create_diagnostic(open_file.text, open_file.uri, err) self._create_diagnostic(open_file.uri, err)
for err in open_file.diagnostics for err in open_file.diagnostics
], ],
}, },
) )
def _create_diagnostic(self, text: str, uri: str, err: CompileError): def _create_diagnostic(self, uri: str, err: CompileError):
message = err.message message = err.message
assert err.start is not None and err.end is not None 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 "severity": (
if isinstance(err, CompileWarning) DiagnosticSeverity.Warning
else DiagnosticSeverity.Error, 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,
} }

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 *
@ -79,9 +82,11 @@ class Completion:
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
@ -94,16 +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),
"kind": "markdown", "documentation": (
"value": self.docs, {
} "kind": "markdown",
if self.docs "value": 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}
@ -119,8 +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,15 +18,19 @@
# 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, CompilerBugError 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 .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
@ -63,6 +67,52 @@ class BlueprintApp:
type=argparse.FileType("r"), 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 = self.add_subcommand(
@ -121,9 +171,11 @@ class BlueprintApp:
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( print(
f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}" f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}"
) )
@ -147,6 +199,135 @@ 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()
@ -154,7 +335,7 @@ class BlueprintApp:
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[PrintableError]]: def _compile(self, data: str) -> T.Tuple[str, T.List[CompileError]]:
tokens = tokenizer.tokenize(data) tokens = tokenizer.tokenize(data)
ast, errors, warnings = parser.parse(tokens) ast, errors, warnings = parser.parse(tokens)

View file

@ -1,18 +1,21 @@
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):
xml.start_tag("interface") if domain := ui.translation_domain:
xml.start_tag("interface", domain=domain.domain)
else:
xml.start_tag("interface")
self._emit_gtk_directive(ui.gtk_decl, xml) self._emit_gtk_directive(ui.gtk_decl, xml)
@ -114,34 +117,42 @@ class XmlOutput(OutputFormat):
elif isinstance(value, Binding): elif isinstance(value, Binding):
if simple := value.simple_binding: if simple := value.simple_binding:
props["bind-source"] = simple.source props["bind-source"] = self._object_id(value, simple.source)
props["bind-property"] = simple.property_name props["bind-property"] = simple.property_name
props["bind-flags"] = "sync-create" 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) xml.put_self_closing("property", **props)
else: else:
xml.start_tag("binding", **props) xml.start_tag("binding", **props)
self._emit_expression(value.expression, xml) self._emit_expression(value.expression, xml)
xml.end_tag() xml.end_tag()
elif isinstance(value, PropertyBinding): elif isinstance(value, ExprValue):
bind_flags = [] xml.start_tag("property", **props)
if not value.no_sync_create: self._emit_expression(value.expression, xml)
bind_flags.append("sync-create") xml.end_tag()
if value.inverted:
bind_flags.append("invert-boolean")
if value.bidirectional:
bind_flags.append("bidirectional")
props["bind-source"] = value.source
props["bind-property"] = value.property_name
props["bind-flags"] = "|".join(bind_flags) or None
xml.put_self_closing("property", **props)
elif isinstance(value, ObjectValue): elif isinstance(value, ObjectValue):
xml.start_tag("property", **props) xml.start_tag("property", **props)
self._emit_object(value.object, xml) self._emit_object(value.object, xml)
xml.end_tag() 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: else:
raise CompilerBugError() raise CompilerBugError()
@ -153,7 +164,7 @@ class XmlOutput(OutputFormat):
elif isinstance(translated, QuotedLiteral): elif isinstance(translated, QuotedLiteral):
return {} return {}
else: else:
return {"translatable": "true", "context": translated.translate_context} 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
@ -163,8 +174,11 @@ 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):
@ -192,17 +206,15 @@ class XmlOutput(OutputFormat):
xml.put_text(value.ident) xml.put_text(value.ident)
elif isinstance(value_type, gir.Enumeration): elif isinstance(value_type, gir.Enumeration):
xml.put_text(str(value_type.members[value.ident].value)) xml.put_text(str(value_type.members[value.ident].value))
elif (
value.ident == "template"
and value.context[ScopeCtx].template is not None
):
xml.put_text(value.context[ScopeCtx].template.gir_class.glib_type_name)
else: else:
xml.put_text(value.ident) xml.put_text(self._object_id(value, value.ident))
elif isinstance(value, TypeLiteral): elif isinstance(value, TypeLiteral):
xml.put_text(value.type_name.glib_type_name) xml.put_text(value.type_name.glib_type_name)
else: else:
xml.put_text(value.value) if isinstance(value.value, float) and value.value == int(value.value):
xml.put_text(int(value.value))
else:
xml.put_text(value.value)
def _emit_value(self, value: Value, xml: XmlEmitter): def _emit_value(self, value: Value, xml: XmlEmitter):
if isinstance(value.child, Literal): if isinstance(value.child, Literal):
@ -211,12 +223,6 @@ class XmlOutput(OutputFormat):
xml.put_text( xml.put_text(
"|".join([str(flag.value or flag.name) for flag in value.child.flags]) "|".join([str(flag.value or flag.name) for flag in value.child.flags])
) )
elif isinstance(value.child, Translated):
raise CompilerBugError("translated values must be handled in the parent")
elif isinstance(value.child, TypeLiteral):
xml.put_text(value.child.type_name.glib_type_name)
elif isinstance(value.child, ObjectValue):
self._emit_object(value.child.object, xml)
else: else:
raise CompilerBugError() raise CompilerBugError()
@ -238,6 +244,9 @@ class XmlOutput(OutputFormat):
raise CompilerBugError() raise CompilerBugError()
def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter): def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter):
if expr.is_this:
return
if expr.is_object: if expr.is_object:
xml.start_tag("constant") xml.start_tag("constant")
else: else:
@ -285,8 +294,11 @@ class XmlOutput(OutputFormat):
def _emit_extensions(self, extension, xml: XmlEmitter): def _emit_extensions(self, extension, xml: XmlEmitter):
if isinstance(extension, ExtAccessibility): if isinstance(extension, ExtAccessibility):
xml.start_tag("accessibility") xml.start_tag("accessibility")
for prop in extension.properties: for property in extension.properties:
self._emit_attribute(prop.tag_name, "name", prop.name, prop.value, xml) for val in property.values:
self._emit_attribute(
property.tag_name, "name", property.name, val, xml
)
xml.end_tag() xml.end_tag()
elif isinstance(extension, AdwBreakpointCondition): elif isinstance(extension, AdwBreakpointCondition):
@ -296,6 +308,9 @@ class XmlOutput(OutputFormat):
elif isinstance(extension, AdwBreakpointSetters): elif isinstance(extension, AdwBreakpointSetters):
for setter in extension.setters: for setter in extension.setters:
if setter.value is None:
continue
attrs = {} attrs = {}
if isinstance(setter.value.child, Translated): if isinstance(setter.value.child, Translated):
@ -303,7 +318,7 @@ class XmlOutput(OutputFormat):
xml.start_tag( xml.start_tag(
"setter", "setter",
object=setter.object_id, object=self._object_id(setter, setter.object_id),
property=setter.property_name, property=setter.property_name,
**attrs, **attrs,
) )
@ -340,7 +355,7 @@ class XmlOutput(OutputFormat):
self._emit_attribute("property", "name", prop.name, prop.value, xml) self._emit_attribute("property", "name", prop.name, prop.value, xml)
xml.end_tag() xml.end_tag()
elif isinstance(extension, ExtAdwMessageDialog): elif isinstance(extension, ExtAdwResponseDialog):
xml.start_tag("responses") xml.start_tag("responses")
for response in extension.responses: for response in extension.responses:
xml.start_tag( xml.start_tag(
@ -356,12 +371,13 @@ class XmlOutput(OutputFormat):
elif isinstance(extension, ExtScaleMarks): elif isinstance(extension, ExtScaleMarks):
xml.start_tag("marks") xml.start_tag("marks")
for mark in extension.children: for mark in extension.marks:
label = mark.label.child if mark.label is not None else None
xml.start_tag( xml.start_tag(
"mark", "mark",
value=mark.value, value=mark.value,
position=mark.position, position=mark.position,
**self._translated_string_attrs(mark.label and mark.label.child), **self._translated_string_attrs(label),
) )
if mark.label is not None: if mark.label is not None:
xml.put_text(mark.label.string) xml.put_text(mark.label.string)
@ -376,10 +392,11 @@ class XmlOutput(OutputFormat):
xml.put_text(value.string) xml.put_text(value.string)
xml.end_tag() xml.end_tag()
xml.end_tag() xml.end_tag()
elif isinstance(extension, ExtListItemFactory): elif isinstance(extension, ExtListItemFactory):
child_xml = XmlEmitter() child_xml = XmlEmitter(generated_notice=False)
child_xml.start_tag("interface") child_xml.start_tag("interface")
child_xml.start_tag("template", **{"class": "GtkListItem"}) child_xml.start_tag("template", **{"class": extension.gir_class})
self._emit_object_or_template(extension, child_xml) self._emit_object_or_template(extension, child_xml)
child_xml.end_tag() child_xml.end_tag()
child_xml.end_tag() child_xml.end_tag()
@ -396,8 +413,14 @@ class XmlOutput(OutputFormat):
elif isinstance(extension, ExtSizeGroupWidgets): elif isinstance(extension, ExtSizeGroupWidgets):
xml.start_tag("widgets") xml.start_tag("widgets")
for prop in extension.children: for prop in extension.children:
xml.put_self_closing("widget", name=prop.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():
@ -63,6 +73,7 @@ class XmlEmitter:
self._needs_newline = False self._needs_newline = False
def put_cdata(self, text: str): def put_cdata(self, text: str):
text = text.replace("]]>", "]]]]><![CDATA[>")
self.result += f"<![CDATA[{text}]]>" self.result += f"<![CDATA[{text}]]>"
self._needs_newline = False self._needs_newline = False

View file

@ -17,23 +17,21 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
""" Utilities for parsing an AST from a token stream. """ """Utilities for parsing an AST from a token stream."""
import typing as T import typing as T
from collections import defaultdict
from enum import Enum from enum import Enum
from .ast_utils import AstNode
from . import utils
from .ast_utils import AstNode
from .errors import ( from .errors import (
assert_true,
CompilerBugError,
CompileError, CompileError,
CompilerBugError,
CompileWarning, CompileWarning,
UnexpectedTokenError, UnexpectedTokenError,
assert_true,
) )
from .tokenizer import Token, TokenType from .tokenizer import Range, Token, TokenType
SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE] SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE]
@ -65,14 +63,16 @@ 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: T.Type[AstNode], 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, T.Optional[Token]] = {} self.tokens: T.Dict[str, T.Optional[Token]] = {}
self.ranges: T.Dict[str, Range] = {}
self.start = start self.start = start
self.end: T.Optional[int] = None self.end: T.Optional[int] = None
self.incomplete = False self.incomplete = False
self.text = text
def add_child(self, child: "ParseGroup"): def add_child(self, child: "ParseGroup"):
self.children.append(child) self.children.append(child)
@ -82,6 +82,12 @@ class ParseGroup:
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."""
@ -89,25 +95,18 @@ class ParseGroup:
try: try:
return self.ast_type(self, children, self.keys, incomplete=self.incomplete) return self.ast_type(self, children, self.keys, incomplete=self.incomplete)
except TypeError as e: except TypeError: # pragma: no cover
raise CompilerBugError( raise CompilerBugError(
f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace." f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace."
) )
def __str__(self):
result = str(self.ast_type.__name__)
result += "".join([f"\n{key}: {val}" for key, val in self.keys.items()]) + "\n"
result += "\n".join(
[str(child) for children in self.children.values() for child in children]
)
return result.replace("\n", "\n ")
class ParseContext: class ParseContext:
"""Contains the state of the parser.""" """Contains the state of the parser."""
def __init__(self, tokens: T.List[Token], index=0): def __init__(self, tokens: T.List[Token], text: str, index=0):
self.tokens = tokens self.tokens = tokens
self.text = text
self.binding_power = 0 self.binding_power = 0
self.index = index self.index = index
@ -115,6 +114,7 @@ class ParseContext:
self.group: T.Optional[ParseGroup] = None self.group: T.Optional[ParseGroup] = None
self.group_keys: T.Dict[str, T.Tuple[T.Any, T.Optional[Token]]] = {} self.group_keys: T.Dict[str, T.Tuple[T.Any, T.Optional[Token]]] = {}
self.group_children: T.List[ParseGroup] = [] self.group_children: T.List[ParseGroup] = []
self.group_ranges: T.Dict[str, Range] = {}
self.last_group: T.Optional[ParseGroup] = None self.last_group: T.Optional[ParseGroup] = None
self.group_incomplete = False self.group_incomplete = False
@ -126,7 +126,7 @@ class ParseContext:
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
@ -142,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)
@ -150,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
@ -163,13 +166,19 @@ class ParseContext:
def start_group(self, ast_type: T.Type[AstNode]): def start_group(self, ast_type: T.Type[AstNode]):
"""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: str, value: T.Any, token: T.Optional[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)."""
@ -208,11 +217,11 @@ class ParseContext:
if ( if (
len(self.errors) 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) -> bool: 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
@ -248,10 +257,6 @@ class ParseNode:
"""Convenience method for err().""" """Convenience method for err()."""
return self.err("Expected " + expect) return self.err("Expected " + expect)
def warn(self, message) -> "Warning":
"""Causes this ParseNode to emit a warning if it parses successfully."""
return Warning(self, message)
class Err(ParseNode): class Err(ParseNode):
"""ParseNode that emits a compile error if it fails to parse.""" """ParseNode that emits a compile error if it fails to parse."""
@ -265,32 +270,12 @@ class Err(ParseNode):
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: str):
self.child = to_parse_node(child)
self.message = message
def _parse(self, ctx: ParseContext):
ctx.skip()
start_idx = ctx.index
if self.child.parse(ctx).succeeded():
start_token = ctx.tokens[start_idx]
end_token = ctx.tokens[ctx.index]
ctx.warnings.append(
CompileWarning(self.message, start_token.start, end_token.end)
) )
return True return True
else:
return False
class Fail(ParseNode): class Fail(ParseNode):
@ -308,7 +293,9 @@ 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
@ -357,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
@ -522,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
@ -556,14 +541,19 @@ class UseQuoted(ParseNode):
if token.type != TokenType.QUOTED: if token.type != TokenType.QUOTED:
return False return False
string = ( unescaped = None
str(token)[1:-1]
.replace("\\n", "\n") try:
.replace('\\"', '"') unescaped = utils.unescape_quote(str(token))
.replace("\\\\", "\\") except utils.UnescapeError as e:
.replace("\\'", "'") start = ctx.tokens[ctx.index - 1].start
) range = Range(start + e.start, start + e.end, ctx.text)
ctx.set_group_val(self.key, string, token) ctx.errors.append(
CompileError(f"Invalid escape sequence '{range.text}'", range)
)
ctx.set_group_val(self.key, unescaped, token)
return True return True
@ -608,6 +598,15 @@ class Keyword(ParseNode):
return str(token) == self.kw return str(token) == self.kw
class Mark(ParseNode):
def __init__(self, key: str):
self.key = key
def _parse(self, ctx: ParseContext):
ctx.set_mark(self.key)
return True
def to_parse_node(value) -> ParseNode: def to_parse_node(value) -> ParseNode:
if isinstance(value, str): if isinstance(value, str):
return Match(value) return Match(value)

View file

@ -19,20 +19,23 @@
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, Template, UI
def parse( def parse(
tokens: T.List[Token], tokens: T.List[Token],
) -> T.Tuple[T.Optional[UI], T.Optional[MultipleErrors], T.List[PrintableError]]: ) -> 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."""
try: try:
ctx = ParseContext(tokens) 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
ast_node = ctx.last_group.to_ast()
errors = [*ctx.errors, *ast_node.errors] errors = [*ctx.errors, *ast_node.errors]
warnings = [*ctx.warnings, *ast_node.warnings] warnings = [*ctx.warnings, *ast_node.warnings]

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, CompilerBugError from . import utils
class TokenType(Enum): class TokenType(Enum):
@ -38,8 +39,8 @@ 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_]+"),
@ -62,7 +63,13 @@ class Token:
def __str__(self) -> str: def __str__(self) -> str:
return self.string[self.start : self.end] return self.string[self.start : self.end]
@property
def range(self) -> "Range":
return Range(self.start, self.end, self.string)
def get_number(self) -> T.Union[int, float]: def get_number(self) -> T.Union[int, float]:
from .errors import CompileError, CompilerBugError
if self.type != TokenType.NUMBER: if self.type != TokenType.NUMBER:
raise CompilerBugError() raise CompilerBugError()
@ -70,15 +77,17 @@ class Token:
try: try:
if string.startswith("0x"): if string.startswith("0x"):
return int(string, 16) return int(string, 16)
else: elif "." in string:
return float(string) return float(string)
else:
return int(string)
except: except:
raise CompileError( raise CompileError(f"{str(self)} is not a valid number literal", self.range)
f"{str(self)} is not a valid number literal", self.start, self.end
)
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
@ -93,7 +102,8 @@ def _tokenize(ui_ml: str):
if not matched: if not matched:
raise CompileError( raise CompileError(
"Could not determine what kind of syntax is meant here", i, i "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)
@ -101,3 +111,45 @@ def _tokenize(ui_ml: str):
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
@ -61,7 +61,14 @@ class Field:
def __init__(self, offset: int, type: str, 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
self._shift = shift if not mask or sys.byteorder == "little":
self._shift = shift
elif self._type == "u8" or self._type == "i8":
self._shift = 8 - (shift + mask)
elif self._type == "u16" or self._type == "i16":
self._shift = 16 - (shift + mask)
else:
self._shift = 32 - (shift + mask)
self._mask = (1 << mask) - 1 if mask else None self._mask = (1 << mask) - 1 if mask else None
self._name = f"{offset}__{type}__{shift}__{mask}" self._name = f"{offset}__{type}__{shift}__{mask}"
@ -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,9 +140,16 @@ 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")
ARG_NAME = Field(0x0, "string")
ARG_TYPE = Field(0xC, "u32")
SIGNATURE_RETURN_TYPE = Field(0x0, "u32")
SIGNATURE_N_ARGUMENTS = Field(0x6, "u16")
SIGNATURE_ARGUMENTS = Field(0x8, "offset")
ATTR_OFFSET = Field(0x0, "u32") ATTR_OFFSET = Field(0x0, "u32")
ATTR_NAME = Field(0x0, "string") ATTR_NAME = Field(0x4, "string")
ATTR_VALUE = Field(0x0, "string") ATTR_VALUE = Field(0x8, "string")
TYPE_BLOB_TAG = Field(0x0, "u8", 3, 5) TYPE_BLOB_TAG = Field(0x0, "u8", 3, 5)
TYPE_BLOB_INTERFACE = Field(0x2, "dir_entry") TYPE_BLOB_INTERFACE = Field(0x2, "dir_entry")
@ -142,11 +157,15 @@ class Typelib:
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")
@ -162,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")
@ -180,6 +199,11 @@ 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")
@ -237,14 +261,14 @@ 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) -> int: def _int(self, size, signed) -> int:
return int.from_bytes( return int.from_bytes(
self._typelib_file[self._offset : self._offset + size], sys.byteorder self._typelib_file[self._offset : self._offset + size],
sys.byteorder,
signed=signed,
) )

View file

@ -18,6 +18,7 @@
# 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:
@ -98,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,12 +18,11 @@
# 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(
[ [

View file

@ -2,9 +2,9 @@ FROM fedora:latest
RUN dnf install -y meson gcc g++ python3-pip gobject-introspection-devel \ RUN dnf install -y meson gcc g++ python3-pip gobject-introspection-devel \
python3-devel python3-gobject git diffutils xorg-x11-server-Xvfb \ python3-devel python3-gobject git diffutils xorg-x11-server-Xvfb \
appstream-devel "dnf-command(builddep)" appstream-devel dbus-x11 "dnf-command(builddep)" glslc
RUN dnf build-dep -y gtk4 libadwaita RUN dnf build-dep -y gtk4 libadwaita
RUN pip3 install furo mypy sphinx coverage black RUN pip3 install furo mypy sphinx coverage black isort
COPY install_deps.sh . COPY install_deps.sh .
RUN ./install_deps.sh RUN ./install_deps.sh

View file

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

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

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

View file

@ -17,9 +17,9 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'Blueprint' project = "Blueprint"
copyright = '2021-2023, 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

@ -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.8.0" "tag": "v0.16.0"
} }
] ]
} }

View file

@ -22,11 +22,11 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces.
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;
} }
} }
@ -59,7 +59,7 @@ Features
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>`_
@ -81,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

@ -21,7 +21,7 @@ The tokenizer encountered an unexpected sequence of characters that aren't part
child_not_accepted child_not_accepted
------------------ ------------------
The parent class does not have child widgets (it does not implement `Gtk.Buildable <https://docs.gtk.org/gtk4/iface.Buildable.html>`_ and is not a subclass of `Gio.ListStore <https://docs.gtk.org/gio/class.ListStore.html>`_). Some classes use properties instead of children to add widgets. Check the parent class's documentation. The parent class does not have child objects (it does not implement `Gtk.Buildable <https://docs.gtk.org/gtk4/iface.Buildable.html>`_ and is not a subclass of `Gio.ListStore <https://docs.gtk.org/gio/class.ListStore.html>`_). Some classes use properties instead of children to add widgets. Check the parent class's documentation.
.. _Diagnostic conversion_error: .. _Diagnostic conversion_error:
@ -166,14 +166,14 @@ 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: 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:: blueprintui .. code-block:: blueprint
using Gtk 4.0; using Gtk 4.0;
using Gtk 3.0; using Gtk 3.0;
But so will this: But so will this:
.. code-block:: blueprintui .. code-block:: blueprint
using Gtk 4.0; using Gtk 4.0;
using Handy 1; using Handy 1;

View file

@ -10,14 +10,14 @@ Document Root
.. rst-class:: grammar-block .. rst-class:: grammar-block
Root = :ref:`GtkDecl<Syntax GtkDecl>` (:ref:`Using<Syntax Using>`)* ( :ref:`Template<Syntax Template>` | :ref:`Menu<Syntax Menu>` | :ref:`Object<Syntax Object>` )* EOF 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 sor more :ref:`imports<Syntax Using>`, and a list of :ref:`objects<Syntax Object>` and/or a :ref:`template<Syntax Template>`. 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 Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
// Gtk Declaration // Gtk Declaration
using Gtk 4.0; using Gtk 4.0;
@ -43,7 +43,7 @@ Every blueprint file begins with the line ``using Gtk 4.0;``, which declares the
Example Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
using Gtk 4.0; using Gtk 4.0;
@ -68,7 +68,21 @@ The compiler requires typelib files for these libraries to be installed. They ar
Example Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
// Import libadwaita // Import libadwaita
using Adw 1; 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

@ -6,12 +6,12 @@ Expressions make your user interface code *reactive*. This means when your
application's data changes, the user interface reacts to the change application's data changes, the user interface reacts to the change
automatically. automatically.
.. code-block:: blueprintui .. code-block:: blueprint
label: bind MyAppWindow.account.username; label: bind template.account.username;
/* ^ ^ ^ /* ^ ^ ^
| creates lookup expressions that are re-evaluated when | creates lookup expressions that are re-evaluated when
| the account's username *or* the account itself changes | the account's username *or* the account itself changes
| |
binds the `label` property to the expression's output binds the `label` property to the expression's output
*/ */
@ -42,22 +42,22 @@ Expressions are composed of property lookups and/or closures. Property lookups a
.. _Syntax LookupExpression: .. _Syntax LookupExpression:
Lookup Expressions Lookups
------------------ -------
.. rst-class:: grammar-block .. rst-class:: grammar-block
LookupExpression = '.' <property::ref:`IDENT<Syntax IDENT>`> LookupExpression = '.' <property::ref:`IDENT<Syntax IDENT>`>
Lookup expressions perform a GObject property lookup on the preceding expression. They are recalculated whenever the property changes, using the `notify signal <https://docs.gtk.org/gobject/signal.Object.notify.html>`_ Lookup expressions perform a GObject property lookup on the preceding expression. They are recalculated whenever the property changes, using the `notify signal <https://docs.gtk.org/gobject/signal.Object.notify.html>`_.
The type of a property expression is the type of the property it refers to. The type of a property expression is the type of the property it refers to.
.. _Syntax ClosureExpression: .. _Syntax ClosureExpression:
Closure Expressions Closures
------------------- --------
.. rst-class:: grammar-block .. rst-class:: grammar-block
@ -65,23 +65,48 @@ Closure Expressions
Closure expressions allow you to perform additional calculations that aren't supported in blueprint by writing those calculations as application code. These application-defined functions are created in the same way as :ref:`signal handlers<Syntax Signal>`. Closure expressions allow you to perform additional calculations that aren't supported in blueprint by writing those calculations as application code. These application-defined functions are created in the same way as :ref:`signal handlers<Syntax Signal>`.
Expressions are only reevaluated when their inputs change. Because blueprint doesn't manage a closure's application code, it can't tell what changes might affect the result. Therefore, closures must be *pure*, or deterministic. They may only calculate the result based on their immediate inputs, properties of their inputs or outside variables. Expressions are only reevaluated when their inputs change. Because blueprint doesn't manage a closure's application code, it can't tell what changes might affect the result. Therefore, closures must be *pure*, or deterministic. They may only calculate the result based on their immediate inputs, not properties of their inputs or outside variables.
Blueprint doesn't know the closure's return type, so closure expressions must be cast to the correct return type using a :ref:`cast expression<Syntax CastExpression>`. 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: .. _Syntax CastExpression:
Cast Expressions Casts
---------------- -----
.. rst-class:: grammar-block .. rst-class:: grammar-block
CastExpression = 'as' '<' :ref:`TypeName<Syntax TypeName>` '>' CastExpression = 'as' '<' :ref:`TypeName<Syntax TypeName>` '>'
Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. This is necessary for closures and for properties of application-defined types.
.. code-block:: blueprintui Example
~~~~~~~
.. code-block:: blueprint
// Cast the result of the closure so blueprint knows it's a string // Cast the result of the closure so blueprint knows it's a string
label: bind $my_closure() as <string> label: bind $format_bytes(template.file-size) as <string>
.. _Syntax ExprValue:
Expression Values
-----------------
.. rst-class:: grammar-block
ExprValue = 'expr' :ref:`Expression<Syntax Expression>`
Some APIs take *an expression itself*--not its result--as a property value. For example, `Gtk.BoolFilter <https://docs.gtk.org/gtk4/class.BoolFilter.html>`_ has an ``expression`` property of type `Gtk.Expression <https://docs.gtk.org/gtk4/class.Expression.html>`_. This expression is evaluated for every item in a list model to determine whether the item should be filtered.
To define an expression for such a property, use ``expr`` instead of ``bind``. Inside the expression, you can use the ``item`` keyword to refer to the item being evaluated. You must cast the item to the correct type using the ``as`` keyword, and you can only use ``item`` in a property lookup--you may not pass it to a closure.
Example
~~~~~~~
.. code-block:: blueprint
BoolFilter {
expression: expr item as <$UserAccount>.active;
}

View file

@ -10,12 +10,14 @@ Properties are the main way to set values on objects, but they are limited by th
Extensions are a feature of ``Gtk.Buildable``--see `Gtk.Buildable.custom_tag_start() <https://docs.gtk.org/gtk4/vfunc.Buildable.custom_tag_start.html>`_ for internal details. Extensions are a feature of ``Gtk.Buildable``--see `Gtk.Buildable.custom_tag_start() <https://docs.gtk.org/gtk4/vfunc.Buildable.custom_tag_start.html>`_ for internal details.
Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue <https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues>`_. Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue <https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues>`_.
.. rst-class:: grammar-block .. rst-class:: grammar-block
Extension = :ref:`ExtAccessibility<Syntax ExtAccessibility>` Extension = :ref:`ExtAccessibility<Syntax ExtAccessibility>`
| :ref:`ExtAdwAlertDialog<Syntax ExtAdwAlertDialog>`
| :ref:`ExtAdwMessageDialog<Syntax ExtAdwMessageDialog>` | :ref:`ExtAdwMessageDialog<Syntax ExtAdwMessageDialog>`
| :ref:`ExtAdwBreakpoint<Syntax ExtAdwBreakpoint>`
| :ref:`ExtComboBoxItems<Syntax ExtComboBoxItems>` | :ref:`ExtComboBoxItems<Syntax ExtComboBoxItems>`
| :ref:`ExtFileFilterMimeTypes<Syntax ExtFileFilter>` | :ref:`ExtFileFilterMimeTypes<Syntax ExtFileFilter>`
| :ref:`ExtFileFilterPatterns<Syntax ExtFileFilter>` | :ref:`ExtFileFilterPatterns<Syntax ExtFileFilter>`
@ -35,12 +37,15 @@ Accessibility Properties
.. rst-class:: grammar-block .. rst-class:: grammar-block
ExtAccessibility = 'accessibility' '{' ExtAccessibilityProp* '}' ExtAccessibility = 'accessibility' '{' ExtAccessibilityProp* '}'
ExtAccessibilityProp = <name::ref:`IDENT<Syntax IDENT>`> ':' :ref:`Value <Syntax Value>` ';' ExtAccessibilityProp = <name::ref:`IDENT<Syntax IDENT>`> ':' (:ref:`Value <Syntax Value>` | ('[' (:ref: Value <Syntax Value>),* ']') ) ';'
Valid in any `Gtk.Widget <https://docs.gtk.org/gtk4/class.Widget.html>`_. 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. 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: .. _Syntax ExtAdwBreakpoint:
@ -62,6 +67,35 @@ Defines the condition for a breakpoint and the properties that will be set at th
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. 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: .. _Syntax ExtAdwMessageDialog:
Adw.MessageDialog Responses Adw.MessageDialog Responses
@ -77,7 +111,7 @@ Valid in `Adw.MessageDialog <https://gnome.pages.gitlab.gnome.org/libadwaita/doc
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. 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:: blueprintui .. code-block:: blueprint
using Adw 1; using Adw 1;
@ -105,7 +139,7 @@ Valid in `Gtk.ComboBoxText <https://docs.gtk.org/gtk4/class.ComboBoxText.html>`_
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. 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:: blueprintui .. code-block:: blueprint
ComboBoxText { ComboBoxText {
items [ items [
@ -132,7 +166,7 @@ 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. 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:: blueprintui .. code-block:: blueprint
FileFilter { FileFilter {
mime-types [ "text/plain", "image/*" ] mime-types [ "text/plain", "image/*" ]
@ -155,7 +189,7 @@ 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. 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:: blueprintui .. code-block:: blueprint
Grid { Grid {
Button { Button {
@ -187,28 +221,30 @@ Gtk.BuilderListItemFactory Templates
.. rst-class:: grammar-block .. rst-class:: grammar-block
ExtListItemFactory = 'template' :ref:`ObjectContent<Syntax Object>` ExtListItemFactory = 'template' :ref:`TypeName<Syntax TypeName>` :ref:`ObjectContent<Syntax Object>`
Valid in `Gtk.BuilderListItemFactory <https://docs.gtk.org/gtk4/class.BuilderListItemFactory.html>`_. 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 the list items. The sub-blueprint may not reference objects in the main blueprint or vice versa. 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 is `Gtk.ListItem <https://docs.gtk.org/gtk4/class.ListItem.html>`_, and the template should have a ``child`` property. 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:: blueprintui .. code-block:: blueprint
ListBox { ListView {
factory: ListItemFactory { factory: BuilderListItemFactory {
template { template ListItem {
child: Label { child: Label {
label: bind item.string; label: bind template.item as <StringObject>.string;
}; };
} }
} };
model: StringList { model: NoSelection {
strings [ "Item 1", "Item 2", "Item 3" ] model: StringList {
} strings [ "Item 1", "Item 2", "Item 3" ]
};
};
} }
@ -241,7 +277,7 @@ 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. The ``widgets`` block defines the widgets that will be added to the size group.
.. code-block:: blueprintui .. code-block:: blueprint
Box { Box {
Button button1 {} Button button1 {}
@ -267,7 +303,7 @@ Valid in `Gtk.StringList <https://docs.gtk.org/gtk4/class.StringList.html>`_.
The ``strings`` block defines the strings in the string list. The ``strings`` block defines the strings in the string list.
.. code-block:: blueprintui .. code-block:: blueprint
StringList { StringList {
strings ["violin", "guitar", _("harp")] strings ["violin", "guitar", _("harp")]
@ -282,13 +318,13 @@ CSS Styles
.. rst-class:: grammar-block .. rst-class:: grammar-block
ExtStyles = 'styles' '[' ExtStylesProp* ']' ExtStyles = 'styles' '[' ExtStylesProp* ']'
ExtStylesClass = <name::ref:`QUOTED<Syntax QUOTED>`> ExtStylesProp = <name::ref:`QUOTED<Syntax QUOTED>`>
Valid in any `Gtk.Widget <https://docs.gtk.org/gtk4/class.Widget.html>`_. 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. The ``styles`` block defines CSS classes that will be added to the widget.
.. code-block:: blueprintui .. code-block:: blueprint
Button { Button {
styles ["suggested-action"] styles ["suggested-action"]
@ -324,7 +360,7 @@ The ``action response`` extension sets the ``action`` child type for the child a
No more than one child of a dialog or infobar may have the ``default`` flag. No more than one child of a dialog or infobar may have the ``default`` flag.
.. code-block:: blueprintui .. code-block:: blueprint
Dialog { Dialog {
[action response=ok default] [action response=ok default]

View file

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

View file

@ -21,7 +21,7 @@ Menus, such as the application menu, are defined using the ``menu`` keyword. Men
Example Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
menu my_menu { menu my_menu {
submenu { submenu {
@ -53,7 +53,7 @@ The most common menu attributes are ``label``, ``action``, and ``icon``. Because
Example Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
menu { menu {
item ("label") item ("label")

View file

@ -24,7 +24,7 @@ Optionally, objects may have an ID to provide a handle for other parts of the bl
Example Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
Label label1 { Label label1 {
label: "Hello, world!"; label: "Hello, world!";
@ -58,7 +58,7 @@ Properties
.. rst-class:: grammar-block .. rst-class:: grammar-block
Property = <name::ref:`IDENT<Syntax IDENT>`> ':' ( :ref:`PropertyBinding<Syntax PropertyBinding>` | :ref:`Binding<Syntax Binding>` | :ref:`ObjectValue<Syntax ObjectValue>` | :ref:`Value<Syntax Value>` ) ';' Property = <name::ref:`IDENT<Syntax IDENT>`> ':' ( :ref:`Binding<Syntax Binding>` | :ref:`ExprValue<Syntax ExprValue>` | :ref:`ObjectValue<Syntax ObjectValue>` | :ref:`Value<Syntax Value>` ) ';'
Properties specify the details of each object, like a label's text, an image's icon name, or the margins on a container. Properties specify the details of each object, like a label's text, an image's icon name, or the margins on a container.
@ -69,7 +69,7 @@ A property's value can be another object, either inline or referenced by ID.
Example Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
Label { Label {
label: "text"; label: "text";
@ -91,7 +91,7 @@ Signal Handlers
.. rst-class:: grammar-block .. rst-class:: grammar-block
Signal = <name::ref:`IDENT<Syntax IDENT>`> ('::' <detail::ref:`IDENT<Syntax IDENT>`>)? '=>' '$' <handler::ref:`IDENT<Syntax IDENT>`> '(' <object::ref:`IDENT<Syntax IDENT>`>? ')' (SignalFlag)* ';' Signal = <name::ref:`IDENT<Syntax IDENT>`> ('::' <detail::ref:`IDENT<Syntax IDENT>`>)? '=>' '$' <handler::ref:`IDENT<Syntax IDENT>`> '(' <object::ref:`IDENT<Syntax IDENT>`>? ')' (SignalFlag)* ';'
SignalFlag = 'after' | 'swapped' SignalFlag = 'after' | 'swapped' | 'not-swapped'
Signals are one way to respond to user input (another is `actions <https://docs.gtk.org/gtk4/actions.html>`_, which use the `action-name property <https://docs.gtk.org/gtk4/property.Actionable.action-name.html>`_). Signals are one way to respond to user input (another is `actions <https://docs.gtk.org/gtk4/actions.html>`_, which use the `action-name property <https://docs.gtk.org/gtk4/property.Actionable.action-name.html>`_).
@ -99,16 +99,17 @@ Signals provide a handle for your code to listen to events in the UI. The handle
Optionally, you can provide an object ID to use when connecting the signal. Optionally, you can provide an object ID to use when connecting the signal.
The ``swapped`` flag is used to swap the order of the object and userdata arguments in C applications. If an object argument is specified, then this is the default behavior, so the ``not-swapped`` flag can be used to prevent the swap.
Example Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
Button { Button {
clicked => $on_button_clicked(); clicked => $on_button_clicked();
} }
.. _Syntax Child: .. _Syntax Child:
Children Children
@ -141,7 +142,7 @@ Examples
Add children to a container Add children to a container
+++++++++++++++++++++++++++ +++++++++++++++++++++++++++
.. code-block:: blueprintui .. code-block:: blueprint
Button { Button {
Image {} Image {}
@ -150,7 +151,7 @@ Add children to a container
Child types Child types
+++++++++++ +++++++++++
.. code-block:: blueprintui .. code-block:: blueprint
HeaderBar { HeaderBar {
[start] [start]
@ -165,7 +166,7 @@ Child types
Child extensions Child extensions
++++++++++++++++ ++++++++++++++++
.. code-block:: blueprintui .. code-block:: blueprint
Dialog { Dialog {
// Here, a child extension annotation defines the button's response. // Here, a child extension annotation defines the button's response.
@ -176,7 +177,7 @@ Child extensions
Internal children Internal children
+++++++++++++++++ +++++++++++++++++
.. code-block:: blueprintui .. code-block:: blueprint
Dialog { Dialog {
[internal-child content_area] [internal-child content_area]

View file

@ -15,7 +15,7 @@ Widget subclassing is one of the primary techniques for structuring an applicati
You could implement this with the following blueprint: You could implement this with the following blueprint:
.. code-block:: blueprintui .. code-block:: blueprint
using Gtk 4.0; using Gtk 4.0;
@ -39,7 +39,7 @@ We can solve these problems by giving each widget its own blueprint file, which
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: 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:: blueprintui .. code-block:: blueprint
using Gtk 4.0; using Gtk 4.0;
@ -56,7 +56,7 @@ This blueprint can only be used by the ``MapsHeaderBar`` constructor. Instantiat
This ``MapsHeaderBar`` class, along with its blueprint template, can then be referenced in another blueprint: This ``MapsHeaderBar`` class, along with its blueprint template, can then be referenced in another blueprint:
.. code-block:: blueprintui .. code-block:: blueprint
using Gtk 4.0; using Gtk 4.0;
@ -66,17 +66,31 @@ This ``MapsHeaderBar`` class, along with its blueprint template, can then be ref
} }
} }
ID & Parent Parameters 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 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. 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 Language Implementations
------------------------ ------------------------
- ``gtk_widget_class_set_template ()`` in C: https://docs.gtk.org/gtk4/class.Widget.html#building-composite-widgets-from-template-xml - **C** ``gtk_widget_class_set_template ()``: https://docs.gtk.org/gtk4/class.Widget.html#building-composite-widgets-from-template-xml
- ``#[template]`` in gtk-rs: https://gtk-rs.org/gtk4-rs/stable/latest/book/composite_templates.html - **gtk-rs** ``#[template]``: https://gtk-rs.org/gtk4-rs/stable/latest/book/composite_templates.html
- ``GObject.registerClass()`` in GJS: https://gjs.guide/guides/gtk/3/14-templates.html - **GJS** ``GObject.registerClass()``: https://gjs.guide/guides/gtk/3/14-templates.html
- **PyGObject** ``@Gtk.Template``: https://pygobject.gnome.org/guide/gtk_template.html

View file

@ -25,8 +25,7 @@ Literals
NumberLiteral = ( '-' | '+' )? <value::ref:`NUMBER<Syntax NUMBER>`> NumberLiteral = ( '-' | '+' )? <value::ref:`NUMBER<Syntax NUMBER>`>
IdentLiteral = <ident::ref:`IDENT<Syntax IDENT>`> IdentLiteral = <ident::ref:`IDENT<Syntax IDENT>`>
Literals are used to specify values for properties. They can be strings, numbers, references to objects, types, boolean values, or enum members. Literals are used to specify values for properties. They can be strings, numbers, references to objects, ``null``, types, boolean values, or enum members.
.. _Syntax TypeLiteral: .. _Syntax TypeLiteral:
@ -45,7 +44,7 @@ The type of a ``typeof<>`` literal is `GType <https://docs.gtk.org/gobject/alias
Example Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
Gio.ListStore { Gio.ListStore {
item-type: typeof<GObject.Object>; item-type: typeof<GObject.Object>;
@ -66,7 +65,7 @@ Flags are used to specify a set of options. One or more of the available flag va
Example Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
Adw.TabView { Adw.TabView {
shortcuts: control_tab | control_shift_tab; shortcuts: control_tab | control_shift_tab;
@ -85,7 +84,7 @@ Translated Strings
Use ``_("...")`` to mark strings as translatable. You can put a comment for translators on the line above if needed. Use ``_("...")`` to mark strings as translatable. You can put a comment for translators on the line above if needed.
.. code-block:: blueprintui .. code-block:: blueprint
Gtk.Label label { Gtk.Label label {
/* Translators: This is the main text of the welcome screen */ /* Translators: This is the main text of the welcome screen */
@ -94,7 +93,7 @@ Use ``_("...")`` to mark strings as translatable. You can put a comment for tran
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. 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:: blueprintui .. code-block:: blueprint
Gtk.Label label { Gtk.Label label {
/* Translators: This is a section in the preferences window */ /* Translators: This is a section in the preferences window */
@ -102,46 +101,49 @@ Use ``C_("context", "...")`` to add a *message context* to a string to disambigu
} }
.. _Syntax PropertyBinding: .. _Syntax Binding:
Property Bindings Bindings
----------------- --------
.. rst-class:: grammar-block .. rst-class:: grammar-block
PropertyBinding = 'bind-property' <source::ref:`IDENT<Syntax IDENT>`> '.' <property::ref:`IDENT<Syntax IDENT>`> (PropertyBindingFlag)* Binding = 'bind' :ref:`Expression<Syntax Expression>` (BindingFlag)*
PropertyBindingFlag = 'inverted' | 'bidirectional' | 'no-sync-create' BindingFlag = 'inverted' | 'bidirectional' | 'no-sync-create'
Bindings keep a property updated as another property changes. They can be used to keep the UI in sync with application data, or to connect two parts of the UI. 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 Example
~~~~~~~ ~~~~~~~
.. code-block:: blueprintui .. code-block:: blueprint
/* Use property bindings to show a label when a switch /* Use bindings to show a label when a switch
* is active, without any application code */ * is active, without any application code */
Switch advanced_feature {} Switch show_label {}
Label warning { Label {
visible: bind-property advanced_feature.active; visible: bind show_label.active;
label: _("This is an advanced feature. Use with caution!"); label: _("I'm a label that's only visible when the switch is enabled!");
} }
.. _Syntax Binding:
Expression Bindings
-------------------
.. rst-class:: grammar-block
Binding = 'bind' :ref:`Expression<Syntax Expression>`
Expression bindings serve the same purpose as property bindings, but are more powerful. They can call application code to compute the value of a property, and they can do multi-step property lookups. See :ref:`the expressions page<Syntax Expression>`.
.. _Syntax ObjectValue: .. _Syntax ObjectValue:
Object Values Object Values
@ -166,3 +168,14 @@ String Values
StringValue = :ref:`Translated<Syntax Translated>` | :ref:`QuotedLiteral<Syntax Literal>` 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. 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.8.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,20 +1,21 @@
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 ( from blueprintcompiler.errors import (
PrintableError,
MultipleErrors,
CompileError, CompileError,
CompilerBugError, CompilerBugError,
MultipleErrors,
PrintableError,
) )
from blueprintcompiler.tokenizer import Token, TokenType, tokenize from blueprintcompiler.tokenizer import Token, TokenType, tokenize
from blueprintcompiler import utils
@PythonFuzz @PythonFuzz

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,9 @@
using Gtk 4.0;
using Adw 1;
Adw.AlertDialog {
responses [
cancel: _("Cancel") disabled disabled,
ok: _("Ok") destructive suggested,
]
}

View file

@ -0,0 +1,2 @@
6,34,8,Duplicate 'disabled' flag
7,29,9,'suggested' and 'destructive' are exclusive

View file

@ -0,0 +1,9 @@
using Gtk 4.0;
using Adw 1;
Adw.MessageDialog {
responses [
cancel: _("Cancel") disabled disabled,
ok: _("Ok") destructive suggested,
]
}

View file

@ -0,0 +1,2 @@
6,34,8,Duplicate 'disabled' flag
7,29,9,'suggested' and 'destructive' are exclusive

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Label {
label: '***** \f *****';
}

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