Add a field to an AST and render it
The *Ast messages are the only interface between parsers and viewers. Adding a
field touches the proto, which lives in three repos, and then the viewer. This
page gives the order.
Take a worked example: a TextAst that should carry an encoding string, shown
in the header line of the text viewer.
Before you start
- The change is additive. You add a field with a new number. You never renumber, retype or reuse a number, because parsers and viewers are built and released separately and an older module will meet a newer one (step 7).
- Work in a worktree in each repo you touch (see
CONTRIBUTING.md).
1. Edit the canonical proto
The canonical copy is core/proto/preview.proto in poka-interfaces. Add
the field at the end of the message, with the next unused number and a comment
that says what it holds and when it is empty:
message TextAst {
// ... existing fields ...
FileFacts facts = 8;
// Character encoding the parser decoded the file from, e.g. "UTF-8" or
// "Windows-1250 (assumed)". Empty when the format has no notion of one.
string encoding = 9;
}
2. Copy it to the other two repos
poka-parsers and poka-viewers each carry their own vendored copy at
core/proto/preview.proto. They are kept byte-identical by hand: copy the file
over.
cp poka-interfaces/core/proto/preview.proto poka-parsers/core/proto/preview.proto
cp poka-interfaces/core/proto/preview.proto poka-viewers/core/proto/preview.proto
Check that nothing else holds a copy. The check script lives in poka-parsers
and takes the repo to check with --root:
python3 poka-parsers/scripts/check-proto-drift.py --root poka-interfaces
python3 poka-parsers/scripts/check-proto-drift.py --root poka-parsers
python3 poka-parsers/scripts/check-proto-drift.py --root poka-viewers
What it checks: that the canonical file exists in the repo and is not a broken
symlink, and that no other preview.proto exists under the source directories.
It does not compare the three copies with each other, so confirm they match
yourself (sha256sum on the three files, or cmp).
Nothing generated is checked in. Each repo builds the Rust bindings from the
proto at build time; in this repo that is the poka-preview-proto crate. Do not
add a poka.preview.rs.
3. Build everything that reads the message
Adding a field changes the generated struct. A place that builds the struct
without ..Default::default() stops compiling with "missing fields". In this
repo:
cargo build --release --workspace --target wasm32-wasip1
Fix each viewer that builds the message, usually in its tests. In poka-parsers,
the pre-push hook runs cargo check --workspace when the proto changes for this
reason, and every parser that builds the message needs the same fix.
4. Render the field in the viewer
In viewers/poka-text-viewer/src/main.rs, show it. It is text from the file's
point of view, so escape it:
let encoding = if ast.encoding.is_empty() {
String::new()
} else {
format!(" · {}", html_escape(&ast.encoding))
};
Handle the empty case on purpose: parsers that have not been updated send the default (an empty string, zero, false), and the page must look as it did before.
5. Test it
Add two tests to the viewer: the field shown when set, and the page unchanged
when it is empty. Add the new field to the hostile-string test so a missing
html_escape fails. See Test a viewer.
6. Make a parser fill it
In poka-parsers, set the field in the parser that knows the answer. That is the
parser repo's work; the tutorial "Write a parser" there describes the flow. A
viewer change without a parser that sends the field has nothing to show.
7. What happens when the pieces meet at different versions
The three repos release separately, and poka-interfaces pins the release of
each in web/extension-releases.json. Protocol Buffers makes the additive change
safe in both directions:
- A parser that sends the new field to a viewer built before it: the old viewer ignores the field it does not know. The preview looks as it always did.
- A viewer that expects the field with a parser built before it: the field has its default value, which is why step 4 handles the empty case.
Renumbering or changing a type breaks both, which is why it is not allowed.
8. Host decoders
The desktop host decodes the proto with generated code, so it needs no edit for a
field it does not use. The web engine has a hand-written decoder,
web/src/proto.js, that reads only the messages the browser puts on the wire:
PreviewRequest and PreviewResponse with its arms, and the facts field of an
*Ast (astFacts). The *Ast bytes themselves are passed from parser to viewer
untouched. So a new field on TextAst needs nothing in proto.js.
A new field in PreviewResponse, in Scene3DAst or Mesh3D (which the hosts
still decode themselves, see
How poka-3d-viewer draws a model),
or in FileFacts (which astFacts reads) may need a change to that decoder.
Check it, and make the change in poka-interfaces in both engines, the Rust one
and web/src/, in the same PR.
Check the result
cargo test --workspaceand the wasm build in this repo.- The same in
poka-parsersfor the parser you changed. sha256sumof the three proto copies: one value.python3 build/scripts/check-docs.py --checkif you changed a page here.