facett-git 0.1.11

facett — git repository facet: browse branches, commit log, the file tree, and syntax-coloured source, all clickable. gix-backed (optional feature), egui-rendered. A consumer (nornir) drops it in to show git for any repo.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
//! **`facett-git`** — a facett [`Facet`](facett_core::Facet) that shows a git
//! repository: its **branches**, the **commit log**, the **file tree**, and
//! **syntax-coloured source** — all clickable. A consumer (e.g. nornir's viz) drops
//! it into a `FacetDeck` to surface git for any repo.
//!
//! - The repository data is a pure [`RepoSnapshot`] (branches / commits / tree). With
//!   the **`gix` feature** it is read live from disk via
//!   [`RepoSnapshot::open`](model::RepoSnapshot::open); without it the host supplies
//!   a snapshot (and a blob), so the facet builds and tests with no git dependency.
//! - Source is rendered with a deterministic Rust [`highlight`]er into an egui
//!   `LayoutJob` (egui *can* render coloured Rust text — this is how).
//! - Everything observable (selected branch, open path, counts) is in
//!   [`state_json`](GitView::state_json) (LAW 6 / FC-6), so it is headless-testable.
//!
//! ## FC contract (the canonical Elm split, FC-2 / FC-9)
//! - **[`GitState`]** — the complete observable, serializable, round-trippable state
//!   (the repo snapshot, the open blob, the selected branch / path, the visible pane,
//!   the zoom). [`GitView::state`] hands back a `&GitState`.
//! - **[`Msg`] + [`GitView::update`]** — the single mutation path (FC-2): the same
//!   gestures the canvas clicks drive (pick a branch, open a tree path, switch pane,
//!   zoom, load a blob), so a headless driver ([`facett_core::harness`]) reaches them.
//! - **[`GitView::view`]** — a **pure** paint (FC-9): it reads `&self`, paints the
//!   three columns, and *returns* the [`Msg`]s the interactions produced; the
//!   [`impl_facet_via_elm!`](facett_core::impl_facet_via_elm) bridge applies them.
//! - **Rich `state_json`** — the derived counts (`branches` / `commits` /
//!   `tree_entries`), the head branch, and the flattened selection a driver reads are
//!   *derived* from the snapshot, not a plain `serde(state())`, so the macro is
//!   invoked in **form 3** (`custom_state_json`) to publish those keys.

pub mod highlight;
pub mod model;

pub use highlight::{highlight, Kind, Span};
pub use model::{Blob, Branch, CommitRow, RepoSnapshot, TreeEntry};

use egui::{text::LayoutJob, Color32, FontId, TextFormat, Ui};
use facett_core::clip::{ClipKind, ClipPayload, CopySource};
use serde::{Deserialize, Serialize};

/// Which middle-pane view is showing. Part of the observable [`GitState`], so it is
/// serializable; the string form the deck reads (`"tree"` / `"log"`) is emitted by
/// [`GitView::state_json`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Pane {
    Tree,
    Log,
}

/// A robot-/CLI-addressable control message (FC-2) — the named boundary a headless
/// driver (or a host) drives the git browser through, the same effect the canvas
/// clicks produce. Applied by [`GitView::update`]; it is the **only** mutation path.
#[derive(Clone, Debug, PartialEq)]
pub enum Msg {
    /// Select a branch by its **name** (FC-5: a stable id, never a row index) — the
    /// branch-list click.
    SelectBranch(String),
    /// Open a tree path by its **name** (FC-5: a stable id) — the tree-entry click.
    SelectPath(String),
    /// Switch the middle pane between the tree and the commit log — the tab click.
    ShowPane(Pane),
    /// Set the source zoom (clamped to `0.5..=4.0`) — the host `set_scale`.
    SetScale(f32),
    /// Replace the open source blob (and select its path) — the host `set_blob` /
    /// the `gix` file-open path.
    SetBlob(Blob),
}

/// Side work as data (FC-8). The git browser does no I/O — every [`Msg`] mutates
/// only the in-memory [`GitState`] — so this is uninhabited on purpose: the
/// type-checked statement that [`GitView::update`] never asks the host to do
/// anything. (Live repo reads happen up-front in [`RepoSnapshot::open`], not here.)
#[derive(Clone, Debug, PartialEq)]
pub enum Effect {}

/// **The complete observable state (FC-1 / FC-3)** of a [`GitView`], in one
/// serializable, round-trippable struct: the repo snapshot, the open source blob,
/// the selected branch / path (keyed on stable **names**, FC-5), the visible pane,
/// and the source zoom. [`GitView::state`] hands back a `&GitState`; a headless
/// driver ([`facett_core::harness`]) snapshots it after feeding a `Vec<Msg>`.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct GitState {
    /// The repository snapshot the facet renders (branches / commits / tree).
    pub snap: RepoSnapshot,
    /// The open source blob (set by the host, or by `gix` when a file is clicked).
    pub blob: Option<Blob>,
    /// The selected branch name (FC-5: keyed on the name, not the row index).
    pub selected_branch: Option<String>,
    /// The open tree path (FC-5: keyed on the entry name).
    pub selected_path: Option<String>,
    /// Which middle pane is showing (tree / log).
    pub pane: Pane,
    /// Source zoom factor (`0.5..=4.0`).
    pub scale: f32,
}

/// The git repository facet.
pub struct GitView {
    title: String,
    /// All observable state (FC-3).
    state: GitState,
}

impl GitView {
    /// A facet over an in-memory [`RepoSnapshot`] (host-supplied data path).
    #[must_use]
    pub fn new(snap: RepoSnapshot) -> Self {
        let selected_branch = snap.head_branch().map(str::to_string);
        Self {
            title: "Git".to_string(),
            state: GitState {
                snap,
                blob: None,
                selected_branch,
                selected_path: None,
                pane: Pane::Tree,
                scale: 1.0,
            },
        }
    }

    /// A demo facet (no git needed) — handy for examples/snapshot tests.
    #[must_use]
    pub fn demo() -> Self {
        Self::new(RepoSnapshot::demo())
    }

    /// Open a repo live (requires the `gix` feature). `max_commits` caps the log.
    #[cfg(feature = "gix")]
    pub fn open(path: &str, max_commits: usize) -> Result<Self, String> {
        Ok(Self::new(RepoSnapshot::open(path, max_commits)?))
    }

    /// **FC-3** — read the complete observable state at any frame boundary.
    #[must_use]
    pub fn state(&self) -> &GitState {
        &self.state
    }

    /// **FC-2 / FC-8** — the single mutation path. Apply one [`Msg`]; returns the
    /// (always empty) [`Effect`]s the host should run. Every canvas click routes its
    /// [`Msg`] here too, so live == headless.
    pub fn update(&mut self, msg: Msg) -> Vec<Effect> {
        match msg {
            Msg::SelectBranch(name) => self.state.selected_branch = Some(name),
            Msg::SelectPath(path) => self.state.selected_path = Some(path),
            Msg::ShowPane(pane) => self.state.pane = pane,
            Msg::SetScale(scale) => self.state.scale = scale.clamp(0.5, 4.0),
            Msg::SetBlob(blob) => {
                self.state.selected_path = Some(blob.path.clone());
                self.state.blob = Some(blob);
            }
        }
        Vec::new()
    }

    /// Replace the open source blob (the source pane content). A thin wrapper over
    /// the FC-2 [`update`](Self::update) mutation path.
    pub fn set_blob(&mut self, blob: Blob) {
        let _ = self.update(Msg::SetBlob(blob));
    }

    /// The current snapshot (read-only).
    #[must_use]
    pub fn snapshot(&self) -> &RepoSnapshot {
        &self.state.snap
    }

    /// The currently selected branch name.
    #[must_use]
    pub fn selected_branch(&self) -> Option<&str> {
        self.state.selected_branch.as_deref()
    }

    /// Build a syntax-coloured `LayoutJob` for Rust `src` under `theme`. Public so a
    /// host can render code with the same colours outside the facet.
    #[must_use]
    pub fn highlight_job(src: &str, theme: &Theme, font: FontId) -> LayoutJob {
        let mut job = LayoutJob::default();
        for span in highlight(src) {
            let fmt = TextFormat { font_id: font.clone(), color: theme.color(span.kind), ..Default::default() };
            job.append(&src[span.start..span.end], 0.0, fmt);
        }
        job
    }
}

/// Colours for each syntax [`Kind`]. [`Default`] is a dark theme.
#[derive(Debug, Clone, Copy)]
pub struct Theme {
    pub text: Color32,
    pub keyword: Color32,
    pub ty: Color32,
    pub string: Color32,
    pub number: Color32,
    pub comment: Color32,
    pub attribute: Color32,
    pub punct: Color32,
}

impl Default for Theme {
    fn default() -> Self {
        Self {
            text: Color32::from_rgb(0xD4, 0xD4, 0xD4),
            keyword: Color32::from_rgb(0x56, 0x9C, 0xD6),
            ty: Color32::from_rgb(0x4E, 0xC9, 0xB0),
            string: Color32::from_rgb(0xCE, 0x91, 0x78),
            number: Color32::from_rgb(0xB5, 0xCE, 0xA8),
            comment: Color32::from_rgb(0x6A, 0x99, 0x55),
            attribute: Color32::from_rgb(0xDC, 0xDC, 0xAA),
            punct: Color32::from_rgb(0xD4, 0xD4, 0xD4),
        }
    }
}

impl Theme {
    /// The colour for a syntax kind.
    #[must_use]
    pub fn color(&self, kind: Kind) -> Color32 {
        match kind {
            Kind::Text => self.text,
            Kind::Keyword => self.keyword,
            Kind::Type => self.ty,
            Kind::Str => self.string,
            Kind::Number => self.number,
            Kind::Comment => self.comment,
            Kind::Attribute => self.attribute,
            Kind::Punct => self.punct,
        }
    }
}

impl GitView {
    /// The copyable text: the selected file path if one is open, else the selected
    /// branch name + tip, else the commit log as a TSV rectangle
    /// (`id \t summary \t author \t time`).
    pub fn copy_text(&self) -> Option<String> {
        if let Some(path) = self.state.selected_path.as_deref() {
            return Some(path.to_string());
        }
        if let Some(b) =
            self.state.selected_branch.as_deref().and_then(|n| self.state.snap.branches.iter().find(|b| b.name == n))
        {
            return Some(format!("{} ({})", b.name, b.tip));
        }
        if self.state.snap.commits.is_empty() {
            return None;
        }
        let mut out = String::from("id\tsummary\tauthor\ttime");
        for c in &self.state.snap.commits {
            out.push('\n');
            out.push_str(&format!("{}\t{}\t{}\t{}", c.id, c.summary, c.author, c.time));
        }
        Some(out)
    }
}

// ── typed copy (§16) — read-only git browser: path / branch / commit-log rows ──
impl CopySource for GitView {
    fn copy_kinds(&self) -> &[ClipKind] {
        &[ClipKind::Text]
    }

    fn copy_payload(&self) -> Option<ClipPayload> {
        self.copy_text().map(ClipPayload::Text)
    }
}

impl GitView {
    /// **FC-9 render** — a **pure** function of `&self`: it paints the three columns
    /// (branches / tree-or-log / source) and *returns* the [`Msg`]s the interactions
    /// produced. It MUST NOT mutate the [`GitState`] or do I/O; the
    /// [`impl_facet_via_elm!`](facett_core::impl_facet_via_elm) bridge applies the
    /// returned messages through [`update`](Self::update).
    pub fn view(&self, ui: &mut Ui) -> Vec<Msg> {
        let mut msgs: Vec<Msg> = Vec::new();
        let theme = Theme::default();
        ui.horizontal(|ui| {
            // ── branches column ──
            ui.vertical(|ui| {
                ui.strong("Branches");
                for b in &self.state.snap.branches {
                    let label = if b.is_head { format!("{} ({})", b.name, b.tip) } else { format!("  {} ({})", b.name, b.tip) };
                    let sel = self.state.selected_branch.as_deref() == Some(b.name.as_str());
                    if ui.selectable_label(sel, label).clicked() {
                        msgs.push(Msg::SelectBranch(b.name.clone()));
                    }
                }
            });
            ui.separator();
            // ── tree / log column ──
            ui.vertical(|ui| {
                ui.horizontal(|ui| {
                    if ui.selectable_label(self.state.pane == Pane::Tree, "Tree").clicked() {
                        msgs.push(Msg::ShowPane(Pane::Tree));
                    }
                    if ui.selectable_label(self.state.pane == Pane::Log, "Log").clicked() {
                        msgs.push(Msg::ShowPane(Pane::Log));
                    }
                });
                match self.state.pane {
                    Pane::Tree => {
                        for e in &self.state.snap.tree {
                            let icon = if e.is_dir { "📁" } else { "📄" };
                            let sel = self.state.selected_path.as_deref() == Some(e.name.as_str());
                            if ui.selectable_label(sel, format!("{icon} {}", e.name)).clicked() {
                                msgs.push(Msg::SelectPath(e.name.clone()));
                            }
                        }
                    }
                    Pane::Log => {
                        for c in &self.state.snap.commits {
                            ui.label(format!("{} {}{}", c.id, c.summary, c.author));
                        }
                    }
                }
            });
            ui.separator();
            // ── source column ──
            ui.vertical(|ui| {
                ui.strong(self.state.selected_path.as_deref().unwrap_or("(no file)"));
                if let Some(blob) = &self.state.blob {
                    if blob.binary {
                        ui.weak("binary file");
                    } else {
                        let job = GitView::highlight_job(&blob.text, &theme, FontId::monospace(12.0 * self.state.scale));
                        ui.label(job);
                    }
                }
            });
        });

        #[cfg(feature = "testmatrix")]
        facett_core::testmatrix::emit(
            "facett-git::GitView::view",
            "ui_render",
            true,
            &format!(
                "branches={} commits={} tree={} pane={}",
                self.state.snap.branches.len(),
                self.state.snap.commits.len(),
                self.state.snap.tree.len(),
                match self.state.pane {
                    Pane::Tree => "tree",
                    Pane::Log => "log",
                },
            ),
        );

        msgs
    }
}

// ── FC-2 / FC-3 / FC-8 / FC-9: the canonical Elm split ────────────────────────
impl facett_core::Elm for GitView {
    type Model = GitState;
    type Msg = Msg;
    type Effect = Effect;

    fn title(&self) -> &str {
        &self.title
    }
    fn state(&self) -> &GitState {
        &self.state
    }
    fn update(&mut self, msg: Msg) -> Vec<Effect> {
        GitView::update(self, msg)
    }
    fn view(&self, ui: &mut Ui) -> Vec<Msg> {
        GitView::view(self, ui)
    }
}

// The bridge macro writes `impl Facet for GitView` from the `Elm` impl: `title`, the
// FC-9 `ui` loop (`for m in view(ui) { update(m) }`), plus the extra overrides below.
// **Form 3** (`custom_state_json`) because the git browser publishes a RICHER
// `state_json` than a plain `serde(state())`: the derived counts (`branches` /
// `commits` / `tree_entries`), the resolved `head` branch, and the flattened
// selection are computed from the snapshot, not dumped from the raw Model — so the
// default `state_json` is suppressed and this one supplied (keys preserved exactly).
facett_core::impl_facet_via_elm!(GitView, custom_state_json, {
    fn copy(&mut self) -> Option<String> {
        self.copy_payload().map(|p| p.as_text())
    }

    fn state_json(&self) -> serde_json::Value {
        serde_json::json!({
            "title": self.title,
            "workdir": self.state.snap.workdir,
            "branches": self.state.snap.branches.len(),
            "head": self.state.snap.head_branch(),
            "commits": self.state.snap.commits.len(),
            "tree_entries": self.state.snap.tree.len(),
            "pane": match self.state.pane { Pane::Tree => "tree", Pane::Log => "log" },
            "selected_branch": self.state.selected_branch,
            "selected_path": self.state.selected_path,
            "open_blob": self.state.blob.as_ref().map(|b| &b.path),
            "scale": self.state.scale,
        })
    }

    fn caps(&self) -> facett_core::FacetCaps {
        facett_core::FacetCaps::NONE.scalable().selectable().themeable().copyable()
    }

    fn scale(&self) -> f32 {
        self.state.scale
    }
    fn set_scale(&mut self, scale: f32) {
        let _ = self.update(Msg::SetScale(scale));
    }

    fn selection_json(&self) -> serde_json::Value {
        serde_json::json!({ "branch": self.state.selected_branch, "path": self.state.selected_path })
    }
});

#[cfg(test)]
mod tests {
    use super::*;
    // `Facet` for `state_json`/`selection_json`/`scale`/`set_scale`/`caps`/`title`.
    use facett_core::Facet;

    #[test]
    fn typed_copy_prefers_path_then_branch_then_commit_log() {
        use facett_core::clip::{ClipKind, CopySource};
        let mut v = GitView::demo();
        // Head branch is pre-selected -> copy is the branch name + tip.
        let p = v.copy_payload().expect("a demo repo copies");
        assert_eq!(p.kind(), ClipKind::Text);
        let head = v.selected_branch().unwrap().to_string();
        assert!(p.as_text().starts_with(&head), "branch copy: {}", p.as_text());
        // Open a blob -> copy prefers the selected file path.
        v.set_blob(Blob { path: "src/main.rs".into(), text: "fn main() {}".into(), binary: false });
        assert_eq!(v.copy_payload().unwrap().as_text(), "src/main.rs");
    }

    /// INJECT-ASSERT: state_json exposes every visible count + the selection (LAW 6
    /// headless surface), and the head branch is pre-selected.
    #[test]
    fn state_json_exposes_counts_and_selection() {
        let v = GitView::demo();
        let s = v.state_json();
        assert_eq!(s["branches"], 2);
        assert_eq!(s["commits"], 2);
        assert_eq!(s["tree_entries"], 3);
        assert_eq!(s["head"], "main");
        assert_eq!(s["selected_branch"], "main");
        assert_eq!(s["pane"], "tree");
    }

    /// INJECT-ASSERT: setting a blob updates the open path + state, and scale clamps.
    #[test]
    fn set_blob_and_scale_clamp() {
        let mut v = GitView::demo();
        v.set_blob(Blob { path: "src/lib.rs".into(), text: "fn x() {}".into(), binary: false });
        assert_eq!(v.state_json()["open_blob"], "src/lib.rs");
        assert_eq!(v.selection_json()["path"], "src/lib.rs");
        v.set_scale(99.0);
        assert_eq!(v.scale(), 4.0, "scale clamps to the cap");
        v.set_scale(0.01);
        assert_eq!(v.scale(), 0.5);
    }

    /// INJECT-ASSERT: the highlight job carries one coloured section per span and
    /// the keyword colour differs from plain text (proves colouring is applied).
    #[test]
    fn highlight_job_colours_spans() {
        let theme = Theme::default();
        let job = GitView::highlight_job("fn main() {}", &theme, FontId::monospace(12.0));
        assert!(!job.sections.is_empty());
        // The 'fn' keyword section is the keyword colour, not the default text colour.
        let kw = job.sections.iter().find(|s| job.text[s.byte_range.clone()].starts_with("fn")).unwrap();
        assert_eq!(kw.format.color, theme.keyword);
        assert_ne!(theme.keyword, theme.text);
    }

    /// INJECT-ASSERT: caps advertise scalable + selectable + themeable.
    #[test]
    fn caps_advertised() {
        let c = GitView::demo().caps();
        assert!(c.scalable && c.selectable && c.themeable);
    }

    // ── FC-2 → FC-3 as a *headless* property: feed a `Vec<Msg>` through the core
    //    harness and assert the resulting `GitState` — no egui, no GPU. ────────────

    #[test]
    fn harness_snapshot_drives_selection_and_pane() {
        use facett_core::harness;
        let mut v = GitView::demo();
        // Pick a branch, open a path, switch to the log pane — all observable.
        let snap = harness::snapshot(
            &mut v,
            [
                Msg::SelectBranch("feature/x".into()),
                Msg::SelectPath("Cargo.toml".into()),
                Msg::ShowPane(Pane::Log),
            ],
        );
        assert_eq!(snap.selected_branch.as_deref(), Some("feature/x"), "branch selection is observable headlessly");
        assert_eq!(snap.selected_path.as_deref(), Some("Cargo.toml"), "path selection (FC-5: keyed on name)");
        assert_eq!(snap.pane, Pane::Log, "pane switch is observable");
        assert_eq!(&snap, v.state(), "the snapshot is a clone of the live state");
    }

    #[test]
    fn drive_reports_no_effects_and_state_round_trips() {
        use facett_core::harness;
        let mut v = GitView::demo();
        // FC-8: the git browser does no I/O, so the effect stream is always empty.
        let effects = harness::drive(
            &mut v,
            [
                Msg::SelectBranch("feature/x".into()),
                Msg::SetBlob(Blob { path: "src/main.rs".into(), text: "fn main() {}".into(), binary: false }),
                Msg::SetScale(2.0),
            ],
        );
        assert!(effects.is_empty(), "FC-8: git emits no Effects");
        // The blob load flows through update (open_blob + selected_path both set).
        assert_eq!(v.state_json()["open_blob"], "src/main.rs");
        assert_eq!(v.state_json()["selected_path"], "src/main.rs");
        assert_eq!(v.state().scale, 2.0);
        // FC-3: the observable Model round-trips through serde.
        let json = serde_json::to_value(v.state()).unwrap();
        let back: GitState = serde_json::from_value(json).unwrap();
        assert_eq!(&back, v.state(), "serde(state) -> state round-trips");
    }

    /// The bridge macro's `state_json` still exposes the exact rich keys the deck
    /// reads, and a headless render draws + reports them (LAW 6).
    #[test]
    fn headless_render_reports_rich_state() {
        use facett_core::harness;
        let mut v = GitView::demo();
        v.set_blob(Blob { path: "src/lib.rs".into(), text: "fn x() -> u32 { 7 }".into(), binary: false });
        let r = harness::headless_render(&mut v);
        assert_eq!(r.title, "Git");
        assert!(r.drew(), "the three columns tessellate to vertices");
        assert_eq!(r.state["branches"], 2);
        assert_eq!(r.state["open_blob"], "src/lib.rs");
        assert_eq!(r.state["pane"], "tree");
    }
}