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