1mod 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;
16pub use changes::review::ReviewRow;
17pub use dispatch::InputOwner;
18pub mod collections;
19mod containers;
20mod cursor;
21mod diagnostics;
22mod dispatch;
23pub mod resolution;
24pub use diagnostics::DocumentDiagnostics;
25mod dive;
26mod document;
27pub mod events;
28mod explain;
29mod git;
30mod git_memory;
31mod help;
32mod indent;
33mod input;
34mod insert;
35pub mod io;
36mod jumps;
37pub mod keys;
38mod lsp;
39pub mod macros;
40pub(crate) mod matching;
41#[cfg(test)]
42mod multicursor_tests;
43pub(crate) mod normal;
44mod occurrence;
45#[cfg(test)]
46mod occurrence_tests;
47mod panes;
48pub mod pending;
49mod permalink;
50mod picker;
51mod registers;
52pub mod remote;
53mod remote_completion;
54mod shell;
55pub mod transact;
56mod undo;
57pub mod view;
58mod visual;
59mod workspaces;
60
61pub use collections::{CollectionRow, CollectionRowInfo};
62pub use document::Document;
63pub use document::{DiffRow, DocumentSource, RemoteDirectory, RemoteDocument, Surface};
64pub use git_memory::{git_channel, BlameGutter, GitJob};
65pub use git_memory::{CommitFiles, PreparedDiff, PreparedFiles, Sidebar, SidebarRow};
66pub use panes::{LayoutDir, Pane};
67pub use picker::{PickerGlue, PreviewKey, PreviewResult, PreviewSource, Previews};
68pub use registers::{ClipboardKey, ClipboardResult, Register};
69pub use shell::{ShellIntent, ShellKey, ShellResult};
70
71pub use containers::{ContainerKey, ContainerResult};
72pub use lsp::attach::AttachRecord;
73pub use lsp::LspServer;
74pub use permalink::PendingPermalink;
75pub use picker::ranking::Event as RankingEvent;
76pub use remote::{RemoteEvent, RemoteView};
77pub use remote_completion::{RemoteCompletionKey, RemoteCompletionResult};
78pub use resolution::{ResolutionEvent, ResolutionState};
79use std::collections::HashMap;
80use std::path::PathBuf;
81use std::time::Duration;
82pub use workspaces::WorkspaceRegistry;
83
84use strop_core::{Buffer, Range};
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum Mode {
88 Normal,
89 Insert,
90 Visual,
91 VisualLine,
92 VisualBlock,
94}
95
96impl Mode {
97 pub fn chip(self) -> &'static str {
98 match self {
99 Mode::Normal => "NORMAL",
100 Mode::Insert => "INSERT",
101 Mode::Visual => "VISUAL",
102 Mode::VisualLine => "V-LINE",
103 Mode::VisualBlock => "V-BLOCK",
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
109pub enum Key {
110 Char(char),
111 Esc,
112 Enter,
113 Backspace,
114 Up,
115 Down,
116 Left,
117 Right,
118 Tab,
119 Backtab,
120 CtrlD,
122 CtrlR,
123 CtrlO,
125 CtrlSpace,
127 CtrlW,
128 CtrlX,
130 CtrlU,
132 CtrlF,
133 CtrlB,
134 CtrlCaret,
136 CtrlV,
138 CtrlL,
140}
141
142pub const FLASH_FOR: Duration = Duration::from_millis(280);
143
144pub type FrameDraw = fn(&mut Editor, u16, u16, bool) -> std::io::Result<()>;
147
148pub struct Editor {
149 pub docs: strop_core::id::Arena<strop_core::id::DocumentKind, Document>,
150 pub(crate) trace_documents:
151 HashMap<strop_core::id::DocumentId, strop_core::diagnostics::BufferTraceId>,
152 pub io: io::IoState,
153 pub(crate) remote: remote::RemoteState,
154 pub(crate) remote_completion: remote_completion::RemoteCompletionState,
155 pub(crate) worker_ids: strop_core::worker::WorkerIds,
156 pub(crate) worker_handles:
157 HashMap<strop_core::worker::WorkerId, strop_core::worker::CancelHandle>,
158 pub(crate) focus_epoch: u64,
159 pub(crate) finishing: bool,
160 pub tape: std::rc::Rc<strop_trace::replay::Tape>,
161 pub git_view: strop_core::worker::WorkerId,
162 pub git_discovery: strop_core::worker::Load<git_memory::ContextKey>,
163 pub hunk_load: strop_core::worker::Load<git_memory::HunkKey>,
164 pub hunks_untracked: bool,
165 pub log_requests:
166 HashMap<strop_core::id::DocumentId, strop_core::worker::Ticket<git_memory::LogKey>>,
167 pub card_request: Option<strop_core::worker::Ticket<git_memory::CardKey>>,
168 pub dive_requests:
169 HashMap<strop_core::id::DocumentId, strop_core::worker::Ticket<git_memory::DiveKey>>,
170 pub git_mutations: std::collections::VecDeque<git_memory::GitMutation>,
171 pub git_mutation: Option<strop_core::worker::Ticket<git_memory::MutationKey>>,
172 pub jumplist_past: Vec<jumps::JumpRecord>,
176 pub jumplist_future: Vec<jumps::JumpRecord>,
177 pub mode: Mode,
178 pub pending: pending::PendingInput,
179 pub walker: input::Walker,
183 pub undo_browser: Option<undo::UndoBrowser>,
185 pub last_find: Option<(char, bool, bool)>,
187 pub last_search: Option<LastSearch>,
190 pub(crate) occurrence: Option<occurrence::OccurrenceState>,
192 pub registers: HashMap<char, Register>,
193 pub marks: HashMap<char, (strop_core::id::DocumentId, usize)>,
195 pub flash: Option<(Range, strop_trace::replay::Tick)>,
196 pub message: String,
197 pub should_quit: bool,
198 pub ctrl_c_armed: bool,
200 pub last_visual: Option<(usize, usize)>,
202 pub last_insert_pos: Option<usize>,
204 pub change_idx: Option<(usize, usize)>,
207 pub view_rows: usize,
210 pub recording: Option<char>,
212 pub app_tx: Option<events::EventSender>,
215 pub(crate) lsp_state: lsp::state::LspState,
216 pub macros: std::collections::HashMap<char, Vec<Key>>,
218 pub last_macro: Option<char>,
220 pub(crate) block_insert_state: Option<block::BlockInsertState>,
222 pub macro_depth: usize,
224 pub picker: Option<PickerGlue>,
225 pub(crate) picker_ranking: picker::ranking::State,
226 pub(crate) analysis: analysis::AnalysisState,
227 pub resolution: resolution::ResolutionState,
228 pub cwd: PathBuf,
229 pub workspaces: workspaces::WorkspaceRegistry,
231 pub(crate) changes: changes::ChangeState,
233 pub(crate) review: changes::review::ReviewState,
234 pub(crate) collections: HashMap<strop_core::id::DocumentId, collections::Collection>,
236 pub(crate) collection_build: Option<collections::CollectionBuild>,
238 pub(crate) containers: containers::ContainerState,
240 pub mru: Vec<strop_core::id::DocumentId>,
242 pub previews: Previews,
244 pub git: Option<strop_git::GitContext>,
246 pub preview_tx: std::sync::mpsc::Sender<PreviewResult>,
249 pub preview_rx: Option<std::sync::mpsc::Receiver<PreviewResult>>,
250 pub(crate) preview_loads: HashMap<PathBuf, strop_core::worker::Load<PreviewKey>>,
251 pub hunks: git_memory::HunkSet,
252 pub staged_hunks: git_memory::HunkSet,
255 pub blame_card: Option<strop_git::memory::BlameCard>,
258 pub blame_gutters: HashMap<strop_core::id::DocumentId, BlameGutter>,
260 pub generation: u64,
264 pub git_tx: std::sync::mpsc::Sender<GitJob>,
265 pub git_rx: Option<std::sync::mpsc::Receiver<GitJob>>,
266 pub osc52: Option<String>,
267 pub terminal_output: Vec<String>,
268 pub needs_repaint: bool,
271 pub clip_tx: std::sync::mpsc::Sender<ClipboardResult>,
275 pub clip_rx: Option<std::sync::mpsc::Receiver<ClipboardResult>>,
276 pub clip_paste_pending: Option<(bool, strop_core::worker::Ticket<ClipboardKey>)>,
277 pub lsp_servers: Vec<crate::editor::lsp::LspServer>,
281 pub diags: HashMap<strop_core::id::DocumentId, DocumentDiagnostics>,
282 pub hover_card: Option<String>,
283 pub shell_tx: std::sync::mpsc::Sender<ShellResult>,
286 pub shell_rx: Option<std::sync::mpsc::Receiver<ShellResult>>,
287 pub(crate) shell_requests: HashMap<strop_core::worker::WorkerId, ShellIntent>,
288 pub(crate) shell_focus: Option<strop_core::worker::WorkerId>,
289 pub panes: Vec<Pane>,
291 pub active_pane: usize,
292 pub layout: LayoutDir,
293 pub config: crate::config::Config,
295 pub state_dir: Option<PathBuf>,
297 pub session_policy: crate::session::SessionPolicy,
298 pub(crate) last_change: Option<strop_grammar::Command>,
300 pub(crate) last_cmd_keys: String,
302 pub(crate) last_insert: Option<String>,
303 pub(crate) recording_insert: Option<String>,
304 pub(crate) insert_count: usize,
307 pub(crate) insert_open: Option<String>,
308 pub frame_draw: Option<FrameDraw>,
312}
313
314#[derive(Debug, Clone)]
316pub struct LastSearch {
317 pub query: strop_grammar::CompiledQuery,
318 pub backward: bool,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub struct FindPending {
325 pub ch: char,
326 pub backward: bool,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub struct BlockRect {
333 pub first_line: usize,
334 pub last_line: usize,
335 pub left_cell: strop_core::id::DisplayColumn,
336 pub right_cell: strop_core::id::DisplayColumn,
337}
338
339impl Editor {
340 pub fn new(buf: Buffer) -> Self {
341 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
344 Self::new_in(buf, cwd)
345 }
346
347 pub fn new_in(buf: Buffer, cwd: PathBuf) -> Self {
349 let (preview_tx, preview_rx) = std::sync::mpsc::channel();
350 let (shell_tx, shell_rx) = std::sync::mpsc::channel();
351 let (clip_tx, clip_rx) = std::sync::mpsc::channel();
352 let (git_tx, git_rx) = git_channel();
353 let mut docs = strop_core::id::Arena::default();
354 let doc = if buf.path.is_some() {
356 Document::new(buf)
357 } else {
358 Document::scratch(buf)
359 };
360 let current = docs.insert(doc);
361 Self {
362 docs,
363 trace_documents: HashMap::new(),
364 io: io::IoState::default(),
365 remote: remote::RemoteState::default(),
366 remote_completion: remote_completion::RemoteCompletionState::default(),
367 picker_ranking: picker::ranking::State::default(),
368 analysis: analysis::AnalysisState::default(),
369 resolution: resolution::ResolutionState::default(),
370 worker_ids: strop_core::worker::WorkerIds::default(),
371 worker_handles: HashMap::new(),
372 focus_epoch: 0,
373 finishing: false,
374 tape: std::rc::Rc::new(strop_trace::replay::Tape::new()),
375 git_view: strop_core::worker::WorkerId::new(0),
376 git_discovery: strop_core::worker::Load::Idle,
377 hunk_load: strop_core::worker::Load::Idle,
378 hunks_untracked: false,
379 log_requests: HashMap::new(),
380 card_request: None,
381 dive_requests: HashMap::new(),
382 git_mutations: std::collections::VecDeque::new(),
383 git_mutation: None,
384 shell_requests: HashMap::new(),
385 shell_focus: None,
386 containers: containers::ContainerState::default(),
387 mru: vec![current],
388 changes: changes::ChangeState::default(),
389 review: changes::review::ReviewState::default(),
390 collections: HashMap::new(),
391 collection_build: None,
392 mode: Mode::Normal,
393 occurrence: None,
394 pending: pending::PendingInput::default(),
395 walker: input::Walker::new(),
396 last_search: None,
397 undo_browser: None,
398 registers: HashMap::new(),
399 marks: HashMap::new(),
400 last_find: None,
401 flash: None,
402 message: String::new(),
403 should_quit: false,
404 ctrl_c_armed: false,
405 last_visual: None,
406 last_insert_pos: None,
407 change_idx: None,
408 view_rows: 24,
409 recording: None,
410 app_tx: None,
411 lsp_state: lsp::state::LspState::default(),
412 macros: std::collections::HashMap::new(),
413 last_macro: None,
414 block_insert_state: None,
415 macro_depth: 0,
416 last_change: None,
417 last_cmd_keys: String::new(),
418 last_insert: None,
419 recording_insert: None,
420 insert_count: 1,
421 insert_open: None,
422 picker: None,
423 workspaces: {
424 let mut registry = workspaces::WorkspaceRegistry::default();
425 registry.bind(strop_workspace::Filesystem::Local, Some(cwd.clone()));
426 registry
427 },
428 cwd,
429 blame_gutters: HashMap::new(),
430 generation: 0,
431 previews: HashMap::new(),
432 shell_tx,
433 shell_rx: Some(shell_rx),
434 git: None,
435 hunks: git_memory::HunkSet::default(),
436 staged_hunks: git_memory::HunkSet::default(),
437 blame_card: None,
438 git_tx,
439 git_rx: Some(git_rx),
440 needs_repaint: false,
441 osc52: None,
442 terminal_output: Vec::new(),
443 preview_tx,
444 preview_rx: Some(preview_rx),
445 preview_loads: HashMap::new(),
446 jumplist_past: Vec::new(),
447 jumplist_future: Vec::new(),
448 lsp_servers: Vec::new(),
449 clip_tx,
450 clip_rx: Some(clip_rx),
451 clip_paste_pending: None,
452 diags: HashMap::new(),
453 hover_card: None,
454 panes: vec![Pane {
455 doc: current,
456 sels: strop_core::selection::SelectionSet::default(),
457 view_top: 0,
458 hscroll: strop_core::id::DisplayColumn::new(0),
459 desired_column: None,
460 }],
461 active_pane: 0,
462 layout: LayoutDir::Row,
463 config: crate::config::Config::default(),
464 state_dir: None,
465 session_policy: crate::session::SessionPolicy::Automatic,
466 frame_draw: None,
467 }
468 }
469
470 pub fn feed_text(&mut self, text: &str) {
471 for key in keys::parse(text) {
472 self.feed(key);
473 }
474 }
475
476 pub fn feed(&mut self, key: Key) {
477 let _trace_scope = trace::InputScope::enter(self, key);
478 self.trace_state();
479 let generated = self.resolution.in_action;
480 if self.resolution.blocked()
481 || (!generated && !self.resolution.queue.is_empty())
482 || (generated && !self.resolution.staged.is_empty())
483 {
484 if generated {
485 self.resolution
486 .staged
487 .push_back(resolution::DeferredInput::GeneratedKey {
488 key,
489 depth: self.macro_depth,
490 });
491 } else {
492 self.resolution
493 .queue
494 .push_back(resolution::DeferredInput::Key(key));
495 }
496 return;
497 }
498 self.run_input_action(|editor| {
499 editor.feed_inner(key);
500 editor.prepare_resolution_preview();
501 });
502 self.trace_state();
503 }
504
505 fn feed_inner(&mut self, key: Key) {
506 self.lsp_state.hover = None;
507 if let Some(build) = self.collection_build.as_mut() {
508 build.focus_on_ready = false;
509 }
510 self.revoke_shell_focus();
511 self.message.clear();
512 if key == Key::Esc
513 && !self.pending.is_active()
514 && self.cancel_open(strop_core::worker::CancelReason::Dismissed)
515 {
516 self.message = "open cancelled".into();
517 }
518 if let Some(reg) = self.recording {
522 let at_ground = self.walker.is_ground() && !self.pending.is_active();
523 if at_ground && key == Key::Char('q') {
524 self.recording = None;
525 self.message = format!("recorded @{}", reg);
526 return;
527 }
528 if let Some(buf) = self.macros.get_mut(®) {
529 buf.push(key);
530 }
531 }
532 self.dispatch_owned(key);
533 }
534
535 pub fn input_normal(&self) -> bool {
538 self.pending.normal()
539 || self
540 .picker
541 .as_ref()
542 .is_some_and(|g| g.picker.input_normal())
543 }
544
545 pub fn pending_sigil(&self) -> Option<char> {
550 self.pending.sigil()
551 }
552
553 pub(crate) fn cur_indent(&self) -> document::Indent {
556 self.indentation_at(self.current(), self.head())
557 }
558
559 pub(crate) fn set_mark(&mut self, mark: char) {
563 self.marks.insert(mark, (self.current(), self.head()));
564 self.message = format!("mark {mark} set");
565 }
566
567 pub fn mark_rows(&self) -> Vec<(char, usize, String)> {
570 let mut rows: Vec<_> = self
571 .marks
572 .iter()
573 .filter_map(|(name, (document, offset))| {
574 let doc = self.docs.get(*document)?;
575 let line = doc.buf.line_of(*offset);
576 let text: String = doc.buf.line_text(line).trim().chars().take(48).collect();
577 Some((*name, line + 1, text))
578 })
579 .collect();
580 rows.sort_by_key(|row| row.0);
581 rows
582 }
583
584 pub(crate) fn jump_mark(&mut self, mark: char) {
586 self.push_jump(); match self.marks.get(&mark).copied() {
588 Some((buf, offset)) => {
589 if self.docs.get(buf).is_some() {
590 if buf != self.current() {
591 self.switch_to(buf);
592 self.discover_git();
593 }
594 self.set_head(
595 self.buf()
596 .clamp_boundary(offset.min(self.buf().len_bytes())),
597 );
598 self.clamp_cursor();
599 self.place_jump_target();
602 }
603 }
604 None => self.message = format!("mark {mark} not set"),
605 }
606 }
607}
608
609#[cfg(any(test, feature = "test-support"))]
612pub mod test_support;
613#[cfg(test)]
614mod tests;
615#[cfg(test)]
616mod transaction_conformance;
617
618impl Drop for Editor {
619 fn drop(&mut self) {
620 for handle in std::mem::take(&mut self.worker_handles).into_values() {
622 handle.cancel(strop_core::worker::CancelReason::Shutdown);
623 }
624 for server in std::mem::take(&mut self.lsp_servers) {
625 if let Some(client) = server.client {
626 client.shutdown();
627 client.wait(Duration::from_millis(500));
628 }
629 }
630 }
631}
632
633pub fn state_json(editor: &Editor) -> String {
637 if editor.docs.is_empty() {
638 return serde_json::json!({"should_quit":editor.should_quit,"documents":0,"message":editor.message}).to_string();
639 }
640 serde_json::json!({
641 "mode": editor.mode.chip(),
642 "cursor": editor.head(),
643 "line": editor.buf().line_of(editor.head()) + 1,
644 "col": editor.buf().col_of(editor.head()) + 1,
645 "pending": editor.pending.text(),
646 "message": editor.message,
647 "extra_cursors": editor.extra_selections().iter().map(|s| s.head).collect::<Vec<_>>(),
648 "panes": editor.panes.len(),
649 "active_pane": editor.active_pane,
650 "picker": editor.picker_open(),
651 "picker_input": editor.picker.as_ref().map(|g| g.picker.input.text.clone()),
652 "picker_items": editor.picker.as_ref().map(|g| g.picker.items.len()),
653 "picker_streaming": editor.picker.as_ref().map(|g| g.picker.streaming),
654 "register": editor.register(None).text,
655 "dirty": editor.buf().dirty,
656 })
657 .to_string()
658}