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