Imported from Zoxc/asm-viewer (
AGENTS.md). Install upstream withnpx skills add Zoxc/asm-viewer. Copyright stays with the author.
User rules
Standing instructions from the user.
-
Never run the app against the user's own storage.
ASSEMBLY_VIEWER_STATEnames the directory everything the app stores goes in -- the projects, the recent list, the settings, the scratchpads and the panic logs -- and every run started from here must set it somewhere throwaway:ASSEMBLY_VIEWER_STATE=$(mktemp -d) cargo runWithout it a run reorders the reader's recent projects, opens and saves over whatever they had open, and, where two checkouts disagree about a stored format, moves their files aside as unreadable -- which is what every load on the way to a write does when it cannot parse one. Two checkouts of this app sharing the desktop's own state directory have already destroyed a
recents.tomltwice this way. It applies to anything that starts the app,cargo run --features devtoolsincluded; the tests keep to directories of their own and need nothing. -
Committing. Whatever is uncommitted when you start stays uncommitted.
-
Run rustfmt over every file you modified, before committing it. Format only those files, with
rustfmt --edition 2021 <paths>, and not the workspace: a barecargo fmtreformats everything, and much of this repo predates anyone running it, so it drags unrelated reflow into a diff that then has to be picked apart by hand. The--edition 2021is load-bearing; plainrustfmtparses as 2015 and will mangle what it cannot read. Nothing here needs arustfmt.toml: the defaults are what the formatted files already follow. -
Commit messages. Keep the title brief -- a line, not a paragraph -- and put what needs saying under it, ideally in one short paragraph. What changed is in the diff; the message is for what it is and why.
-
Don't reference an uncommitted file from a committed one.
-
Keep
notes/Goals.mdcurrent. It is the checklist of planned features. -
notes/specs/is what a finished feature does, one file per area with a section per feature, moved there fromnotes/Goals.md. Never write or change a spec without asking the user first; read the spec files to gauge the writing style and keep development history and implementation detail out (notes/specs/README.md). -
Before presenting edits to the user, make one final pass over every text written (prose, a code comment, a doc comment, markdown): cut what can go, and prefer simple English, short direct sentences and plain words. Neither may lose precision. Only then, not after every edit.
-
Adding a goal is not a request to do it. "Add a goal: …" means write the item down and stop there; the checklist is where work is planned, not where it is started. Implement one only when asked for the thing itself.
-
Prefer TOML for files, not JSON.
-
Add a minimal test case every time something is found wrong with binary inspection.
-
Answer a question about the UI with a headless test rather than by launching the app. A throwaway one, deleted once it has answered, is fine. See
agents/Headless.md.
What this is
A desktop GUI ("Assembly Viewer") for inspecting object files: open an ELF/PE/Mach-O object or a static archive, browse its text symbols, and read a symbol's disassembly, with relocation targets resolved to clickable symbol names, beside the source it was compiled from. Rust, cargo workspace, freya 0.4 for the UI.
Build
Set ASSEMBLY_VIEWER_STATE on every run (the user rule above says why):
ASSEMBLY_VIEWER_STATE=$(mktemp -d) cargo run. It replaces the desktop's own state
directory, so the app keeps its projects, recents, settings and scratchpads there instead.
Alt held while the pages menu is opened adds the Debug page to it: the ways to make the
app misbehave on purpose -- the three panics src/panics.rs tells apart, so the box it puts
up can be looked at -- and every panic file the app has written. A gesture rather than a
feature or a variable, so it needs neither a rebuild nor a restart. Only the menu is gated:
a session that names the page puts it back, a reader with one open having asked for it.
The app's icon is assets/app.svg. assets/render.sh renders it into the committed app.png,
which is the window's icon, and app.ico, which build.rs links into the Windows executable
(on a Windows host only). Wayland ignores a window's own icon: KDE takes it from the desktop
entry named by the app id (APP_ID, src/main.rs), so there it shows only once one is
installed.
cargo run --features devtools starts freya's devtools server alongside the app ([::1]:7354,
opt-in so it never reaches a release build). The viewer is a separate cargo install freya-devtools-app, only one devtools-enabled freya app can run at a time, and there is no in-app
shortcut. See notes/DevTools.md.
The first build compiles Skia (via freya-engine -> freya-skia-safe) and takes a long time. On
Fedora it needs freetype-devel fontconfig-devel libglvnd-devel wayland-devel to link, and mold
is not a supported linker.
Dependency versions are pinned by compatibility, not taste. tree-sitter declares
links = "tree-sitter", so cargo allows the graph exactly one copy of it whatever the
semver: it can move only when freya-code-editor does, and the tree-sitter-* grammars must
sit on that copy's tree-sitter-language ABI (cargo tree -p viewer -d). addr2line /
gimli / object / ruzstd move as a set, each naming the next by exact minor, and must
stay one copy each — check with cargo tree -p analysis -d after touching any of them, which
should report nothing. The reasoning is in the Cargo.toml comments; keep them current.
Test fixtures
Almost every fixture is built in memory with the object and gimli writers
(crates/analysis/tests/common/mod.rs), so the suite needs nothing on disk and is green in a fresh
checkout. The exception is crates/analysis/tests/fixtures/: one small C file and, committed
from it, the two objects gcc produced (so the crate is pinned against DWARF a real toolchain
emits); a stripped shared object gcc and ld produced with its functions hidden
(line_fixture_hidden.so: no symbol table, nothing in .dynsym, so the crate is pinned against
an .eh_frame as a real linker lays it out, the only thing naming its functions); and three DLL
plus .pdb pairs that clang-cl and rustup's rust-lld produced (so it is pinned against a real
linker's PE debug directory and PDB, which nothing in memory can synthesize). Of the three, one
exports its three functions; line_fixture_noexport exports nothing, so every name it shows is
the PDB's, and alone it lists the three <function 0x…>s its .pdata states; and
line_fixture_public is that object linked with a one-function C++ file (public_fixture.cpp)
compiled without /Z7, so the PDB's only name for that function is a decorated public symbol.
tests/real_object.rs and tests/pdb.rs read them and fail loudly rather than skipping when they
are missing; the build commands are in line_fixture.c's header, pdb.rs's and unwind.rs's.
The measurements quoted in agents/ were taken on two inputs cargo build produces: the app's
own debug binary (~331 MB, one linked ELF, ~115k text symbols, ~267 MB of DWARF) and the analysis
crate's own rlib (~20 MB, 196 archive members, DWARF per member). Session state is restored on
launch, so cargo run reopens the last binaries on the last symbol and a visual check costs one
command.
Layout
crates/analysis/src/lib.rs— the crate's root: its modules, what it exports, and the assertion that what crosses threads isSend + Sync.crates/analysis/src/address.rs— the two address spaces as two types:SectionAddress, one of a section's own, andPlacedAddress, one in the space every section of an object shares;Bias, the only thing that crosses between them; and the one conversion itself.crates/analysis/src/model.rs— the data model:Object,Sectionwith theCodeSectiononly a section holding code has,SymbolData,Symbol,ObjectData, the bytes an object was parsed from, andLoadMessage, what went wrong while it was read: a variant per problem, which gives its severity and its words. Alsocovering, the one search an address is looked up in a list of ranges with.crates/analysis/src/extent.rs— how many bytes of code a symbol is: the end its unwind entry states, an ELF's declared size, the debug info's, or the estimate.crates/analysis/src/parse.rs— one object file read into anObject: its sections and where each is placed, its symbols, and the code it declares outside its symbol table.crates/analysis/src/sections.rs— the two rules the parse and the DWARF loader both follow: where each code section is placed, with the error when they cannot be placed apart, and a section's bytes read with a believable size. Also the byte ordergimlireads a file in, for the DWARF loader and the unwind reader.crates/analysis/src/open.rs— the entry point: each file tried as an archive and as an object, and every object handed over as it is parsed.crates/analysis/src/regular.rs— a path someone else chose, opened without trusting it: a regular file or nothing, never a wait on a fifo, read with a bound. Every binary,.pdband source file is read through it, and every text file a project's tree holds.crates/analysis/src/demangle.rs— an object's symbol names demangled in one batch, on a pool of threads with stacks big enough for the deepest name a file can ask for.crates/analysis/src/made_up.rs— the names given to code the file names nothing (an entry point, a function only an unwind entry declares, a fragment of one), and the one place each is spelled.crates/analysis/src/line.rs— line info, lazy: an address range in, source rows out. The seam: the trait every backend answers, the space it answers in, and the one collector that clips every answer's rows to the query, takes the section's bias off them and makes them holdLineInfo's invariants. Names no debug format.crates/analysis/src/line/dwarf.rs— the DWARF backend, and the only part that knows DWARF's debug sections andaddr2line.crates/analysis/src/line/pdb.rs— the PDB backend: a PE's.pdbfound by its CodeView record, matched by GUID and age, read a page at a time; the only part that knowspdb2. Also the one eager path through the seam: opened at parse for the procedures and publics it names, whichparse_objecttakes as symbols.crates/analysis/src/line/intervals.rs— ranges that may overlap, each carrying a value, searched for the ones a range overlaps: a PDB's section contributions and the source index's symbols, one index for both.crates/analysis/src/line/source.rs— the same line info the other way: a file and a line, out to the symbols compiled from them, built on the seam and not on a backend.crates/analysis/src/unwind.rs— the unwind tables a linked image states its functions' bounds in (an x86-64 PE's.pdata, an ELF's.eh_frame), read for the ranges they declare; the only part that reads call-frame information.crates/analysis/src/disasm.rs— the disassembler seam;disasm/x86.rsis the onlyiced-x86.crates/analysis/src/listing.rs— an object's code as one listing keyed by address: a stretch per symbol, decoded on demand, and the bytes between them shown but never decoded.crates/analysis/src/guard.rs— the calls whose panics are caught on purpose, and the flag that lets a panic hook tell one of those from a panic that has broken the app.src/cargo.rs— running cargo and reading what it said: the artifacts it names, the diagnostics it reports, the one line a pane says about a build, and the profile's debug information in the manifest being built. Plain data about someone else's file: what a diagnostic's place means to a cursor issrc/chars.rs.src/verdict.rs— one line saying how something went and whether that is bad news: what every build, run and server reports itself as, and the count with the word for it.src/project.rs— projects: entering one, leaving it, putting it somewhere else, taking it away, and the two calls that save: the lifecycle, which is what touches more than one of the five below.src/project/files.rs— the two files a project is stored in, as serde reads and writes them: the project file the reader may check in, the session the app keeps beside it, and the id that ties the two together. The schema and nothing else.src/project/restore.rs— live state into a session and back: every open tab and where each place was left on the way out, every saved place looked up in the objects loaded now on the way back, under one answer about which of their files have been rebuilt.src/project/recents.rs— the projects the reader has had open, most recently first, and the rows the recent list is drawn from.src/project/saves.rs— when the two files are written: what each last held, what has changed since, and which changes go to disk at once rather than waiting for a flush.src/project/trust.rs— the directories the reader agreed to a language server reading: kept in the store, since both of a project's files can arrive with it.src/store.rs— everything the app stores: the directory it goes in and the variable that names it, the atomic write, the read that moves a file aside rather than let the next write replace it, the claim of a free name, and how much of a recent order a file is written with.src/dialog.rs— the box the app says something in outside its own window: the desktop's own, which a panic can put up where there is no frame left to draw in, and which will not scroll -- so what goes in one is capped by the caller.src/reveal.rs— showing a file or a folder in the desktop's file manager: the programs each platform is asked with, in the order they are tried, and the thread they are run on.src/settings.rs— the user's own settings (settings.toml): the font overrides and the theme.src/shortcuts.rs— every key and every mouse gesture the app answers to, under the place each applies: the list the Shortcuts page draws, written by hand.src/shutdown.rs— everything that has to happen before the process ends, in the one order both the window's close hook and the panic hook's shutdown thread run it in: every program the app started stopped, then the project, the settings and the scratchpads flushed.src/languages.rs—Language: the one list of extensions the app knows, and the one place a per-language fact is decided -- what compiles, which tree-sitter grammar colours it, how its functions are found, and which language server reads it. Plural becausesrc/ui/language.rsholds the language server's own state and the prelude brings that name in.src/source.rs— source files read off disk, uncached (the parse over one is what is cached,src/ui/highlight.rs); the one name a path is called by; and whether a path can be shown at all, which is the reader's own first step and the gate the UI puts in front of it. Alsoread_text_in, how the other text files a project's tree holds are read.Seeded, the test-only way to hand the read a file with nothing on the disk behind it, is insrc/source/tests.rsand re-exported here.src/scratchpad.rs— a scratchpad: its id, its name, the cargo package generated around one source file, its build, and the pads there are in the order they were last opened.src/temporary.rs— test-only: a path under the system temporary directory that a test owns, removed when the test ends; and a fifo made there, with the timeout that fails a read that waits on one.src/shared.rs— a list built once and passed on by its pointer, equal only to the same build: what every list of rows the UI draws is; and the same identity for oneArc, whether or not a field has it.src/filter.rs— what a filter bar is asking for, the matcher it compiles to, and what it leaves of a list: the ranked index every filtered list is drawn from.src/fuzzy.rs— characters in order: what the file finder's box asks of a path, where it hit, and how well.src/find.rs— what a find bar asks of one code pane: where a pattern hits in a line as it is drawn, in the columns a pane counts, and which hit a step goes to.src/walk.rs— the project's directory walked: the rules both readers of it share, and the files that came back.src/search.rs— the project's directory searched for a pattern: the walk, the match, and the cap.src/grouped.rs— items under the file each is in, with a fold per file, flattened into the rows a list draws: what a search's hits and a name's references are both held in, and the cut that makes a line of a file into the text one of those rows draws.src/tree.rs— the Objects list's tree shape, and which files are still being read into it.src/files.rs— the project's directory as a tree: read one level per unfold, forgotten on the fold, and flattened into the rows the Files view draws.src/lanes.rs— where each branch is drawn in the assembly view's arrow gutter.src/lsp.rs— the language server: the program the project names started over its directory, the messages spoken to it, which way it counts a column and the two conversions no column crosses the module without, and the process a stop kills.src/lsp/settings.rs— what a server is told about the project: what this app asks of every one, and the project's own.vscode/settings.jsonread and laid over it. The only part of the conversation that reads a file.src/uri.rs— a path as afile:URI and back: the one encoder the language server and the file manager call both use, the decoder, anddrive, the rule saying a path is Windows' by its drive letter, whichsrc/cargo.rsfollows too.src/references.rs— the places a language server answered a question with, grouped under the file each is in and with the text of the line each is on: what the Locations panel draws.src/links.rs— which names in a source file are links, out of what a language server calls them: the rule, and which of two questions following one asks.src/process.rs— every program the app starts and must be able to end outright: the group each is started in, the handle that stops it, the one list a shutdown walks, the pipes read on threads of their own, and a run's output cut into rows.src/pixels.rs— the device pixel grid, and a stroke put on it by its edges.src/chars.rs— the run a sweep over a listing selects: a place is a row and a byte column, and what each row draws, what the rows it touches are, and what it copies. Also the two conversions to and from the UTF-16 units the text engine, a language server and freya's editor count in, each made at that edge; where the nth character of a string begins; and where a compiler's line and column put a cursor.src/section.rs— the rows a listing of an object's whole code is made of: estimated before a stretch is decoded, the symbol's own after, and an address for every one.src/document.rs— what the reader has open: an object, a symbol, a source file, or an object's whole code, one flat enum. What every tab, trail, visit and bookmark is keyed by. AlsoPane, the two sides a tab has, and which of them a document is driven from;Kind, the three kinds of place a document and aSavedDocumentboth answer for, which is all a glyph needs of either; andAddress, an address that has not committed to either of the crate's two spaces, for the three places that hold one apart from the document that says which it is.src/docs.rs—Docs, the table mapping a document tab'sDocIdto the trail behind it: every place the tab has shown with a cursor on the one it shows, and which tab is the temporal one.src/compiled.rs— the symbols a source line was compiled into, and which of them a tab follows.src/tabs.rs—Strip, the open tabs in the reader's order and which is on screen, and what a tab is: a document, or one of the pages;landing, the rule a close obeys.src/positions.rs— where each place on each tab was left:Positions, the map all of it is kept in;Spot, a place in the listing of an object's whole code; andDriven, two of those maps -- which line a source-driven tab's assembly side follows and which symbol was chosen.src/order.rs— one list of places, newest first and no two the same: what a tab's trail, the record of visits and the two recent orders on disk are all made of.src/history.rs— one tab's back/forward trail: anOrderof places and a cursor into it.src/visits.rs— everywhere the reader has been, across every tab: what the History panel lists, and anOrderand nothing else.src/bookmarks.rs— the reader's bookmarks: a saved place and the name it was made under, in the order they were added; saved inproject.toml, live only against what is loaded.src/naming.rs— a demangled name cut down to themodule::fn_namea tab is called by.src/panics.rs— a panic on any thread: the record, the file a run appends it to and every such file there is, the box the reader is shown and how much of the message and the backtrace it is given, and the shutdown after it, which one panic starts and the main thread waits for.src/fonts.rs— the desktop's font settings (KDE, Gnome, Win32) merged under the user's own.src/functions.rs— the functions a source file defines, by the lines they span, and which one a line is inside;functions/rust.rsis the scanner that finds Rust's without the grammar.src/ui.rs— the freya UI's root: its prelude, the list of its files,toolbarwith its two history buttons,app, androots— the one list of root contexts, which the headless tests are given too. A context holding one state is onecontext(Wrapper, value)line there; a bundle or a memo is aprovide; and one state is handed down rather than provided at all.src/ui/metrics.rs— every measurement no component owns, and the fonts they follow.src/ui/palette.rs— every colour, the appearance the window is drawn in, the stored choice it is resolved from, and the compositing rules.src/ui/state.rs— what the root provides and no one mechanism owns, and the bundles the whole app is passed around in. A context lives with the mechanism it belongs to (Markedinmarks.rs,Doorsinfocus.rs,Loadinginloading.rs, thePad*family inpad.rs), and so does the bundle that groups it; the rest is here,Placingamong them -- whether a code pane is a tab's or the Scratchpad's, which is what its place, its runs and its find bar are filed under.src/ui/analyzed.rs— the analysis worker: the four kinds of question, the drain that supersedes each kind on its own, the work itself, and the task that takes every answer.src/ui/studied.rs— the listing the two panes draw: the question they put, what answers it, what a pane draws while it is being worked out, which answers are kept, which place on a tab's trail the listing on screen is for, and when an instruction and a source line are the same place.src/ui/finder.rs— the file finder: the box Ctrl+P opens over the app, the files of the project's directory under it, and the one worker that walks them and picks them out. The walked files never reach the UI thread; the rows a query picked out are what cross.src/ui/focus.rs— a place in a file, the landing a click from outside the panes makes,Doors(what every door out of one place into another is given), what each tab keeps of where it was left and of its runs, and the effects that spend a landing.src/ui/scrolling.rs— keeping each pane scrolled where its place was left, and bringing a row into view: the row a tab is left at, written down as the reader scrolls and put back as the tab comes round, and the two reveals -- a click's, with the rows kept above it, and the keyboard's, which moves only for a row off screen.src/ui/follow.rs— following a name in the source to what it names: whom the question is put to, the question itself, and where its answer opens, with the caret it lands on.src/ui/linking.rs— which names in the file the Source pane is showing are links: what it has asked the server, what came back, and why the asking waits until the server is ready.src/ui/opened.rs— which files the server has been told the reader has open, which is what makes it answer about them: the set and the run it was sent to, which files a project's server is for at all, what a build rewrote under them, and the effect that sends the difference.src/ui/coded.rs— which lines of the file the Source pane is showing produced code: what the gutter marks, the objects that answer was worked out over, and the question asked again whenever either changes.src/ui/keyboard.rs— the boxes inside the tab on screen the keyboard can be in, and the ask a press on a chip makes for it to go there.src/ui/keys.rs— whether Shift, Ctrl and Alt are held, kept by the root's global key handlers because a pointer event carries no modifiers:ModifierKeys, those three and the two states a Caps Lock made into Ctrl is learnt with, made and provided in one call (provide_modifiers) so no caller can join five booleans in the wrong order; and all let go of when the window loses the focus.src/ui/chords.rs— the chords the window answers wherever the keyboard is, and the one hook every text box declines them with: freya's own default for each of its two boxes, written once, with the modifier keys let through to the root.src/ui/marks.rs— the run picked out in each pane, the pair it lights on the other side, the scroll it owes, the keyboard's moves over it, and what Ctrl+C copies. Also the one hook all three code listings wire their keyboard with, and the two ways each reads one of its rows.src/ui/glyph.rs— an icon: its SVG rasterized once per name, size and colour, and drawn unscaled on whole device pixels by an element of the app's own, since freya 0.4'simagesamples a raster at a fractional place.src/ui/highlight.rs— a source file read and parsed off the UI thread: the reader's worker thread, the cache its answers land in, what the pane draws until one does, and every line of a parse cut into what a row draws.src/ui/hovering.rs— the name the pointer is on in the source, the question the server is put about it, and what came back.src/ui/hover_view.rs— the box that draws the answer: where it goes, and when it goes.src/ui/locations.rs— every symbol a line, or the function around it, was compiled into: the question, the answer, the panel; and the three questions a name's menu offers.src/ui/reading.rs— what the worker has decoded of an object's code for the section view, and the window of it the view asks for next:Sectioned, the one bundle those, the object a listing that is no tab claims, and the rows the view built are all held in.src/ui/rescued_view.rs— the window naming the stored files that would not parse and where each was moved to.src/ui/search_view.rs— the Search panel: what was searched for, the hits as they arrive, and the one worker that finds them.src/ui/section_view.rs— the section view: an object's code as one listing, its rows, the place it keeps as an address, the window it asks for, andstretch_texts, the one statement of what a stretch's rows say, which the search that walks the code reads too.src/ui/filter_bar.rs— one filter bar, its three toggles, and the pane its list is drawn in.src/ui/find_bar.rs— find in a code pane: the bar under one, what each is asking, and the worker that searches a listing the pane holds entire.src/ui/hunt.rs— the same over an object's code, which no pane holds entire: a step reads on from where the reader is, stretch by stretch, for the next match. Which of the two a bar is, is whether it has a listing.src/ui/language.rs— whether a language server is running, what the project's own settings said, and the presses that start it, stop it and put a question to it;language/worker.rsis the blocking half -- the jobs, the answers, which of them a newer question takes the place of, and the only part that nameslsp::Server.src/ui/language_view.rs— what that server is drawn as: the control in the top bar that starts and stops it, and the band under the bar that asks before a first start.src/ui/documents.rs— what opening, closing and moving between documents means: the doors in, the three into a place among them (a file and a line, an address in an object's code, a symbol of its own); which spelling of a path a source tab is named by; the closers, and a step along a tab's trail.src/ui/entries.rs— what a document is called and drawn as wherever a list names one: the short spelling and the whole one, the glyph, and the key a row is drawn under.src/ui/loading.rs— reading binaries onto the objects list: the one path anything is ever added by, the thread each load is read on, the batches, and which load an answer belongs to.src/ui/menus.rs— the menus a right-click opens over a tab, over a file row and over a sidebar row, the items more than one menu is built of, and the pieces they are all drawn from: a row, what its text says, the mark after it, the line between two groups, and the button a menu hangs under. Each menu is built per press, in an event handler.src/ui/pages_menu.rs— the menu at the top left of the window: the ways in and out of a project, the projects there have been under one row of it, and the pages under them; and the table saying what each page is drawn as.src/ui/sidebar.rs— the three lists a binary is browsed with, and the rows each is built of.src/ui/building.rs— building the project's own workspace: what is held about it, the one worker thread, which binaries a finished build replaces, and which of the files its diagnostics name the view may open.src/ui/bookmarks_view.rs— the Bookmarks list: one row per bookmark, live against what is loaded and kept dimmed when it is not.src/ui/files_view.rs— the Files view: the project's directory as a tree, a file's row opening it as source and its menu offering it as a binary.src/ui/code_row.rs— one row of a code listing as all three listings draw theirs: the shared width, wash and pointer handlers, the one paragraph a row's text is, and the hit-tests that ask it where a pointer is and where a column is.src/ui/list_box.rs— the box a code listing is drawn in, one box for all three of them: the hooks a list opens with and the rect it closes around its rows.src/ui/assembly.rs— the assembly side of a document: the rows, the gutter, the pane.src/ui/symbol_bar.rs— the bar over that pane naming what it is drawing, and its section.src/ui/source_view.rs— the source side of one: the list of rows, which file it is showing, and the pane that decides. AlsoShowingFile, that file as the one fact the reader, the gutter's marks and the links are each asked about.src/ui/source_row.rs— one row of that list: the line as it is drawn, the names the language server placed on it, and what a press, a menu or the F12 family asks about one.src/ui/source_bar.rs— the bar over that pane naming the file, and what it says over a file the binary was not built from.src/ui/split.rs— one document drawn: which side leads, and which panes a tab has, with the control on the leading pane's bar that puts the other away. AlsoSplit, what each of the app's three resizable splits is: the number it holds across the container's unmount, the context that number is read back out of, and whether it is a percentage or pixels.src/ui/debug_view.rs— the Debug page: the panics that can be raised on purpose, so the boxsrc/panics.rsputs up is one press away rather than a patched build, and the files they left behind. In the pages menu only when Alt was held as it opened.src/ui/dock.rs— the sidebar's dock: what a panel is, every panel there is, and the groups they can be arranged in.src/ui/strip.rs— the app's own tab bar: the chips, the × on one, the list of every open tab, and the body under it all.src/ui/strip/scroll.rs— the bar's own scrolling: where every chip has been measured to, how far the row of them is slid, and every rule over the two.src/ui/project_view.rs— which project is open: the project's own fields, its binaries, the cargo build, the language server and the other projects, five sections each redrawn on its own; the chip in the top bar that opens the page, with the buttons that close, save and delete the project; the window that asks before a delete and the one that says a project would not open; andOpenProject, the project as the view's boxes hold it.src/ui/session.rs— the session as the UI keeps it in step withproject.rs: what is saved when, what a restore fills in, and what a switch empties; andsettings.tomlwired to the appearance and the fonts. Every hookapp()calls that draws nothing.src/ui/no_project.rs— the window with no project open: what is drawn under the top bar either way, and the screen that offers the ways into one.src/ui/settings_view.rs— the settings page: the theme choice and the two font overrides, andEditedSettings, the settings as the page holds them.src/ui/shortcuts_view.rs— the Shortcuts page: the gestures under the place each applies, and the box that filters them.src/ui/pad.rs— the scratchpads the app holds, which is shown, and their one worker thread.src/ui/pad_view.rs— the scratchpad's pane: pad list, editor, crates, diagnostics, output.src/ui/parts.rs— the small stateless pieces of drawing shared by unrelated panes,verdict_lineamong them: the one line aVerdictis drawn as, wherever one is. Alsoglyph, the one small icon every bar, header and row draws, andgiven: what a text box says, or nothing. Andkeyed!, which writes what a row needs to be diffed by the key it is given.src/ui/picks.rs— the row each list has picked out: what a pick is, the one per panel, the Alt that picks without opening, and which of two colours a picked row wears. Also the keyboard in a list -- the pick as its cursor, and the moves a panel answers with itsListKeys-- andopened, the one door a row's press goes through, which says whether the press took the keyboard with it.src/ui/place_row.rs— the row the Search and Locations panels both draw: a file, or one place found in it, andFolding, the one thing the panels differ in: which state a fold is written to.src/ui/place_target.rs— the place a diagnostic names, drawn as a target: the one component the Scratchpad pane and the Project view both press to get there, with the press handed in.src/ui/width.rs— the widest row a code listing has drawn, and the width every row of it takes from that: what lets the code panes scroll sideways with their wash whole.src/ui/worker.rs— how anything is asked of a thread: the named thread, the job sender, the drain policy that supersedes and the task that takes the answers, in the two shapes every worker in the app is one of.
No ui/ file carries the name of a crate module the prelude brings in: source_view,
project_view, search_view, section_view, settings_view, shortcuts_view,
bookmarks_view, files_view, filter_bar, find_bar, documents, language,
linking, pad, analyzed, building and strip are each a name beside one. A count is
left out: source_view, source_row and source_bar all stand off source, so it would
go stale with the next file. Panel is imported by name beside the glob, freya's prelude
having one of its own. The rest is in agents/UI.md.
Everything except the UI is framework-free and unit-tested rather than eyeballed. A module's
tests are a file of their own: src/<module>/tests.rs, declared #[cfg(test)] mod tests; at
the foot of src/<module>.rs, so the module a reader opens is the module and not the module plus
half again of what it is asserted to do. The path a test is named by (project::tests::…) is
unchanged, which is the point: it is where the file sits and not what the module tree looks like.
A fixture only tests use lives there too, re-exported by the module where other files need it
(Seeded, src/source/tests.rs).
No test runs cargo or rustc. A suite that builds a program to run costs a compile per test and
leaves the process behind whenever an assertion fails short of the stop, so what only a real
process could show is judged by hand instead (agents/Scratchpad.md).
A test that writes to the system temporary directory takes what it wrote with it, through
Temporary (src/temporary.rs; Scratch in crates/analysis/tests/common/mod.rs for the other
crate). A removal at the foot of the body is not enough: the usual failure is an assert! part way
down, and the lines after it never run. The guard removes on Drop, which unwinding runs. It names
the path too, so no test spells one: the process id and a count kept for the whole process make
each call's its own. /tmp is memory on many systems and the names carry the process id, so a
leak is per run rather than once.
A test writes a real file only when the filesystem is what it is about: an atomic write, a
rename, a create_new collision, a file moved aside, directory order, an unreadable directory, a
path that only reduces through canonicalize. Where the file is a fixture -- something for the
Source pane to draw -- it is seeded into the read instead, through Seeded (src/source/tests.rs),
which forgets what it seeded on Drop as Temporary removes what it wrote. analysis has the
same seam a step lower: open_data_streaming is open_files_streaming with the reading already
done, so a test hands over an archive the object writer built rather than a path it wrote it to.
Design notes
The reasoning behind the code (what was decided, what it cost, and what was measured) is in
agents/. Read the note for the area before changing it, and rewrite the paragraph a change
invalidates in the same commit: these are the record of why things are the way they are.
agents/Analysis.md— the crate: parse pipeline, data model, demangling, line info both ways, the disassembler seam, relocations, branch edges, and the never-panic testing.agents/Persistence.md— projects and their two files, the session restore,Saves, recents, andsettings.toml.agents/Process.md— starting a program and ending it: the handle, the group, the two reaps, the one list a shutdown walks, and the named threads its pipes are read on.agents/Scratchpad.md— a scratchpad as a generated cargo package, its id and name, building, running, and the view with its one worker thread.agents/UI.md— freya 0.4, the root contexts, documents and the dock, per-tab positions, opening a binary, and how the UI is tested.agents/Worker.md— the analysis worker: asks, locates, supersession, what shows meanwhile, and why reading a source file is a worker of its own.agents/Panes.md— the Source and Assembly panes: companion files,Driven, the two runs and the pair, landing, the arrow gutter, copying rows.agents/Sidebar.md— the filtered lists, the Objects tree, closing a binary, the Project view.agents/Finding.md— the file finder: the shared walk, the matcher, the list that is kept, and the overlay that is not aPopup.agents/Appearance.md— the palette, theme switching, fonts, row heights, the Settings page.agents/Lsp.md— the language server: why it is a control, the hand-rolled protocol and what rust-analyzer needs of it, the process, what an answer is about, and what a project's own settings file is read into.agents/Headless.md—freya-testingas it actually behaves, checked against its sources.
What the app does, the rules a finished feature follows, is in notes/specs/, one file per
area with a section per feature, moved there from notes/Goals.md once the goal is done. A spec
says what and an agents/ note says why; neither repeats the other, and a spec is only written
or changed on the user's say-so (notes/specs/README.md).
Bugs and gaps in dependencies are in notes/upstream/, one file per crate: what was hit, what
it cost here, and whether it was reported. Add the note with the workaround. Each file may end
with a ## Wanted section for features the crate lacks and the app substitutes for: add the
feature there with the substitute, so a release that brings it is noticed.
Rules that hold everywhere
- Never panic on any file input. Checked arithmetic in preference to a wider
catch_unwind; the guard is for a dependency's bug, never for ours. A stack overflow aborts and cannot be caught, so anything recursing over file-controlled input is bounded before the call. Never reimplement part of a parsing crate (object,gimli,addr2line,pdb2, …) to get around its overflow or panic. Catch it with the guard, check the input before the call, or lose what the crate cannot read; then write it up innotes/upstream/with a minimal reproduction, so it is fixed upstream and not here. A guard finer than the seam's is for what a real toolchain emits. A crate bug only a corrupt or mutated file reaches is left to the net around the whole question: losing that file's debug info is fine. Add a narrower guard only where real compiler, assembler or linker output hits the bug. - Nothing is analysed on the UI thread: a parse, a decode or a search is a worker's, and
its answer is held rather than worked out again in a render. The shape a drawn row wants of
an answer is made with the answer, on the same thread -- every line of a parse cut into the
pieces a row draws (
Highlighted::text,src/ui/highlight.rs) -- so a render is a lookup and memoizes nothing. What is held is keyed by what it answers for, never a single slot:HIGHLIGHTEDby path, where a state field holding one file would blank every file already read on a tab switch. - A document is a place in a binary or a file; everything else is a view.
open_document,raise,navigate,close_tab,close_othersandclose_binaryare the only six functions that open or close a document tab, or change what one shows. A page's chip goes in and comes out on its own, throughshow_pageandclose_page: a page draws state held at the root, so it has no trail to keep in step and closing one loses nothing. Neitherraise_tabnorcloseis a seventh: they are what a caller holding aTabrather than an id goes through --raiseisraise_tabgiven a document's tab, andclosethe one match from aTabonto whichever of the two closes it belongs to. - Identity in the UI is
Arcpointer identity, never names or indices: list keys areArc::as_ptr(..).addr()and propPartialEqs are hand-written withArc::ptr_eq. A list of rows is aShared(src/shared.rs), which is that rule written once; an optionalArcissame_arcbeside it, and one handed to a hook that keys onPartialEqisByPtr. - Asking for a colour or a font is what subscribes a scope to it (
palette(),fonts());set_appearanceandset_fontsare the only writers. Never write a literal colour or row height. - Persisted formats need no backward compatibility yet: a stale file is ignored, not migrated.
Gotchas before editing the UI
- A
State'speek/readhands back a guard, and anif letholds its scrutinee's temporary until the end of its body, soif let Some(x) = *state.peek() { state.set(..) }compiles and panics the moment it runs.let ... elseandmatchend theirs with the statement. Bind the read to aletof its own before any write. That class of bug is invisible to every other test in the repo; the headless tests insrc/ui/tests.rscatch it. - A hook may only be called while a component is rendering, and the same ones every time.
use_consumeis a hook, which makes reaching for a context one -- so a plain function that consumes a context cannot be called from an event handler, from a task, or from inside another hook's closure. Doing it takes slots in whatever scope is rendering, and nothing fails there: the next render of that scope reads a hook back as the wrong type and panics somewhere else entirely, with a message about conditional hooks and a backtrace pointing at the innocent hook after the guilty one. Worse, a function whose hooks are behind anif letshifts the order by however many keys the data happened to have, so it is a crash that only some readers see. Consume in the component and hand the states down (Arrangement,src/ui/state.rs). - There is no
.hover()pseudo-state. A hoverable row is aComponentwithuse_state(|| false)pluson_pointer_over/on_pointer_out(over/out, notenter/leave, so hovering a child keeps the highlight). VirtualScrollView's builder closure is never compared across renders, so anything the rows depend on must go throughnew_with_data, not be captured.- A row's height must equal the
item_sizegiven to theVirtualScrollViewover it, or scrolling misaligns. There are two of them,list_row_height()for rows in the interface font andcode_row_height()for rows in the fixed-width one, so a view and its rows have to agree about which, as well as about the number. Both are functions of the fonts and no longer aconst, so never write a literal row height anywhere; the two halves are safe only because both are read in the same render pass. This is also why variable-height rows are not free. Sizehas noFrom<f32>; writeSize::px(300.). But.padding,.spacing,.marginand.corner_radiusdo take plainf32.label()andparagraph()do not implementStyleExt, so they have no.background()/.border(); wrap them in arect().spawnties a task to the scope it was called in, and a task whose scope is unmounted is dropped, before its first poll if that comes first. A handler on something the handler itself takes down (a context menu's item, which the press closes) has tospawn_forever.- A
VirtualScrollViewscrolls sideways only as far as the widest row it has built, so a row that should be reachable sideways is neverwidth(Size::fill()). It is notwidth(Size::auto())with amin_widtheither: torin sizes an auto-width node from its minimum plus its children. The code panes' rows takeWidest::row_width(src/ui/width.rs) and report their content throughon_sized'sinner_sizes. - An
EventHandlerprop never compares equal, nor does aCallback, so a component holding one re-renders whenever its parent does; that is whyPlaceTargetis one label and one hover flag (src/ui/place_target.rs). ANoArgCallbackprop always compares equal, which is the opposite failure and the worse one: the component is never re-rendered and goes on calling the closure it mounted with. The app uses none. - Two
Writables compare equal, always, so a component holding one mapped by a key is never handed a new map: it goes on reading whatever the key said when it mounted. Anything passing one down is keyed by that key, so a change remounts it (src/ui/pad_view.rs). - Every event one press produces -- the targeted press and every listener's global press --
is emitted against the tree measured before any of them ran, with no render between. So a
handler that takes something away is followed, in that same batch, by handlers on nodes
the next render will unmount, and a
Writablemapped by a key is read after the render that guarded it.PadBuffers's index is total for this reason (src/ui/pad.rs). - A bubbling pointer event (
pointer_down,press) is measured once against the deepest listener and every ancestor's handler gets the same data, soelement_location()in an ancestor is relative to that child. Nothing inside a code row listens topointer_downfor this reason (src/ui/code_row.rs). Andpointer_overfires on entry only, whatever its doc string says; a sweep that follows the pointer ispointer_move.
Testing the UI
freya-testing runs the whole app headless on the test's own thread. The binary's suite runs in
under two seconds, so a test written to settle one point costs less than a cargo run and a
look. It can be asked about any control, drag, scroll, keyboard binding, laid-out size, worker
answer, or which component re-rendered, and, through a counter, how many times the
thread that draws did something it should not have -- read a file (source::touches),
copy a whole answer (grouped::copies), draw a row again (ui::pad_view::rows_drawn).
A counter is one counter! call (src/main.rs) and one #[cfg(test)] bump where the
thing is done; there are nineteen. It cannot say how anything looks, measure text, or
observe the platform. Keep the tests that pin a mechanism and delete the ones that only proved
the code just written does what it says. A headless test has to be made to fail first on the
mechanism it claims to test. The rest is in agents/UI.md and agents/Headless.md.
