Skip to main content

strop_engine/editor/
mod.rs

1//! The editor state: modes, pending keys, named registers, buffers.
2//! One input path for TUI and headless — both call `feed`.
3//!
4//! Mode handlers live beside this file: `normal`, `visual`, `insert`.
5
6mod blame;
7pub mod block;
8#[cfg(test)]
9pub mod conformance;
10#[cfg(test)]
11pub mod contract_probes;
12pub mod trace;
13
14pub(crate) mod analysis;
15mod changes;
16mod collections;
17mod containers;
18mod cursor;
19mod diagnostics;
20mod dispatch;
21pub mod resolution;
22pub use diagnostics::DocumentDiagnostics;
23mod dive;
24mod document;
25pub mod events;
26mod explain;
27mod git;
28mod git_memory;
29mod help;
30mod input;
31mod insert;
32pub mod io;
33mod jumps;
34pub mod keys;
35mod lsp;
36pub mod macros;
37#[cfg(test)]
38mod multicursor_tests;
39pub(crate) mod normal;
40mod panes;
41pub mod pending;
42mod permalink;
43mod picker;
44mod registers;
45pub mod remote;
46mod remote_completion;
47mod shell;
48pub mod transact;
49mod undo;
50pub mod view;
51mod visual;
52mod workspaces;
53
54pub use document::Document;
55pub use document::{DiffRow, DocumentSource, RemoteDirectory, RemoteDocument, Surface};
56pub use git_memory::{git_channel, BlameGutter, GitJob};
57pub use git_memory::{CommitFiles, PreparedDiff, PreparedFiles, Sidebar, SidebarRow};
58pub use panes::{LayoutDir, Pane};
59pub use picker::{PickerGlue, PreviewKey, PreviewResult, PreviewSource, Previews};
60pub use registers::{ClipboardKey, ClipboardResult, Register};
61pub use shell::{ShellIntent, ShellKey, ShellResult};
62
63pub use containers::{ContainerKey, ContainerResult};
64pub use lsp::attach::AttachRecord;
65pub use lsp::LspServer;
66pub use permalink::PendingPermalink;
67pub use picker::ranking::Event as RankingEvent;
68pub use remote::{RemoteEvent, RemoteView};
69pub use remote_completion::{RemoteCompletionKey, RemoteCompletionResult};
70pub use resolution::{ResolutionEvent, ResolutionState};
71use std::collections::HashMap;
72use std::path::PathBuf;
73use std::time::Duration;
74pub use workspaces::WorkspaceRegistry;
75
76use strop_core::{Buffer, Range};
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum Mode {
80    Normal,
81    Insert,
82    Visual,
83    VisualLine,
84    /// ctrl-v: the rectangle selection (0017).
85    VisualBlock,
86}
87
88impl Mode {
89    pub fn chip(self) -> &'static str {
90        match self {
91            Mode::Normal => "NORMAL",
92            Mode::Insert => "INSERT",
93            Mode::Visual => "VISUAL",
94            Mode::VisualLine => "V-LINE",
95            Mode::VisualBlock => "V-BLOCK",
96        }
97    }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
101pub enum Key {
102    Char(char),
103    Esc,
104    Enter,
105    Backspace,
106    Up,
107    Down,
108    Left,
109    Right,
110    Tab,
111    Backtab,
112    /// Replace picker: exclude the row's whole file (vscode's toggle).
113    CtrlD,
114    CtrlR,
115    /// vim's jump-back (ctrl-i forward is Tab in a terminal).
116    CtrlO,
117    CtrlW,
118    /// Replace picker: exclude/include the selected match (0007 §2).
119    CtrlX,
120    /// vim ctrl-u/ctrl-f/ctrl-b: half/full page up.
121    CtrlU,
122    CtrlF,
123    CtrlB,
124    /// vim ctrl-^: alternate buffer.
125    CtrlCaret,
126    /// vim ctrl-v: visual block mode.
127    CtrlV,
128    /// vim ctrl-l: force a full terminal repaint (desync recovery).
129    CtrlL,
130}
131
132pub const FLASH_FOR: Duration = Duration::from_millis(280);
133
134/// The injected frame renderer's shape (0046): editor, columns, rows,
135/// record-action flag — the binary's implementation renders via TestBackend.
136pub type FrameDraw = fn(&mut Editor, u16, u16, bool) -> std::io::Result<()>;
137
138pub struct Editor {
139    pub docs: strop_core::id::Arena<strop_core::id::DocumentKind, Document>,
140    pub(crate) trace_documents:
141        HashMap<strop_core::id::DocumentId, strop_core::diagnostics::BufferTraceId>,
142    pub io: io::IoState,
143    pub(crate) remote: remote::RemoteState,
144    pub(crate) remote_completion: remote_completion::RemoteCompletionState,
145    pub(crate) worker_ids: strop_core::worker::WorkerIds,
146    pub(crate) worker_handles:
147        HashMap<strop_core::worker::WorkerId, strop_core::worker::CancelHandle>,
148    pub(crate) focus_epoch: u64,
149    pub(crate) finishing: bool,
150    pub tape: std::rc::Rc<strop_trace::replay::Tape>,
151    pub git_view: strop_core::worker::WorkerId,
152    pub git_discovery: strop_core::worker::Load<git_memory::ContextKey>,
153    pub hunk_load: strop_core::worker::Load<git_memory::HunkKey>,
154    pub hunks_untracked: bool,
155    pub log_requests:
156        HashMap<strop_core::id::DocumentId, strop_core::worker::Ticket<git_memory::LogKey>>,
157    pub card_request: Option<strop_core::worker::Ticket<git_memory::CardKey>>,
158    pub dive_requests:
159        HashMap<strop_core::id::DocumentId, strop_core::worker::Ticket<git_memory::DiveKey>>,
160    pub git_mutations: std::collections::VecDeque<git_memory::GitMutation>,
161    pub git_mutation: Option<strop_core::worker::Ticket<git_memory::MutationKey>>,
162    /// vim's jumplist (ctrl-o/ctrl-i): past/future stacks of
163    /// (document, byte offset) (jumps.rs).
164    pub jumplist_past: Vec<(strop_core::id::DocumentId, usize)>,
165    pub jumplist_future: Vec<(strop_core::id::DocumentId, usize)>,
166    pub mode: Mode,
167    pub pending: pending::PendingInput,
168    /// The input walker (0008 stage 2): typed parser state for
169    /// counts/registers/operators/prefixes — pending stays for the
170    /// free-text lines only.
171    pub walker: input::Walker,
172    /// `Space u` browser state (editor/undo.rs); None when closed.
173    pub undo_browser: Option<undo::UndoBrowser>,
174    /// Last `f/F/t/T` find: (char, backward, till). `;` and `,` replay it.
175    pub last_find: Option<(char, bool, bool)>,
176    /// Armed by `/`/`?`/`*`/`#` searches. `n`/`N` replay it; the render
177    /// highlights matches persistently (rootle: current match underlined).
178    pub last_search: Option<LastSearch>,
179    pub registers: HashMap<char, Register>,
180    /// Marks: char → (document, byte offset). `m{a}` sets, `'{a}` jumps.
181    pub marks: HashMap<char, (strop_core::id::DocumentId, usize)>,
182    pub flash: Option<(Range, strop_trace::replay::Tick)>,
183    pub message: String,
184    pub should_quit: bool,
185    /// ctrl-c is armed after the first warn (0015 quit policy).
186    pub ctrl_c_armed: bool,
187    /// Last visual range for `gv` (recorded per visual-mode key).
188    pub last_visual: Option<(usize, usize)>,
189    /// Where the last insert session was for `gi`.
190    pub last_insert_pos: Option<usize>,
191    /// g;/g, walk: (index, history depth it was taken at) — a new
192    /// commit invalidates the walk.
193    pub change_idx: Option<(usize, usize)>,
194    /// Text area height in rows — the render loop feeds it via
195    /// scroll_to_cursor; viewport motions read it.
196    pub view_rows: usize,
197    /// Macro recording (0016): the register being recorded into.
198    pub recording: Option<char>,
199    /// The app event channel (0018): set by connect_events; late LSP
200    /// attaches forward through it.
201    pub app_tx: Option<events::EventSender>,
202    pub(crate) lsp_state: lsp::state::LspState,
203    /// Recorded macros: register → key events.
204    pub macros: std::collections::HashMap<char, Vec<Key>>,
205    /// The last replayed macro register (@@).
206    pub last_macro: Option<char>,
207    /// The rows and cell edge owned by an ongoing block insert/change.
208    pub(crate) block_insert_state: Option<block::BlockInsertState>,
209    /// Macro self-replay depth guard.
210    pub macro_depth: usize,
211    pub picker: Option<PickerGlue>,
212    pub(crate) picker_ranking: picker::ranking::State,
213    pub(crate) analysis: analysis::AnalysisState,
214    pub resolution: resolution::ResolutionState,
215    pub cwd: PathBuf,
216    /// Bound workspace contexts (0042 slice 2): one per filesystem in use.
217    pub workspaces: workspaces::WorkspaceRegistry,
218    /// Applied change plans and their receipts (0043); grouped undo reads it.
219    pub(crate) changes: changes::ChangeState,
220    /// Open editable code collections by their buffer document (0044).
221    pub(crate) collections: HashMap<strop_core::id::DocumentId, collections::Collection>,
222    /// A build waiting on background source loads (0044 v2).
223    pub(crate) collection_build: Option<collections::CollectionBuild>,
224    /// Container attach/browse state (0037 DC1a).
225    pub(crate) containers: containers::ContainerState,
226    /// MRU document order (most recent first); drives `Space b`.
227    pub mru: Vec<strop_core::id::DocumentId>,
228    /// Picker preview file cache.
229    pub previews: Previews,
230    /// Git working surface state (M2).
231    pub git: Option<strop_git::GitContext>,
232    /// Preview file reads run on worker threads (0001 §3); results and
233    /// the in-flight set are drained in drain_picker.
234    pub preview_tx: std::sync::mpsc::Sender<PreviewResult>,
235    pub preview_rx: Option<std::sync::mpsc::Receiver<PreviewResult>>,
236    pub(crate) preview_loads: HashMap<PathBuf, strop_core::worker::Load<PreviewKey>>,
237    pub hunks: git_memory::HunkSet,
238    /// HEAD↔index — the staged set (0014 wave 4); rendered in the
239    /// gutter's committed-adjacent color.
240    pub staged_hunks: git_memory::HunkSet,
241    /// Git memory (M3): per-buffer surface kinds, blame card, job channel,
242    /// OSC52 clipboard payload drained by the TUI.
243    pub blame_card: Option<strop_git::memory::BlameCard>,
244    /// Each full/range/tail document owns its own revision-checked gutter.
245    pub blame_gutters: HashMap<strop_core::id::DocumentId, BlameGutter>,
246    /// Bumped on every buffer-list mutation; git jobs carry the
247    /// generation they were spawned under so results for dead
248    /// surfaces are dropped (0011 §2).
249    pub generation: u64,
250    pub git_tx: std::sync::mpsc::Sender<GitJob>,
251    pub git_rx: Option<std::sync::mpsc::Receiver<GitJob>>,
252    pub osc52: Option<String>,
253    pub terminal_output: Vec<String>,
254    /// ctrl-l: the terminal desynced from the model — the draw loop
255    /// answers with a full repaint (vim's redraw).
256    pub needs_repaint: bool,
257    /// System-clipboard reads (paste from `+`) run on a worker thread;
258    /// `clip_paste_pending` remembers before/after AND the initiating
259    /// document until the read lands (0023 §4).
260    pub clip_tx: std::sync::mpsc::Sender<ClipboardResult>,
261    pub clip_rx: Option<std::sync::mpsc::Receiver<ClipboardResult>>,
262    pub clip_paste_pending: Option<(bool, strop_core::worker::Ticket<ClipboardKey>)>,
263    /// LSP server pool (0014 wave 2): one client per (workspace root,
264    /// server) — a rust file and a python file in one session get their
265    /// own servers. Diagnostics by path, hover card, open bookkeeping.
266    pub lsp_servers: Vec<crate::editor::lsp::LspServer>,
267    pub diags: HashMap<strop_core::id::DocumentId, DocumentDiagnostics>,
268    pub hover_card: Option<String>,
269    /// Shell jobs (`:!cmd` output buffers, `|cmd` pipes): results land
270    /// in drain_shell — never a subprocess on the input path (0001 §3).
271    pub shell_tx: std::sync::mpsc::Sender<ShellResult>,
272    pub shell_rx: Option<std::sync::mpsc::Receiver<ShellResult>>,
273    pub(crate) shell_requests: HashMap<strop_core::worker::WorkerId, ShellIntent>,
274    pub(crate) shell_focus: Option<strop_core::worker::WorkerId>,
275    /// Splits: flat row/column of panes (v1; tree layout later).
276    pub panes: Vec<Pane>,
277    pub active_pane: usize,
278    pub layout: LayoutDir,
279    /// User config (0005-lite: TOML, embedded defaults, never bricks).
280    pub config: crate::config::Config,
281    /// Shared state root for explicit trust and optional session persistence.
282    pub state_dir: Option<PathBuf>,
283    pub session_policy: crate::session::SessionPolicy,
284    /// The last grammar-level change (dot-repeat's semantic form).
285    pub(crate) last_change: Option<strop_grammar::Command>,
286    /// Direct non-grammar commands (x, p, J…) replay their key string.
287    pub(crate) last_cmd_keys: String,
288    pub(crate) last_insert: Option<String>,
289    pub(crate) recording_insert: Option<String>,
290    /// vim insert counts: `3i…`/`2o` repeat the session's text (o/O
291    /// repeat the opened line too — `insert_open` carries it).
292    pub(crate) insert_count: usize,
293    pub(crate) insert_open: Option<String>,
294    /// Injected frame renderer (0046): cell-grid production belongs to the
295    /// binary; replay of a recorded `Frame` action renders through this
296    /// hook. The engine never renders on its own.
297    pub frame_draw: Option<FrameDraw>,
298}
299
300/// The compiled query owns matching semantics for repeat, preview and highlighting.
301#[derive(Debug, Clone)]
302pub struct LastSearch {
303    pub query: strop_grammar::CompiledQuery,
304    pub backward: bool,
305}
306
307/// A pending `f/F/t/T` awaiting its target char — the leap-style
308/// candidate overlay's input. Named fields, not a naked `(u8, bool)`.
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub struct FindPending {
311    pub ch: char,
312    pub backward: bool,
313}
314
315/// The ctrl-v rectangle (0017): line span by buffer index, cell span
316/// by LineLayout columns — named fields, not a naked mixed-unit tuple.
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub struct BlockRect {
319    pub first_line: usize,
320    pub last_line: usize,
321    pub left_cell: strop_core::id::DisplayColumn,
322    pub right_cell: strop_core::id::DisplayColumn,
323}
324
325impl Editor {
326    pub fn new(buf: Buffer) -> Self {
327        // cwd is the process directory (project-wide): pickers walk it,
328        // LSP/git resolve against it; a file's own dir is not the project.
329        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
330        Self::new_in(buf, cwd)
331    }
332
333    /// Pure construction. Native services start explicitly after forensic seeding.
334    pub fn new_in(buf: Buffer, cwd: PathBuf) -> Self {
335        let (preview_tx, preview_rx) = std::sync::mpsc::channel();
336        let (shell_tx, shell_rx) = std::sync::mpsc::channel();
337        let (clip_tx, clip_rx) = std::sync::mpsc::channel();
338        let (git_tx, git_rx) = git_channel();
339        let mut docs = strop_core::id::Arena::default();
340        // the source identity is set at construction, not by convention
341        let doc = if buf.path.is_some() {
342            Document::new(buf)
343        } else {
344            Document::scratch(buf)
345        };
346        let current = docs.insert(doc);
347        Self {
348            docs,
349            trace_documents: HashMap::new(),
350            io: io::IoState::default(),
351            remote: remote::RemoteState::default(),
352            remote_completion: remote_completion::RemoteCompletionState::default(),
353            picker_ranking: picker::ranking::State::default(),
354            analysis: analysis::AnalysisState::default(),
355            resolution: resolution::ResolutionState::default(),
356            worker_ids: strop_core::worker::WorkerIds::default(),
357            worker_handles: HashMap::new(),
358            focus_epoch: 0,
359            finishing: false,
360            tape: std::rc::Rc::new(strop_trace::replay::Tape::new()),
361            git_view: strop_core::worker::WorkerId::new(0),
362            git_discovery: strop_core::worker::Load::Idle,
363            hunk_load: strop_core::worker::Load::Idle,
364            hunks_untracked: false,
365            log_requests: HashMap::new(),
366            card_request: None,
367            dive_requests: HashMap::new(),
368            git_mutations: std::collections::VecDeque::new(),
369            git_mutation: None,
370            shell_requests: HashMap::new(),
371            shell_focus: None,
372            containers: containers::ContainerState::default(),
373            mru: vec![current],
374            changes: changes::ChangeState::default(),
375            collections: HashMap::new(),
376            collection_build: None,
377            mode: Mode::Normal,
378            pending: pending::PendingInput::default(),
379            walker: input::Walker::new(),
380            last_search: None,
381            undo_browser: None,
382            registers: HashMap::new(),
383            marks: HashMap::new(),
384            last_find: None,
385            flash: None,
386            message: String::new(),
387            should_quit: false,
388            ctrl_c_armed: false,
389            last_visual: None,
390            last_insert_pos: None,
391            change_idx: None,
392            view_rows: 24,
393            recording: None,
394            app_tx: None,
395            lsp_state: lsp::state::LspState::default(),
396            macros: std::collections::HashMap::new(),
397            last_macro: None,
398            block_insert_state: None,
399            macro_depth: 0,
400            last_change: None,
401            last_cmd_keys: String::new(),
402            last_insert: None,
403            recording_insert: None,
404            insert_count: 1,
405            insert_open: None,
406            picker: None,
407            workspaces: {
408                let mut registry = workspaces::WorkspaceRegistry::default();
409                registry.bind(strop_workspace::Filesystem::Local, Some(cwd.clone()));
410                registry
411            },
412            cwd,
413            blame_gutters: HashMap::new(),
414            generation: 0,
415            previews: HashMap::new(),
416            shell_tx,
417            shell_rx: Some(shell_rx),
418            git: None,
419            hunks: git_memory::HunkSet::default(),
420            staged_hunks: git_memory::HunkSet::default(),
421            blame_card: None,
422            git_tx,
423            git_rx: Some(git_rx),
424            needs_repaint: false,
425            osc52: None,
426            terminal_output: Vec::new(),
427            preview_tx,
428            preview_rx: Some(preview_rx),
429            preview_loads: HashMap::new(),
430            jumplist_past: Vec::new(),
431            jumplist_future: Vec::new(),
432            lsp_servers: Vec::new(),
433            clip_tx,
434            clip_rx: Some(clip_rx),
435            clip_paste_pending: None,
436            diags: HashMap::new(),
437            hover_card: None,
438            panes: vec![Pane {
439                doc: current,
440                sels: strop_core::selection::SelectionSet::default(),
441                view_top: 0,
442                hscroll: strop_core::id::DisplayColumn::new(0),
443                desired_column: None,
444            }],
445            active_pane: 0,
446            layout: LayoutDir::Row,
447            config: crate::config::Config::default(),
448            state_dir: None,
449            session_policy: crate::session::SessionPolicy::Automatic,
450            frame_draw: None,
451        }
452    }
453
454    pub fn feed_text(&mut self, text: &str) {
455        for key in keys::parse(text) {
456            self.feed(key);
457        }
458    }
459
460    pub fn feed(&mut self, key: Key) {
461        let _trace_scope = trace::InputScope::enter(self, key);
462        self.trace_state();
463        let generated = self.resolution.in_action;
464        if self.resolution.blocked()
465            || (!generated && !self.resolution.queue.is_empty())
466            || (generated && !self.resolution.staged.is_empty())
467        {
468            if generated {
469                self.resolution
470                    .staged
471                    .push_back(resolution::DeferredInput::GeneratedKey {
472                        key,
473                        depth: self.macro_depth,
474                    });
475            } else {
476                self.resolution
477                    .queue
478                    .push_back(resolution::DeferredInput::Key(key));
479            }
480            return;
481        }
482        self.run_input_action(|editor| {
483            editor.feed_inner(key);
484            editor.prepare_resolution_preview();
485        });
486        self.trace_state();
487    }
488
489    fn feed_inner(&mut self, key: Key) {
490        self.revoke_shell_focus();
491        self.message.clear();
492        if key == Key::Esc
493            && !self.pending.is_active()
494            && self.cancel_open(strop_core::worker::CancelReason::Dismissed)
495        {
496            self.message = "open cancelled".into();
497        }
498        // macro recording (0016): q at ground stops and never reaches
499        // the machine; everything else records BEFORE it runs, so
500        // replay is exactly the live stream
501        if let Some(reg) = self.recording {
502            let at_ground = self.walker.is_ground() && !self.pending.is_active();
503            if at_ground && key == Key::Char('q') {
504                self.recording = None;
505                self.message = format!("recorded @{}", reg);
506                return;
507            }
508            if let Some(buf) = self.macros.get_mut(&reg) {
509                buf.push(key);
510            }
511        }
512        self.dispatch_owned(key);
513    }
514
515    /// True when a modal input field sits in normal mode (picker field
516    /// or pending line) — the TUI draws the block cursor for it.
517    pub fn input_normal(&self) -> bool {
518        self.pending.normal()
519            || self
520                .picker
521                .as_ref()
522                .is_some_and(|g| g.picker.input_normal())
523    }
524
525    /// The modal line's sigil when a free-text line is open (`: / ? |`)
526    /// — the ONE authority; the render card, the terminal's bar-cursor
527    /// shape, and pending dispatch all ask here (a `|sed s/a/b/` body
528    /// is a pipe, not a search).
529    pub fn pending_sigil(&self) -> Option<char> {
530        self.pending.sigil()
531    }
532
533    // ---- shared helpers -------------------------------------------------
534
535    /// `m{a}`: set mark a at the cursor.
536    pub(crate) fn set_mark(&mut self, mark: char) {
537        self.marks.insert(mark, (self.current(), self.head()));
538        self.message = format!("mark {mark} set");
539    }
540
541    /// `'{a}`: jump to mark a (switches buffer if the mark lives there).
542    pub(crate) fn jump_mark(&mut self, mark: char) {
543        self.push_jump(); // mark jumps are jumplist entries
544        match self.marks.get(&mark).copied() {
545            Some((buf, offset)) => {
546                if self.docs.get(buf).is_some() {
547                    if buf != self.current() {
548                        self.switch_to(buf);
549                        self.discover_git();
550                    }
551                    self.set_head(
552                        self.buf()
553                            .clamp_boundary(offset.min(self.buf().len_bytes())),
554                    );
555                    self.clamp_cursor();
556                }
557            }
558            None => self.message = format!("mark {mark} not set"),
559        }
560    }
561}
562
563/// Local-channel delivery and fixture helpers for engine-consumer tests;
564/// live drivers use the shared AppEvent channel.
565#[cfg(any(test, feature = "test-support"))]
566pub mod test_support;
567#[cfg(test)]
568mod tests;
569#[cfg(test)]
570mod transaction_conformance;
571
572impl Drop for Editor {
573    fn drop(&mut self) {
574        // One shutdown boundary, including headless errors and terminal failures.
575        for handle in std::mem::take(&mut self.worker_handles).into_values() {
576            handle.cancel(strop_core::worker::CancelReason::Shutdown);
577        }
578        for server in std::mem::take(&mut self.lsp_servers) {
579            if let Some(client) = server.client {
580                client.shutdown();
581                client.wait(Duration::from_millis(500));
582            }
583        }
584    }
585}
586
587/// The headless `state` directive's logical observation (0006): mode,
588/// cursor, pending input, picker and register state — no cells. The
589/// binary's headless driver prints it; the forensic observation embeds it.
590pub fn state_json(editor: &Editor) -> String {
591    if editor.docs.is_empty() {
592        return serde_json::json!({"should_quit":editor.should_quit,"documents":0,"message":editor.message}).to_string();
593    }
594    serde_json::json!({
595        "mode": editor.mode.chip(),
596        "cursor": editor.head(),
597        "line": editor.buf().line_of(editor.head()) + 1,
598        "col": editor.buf().col_of(editor.head()) + 1,
599        "pending": editor.pending.text(),
600        "message": editor.message,
601        "extra_cursors": editor.extra_selections().iter().map(|s| s.head).collect::<Vec<_>>(),
602        "panes": editor.panes.len(),
603        "active_pane": editor.active_pane,
604        "picker": editor.picker_open(),
605        "picker_input": editor.picker.as_ref().map(|g| g.picker.input.text.clone()),
606        "picker_items": editor.picker.as_ref().map(|g| g.picker.items.len()),
607        "picker_streaming": editor.picker.as_ref().map(|g| g.picker.streaming),
608        "register": editor.register(None).text,
609        "dirty": editor.buf().dirty,
610    })
611    .to_string()
612}