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