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;
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 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 CtrlD,
114 CtrlR,
115 CtrlO,
117 CtrlW,
118 CtrlX,
120 CtrlU,
122 CtrlF,
123 CtrlB,
124 CtrlCaret,
126 CtrlV,
128 CtrlL,
130}
131
132pub const FLASH_FOR: Duration = Duration::from_millis(280);
133
134pub 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 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 pub walker: input::Walker,
172 pub undo_browser: Option<undo::UndoBrowser>,
174 pub last_find: Option<(char, bool, bool)>,
176 pub last_search: Option<LastSearch>,
179 pub registers: HashMap<char, Register>,
180 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 pub ctrl_c_armed: bool,
187 pub last_visual: Option<(usize, usize)>,
189 pub last_insert_pos: Option<usize>,
191 pub change_idx: Option<(usize, usize)>,
194 pub view_rows: usize,
197 pub recording: Option<char>,
199 pub app_tx: Option<events::EventSender>,
202 pub(crate) lsp_state: lsp::state::LspState,
203 pub macros: std::collections::HashMap<char, Vec<Key>>,
205 pub last_macro: Option<char>,
207 pub(crate) block_insert_state: Option<block::BlockInsertState>,
209 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 pub workspaces: workspaces::WorkspaceRegistry,
218 pub(crate) changes: changes::ChangeState,
220 pub(crate) collections: HashMap<strop_core::id::DocumentId, collections::Collection>,
222 pub(crate) collection_build: Option<collections::CollectionBuild>,
224 pub(crate) containers: containers::ContainerState,
226 pub mru: Vec<strop_core::id::DocumentId>,
228 pub previews: Previews,
230 pub git: Option<strop_git::GitContext>,
232 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 pub staged_hunks: git_memory::HunkSet,
241 pub blame_card: Option<strop_git::memory::BlameCard>,
244 pub blame_gutters: HashMap<strop_core::id::DocumentId, BlameGutter>,
246 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 pub needs_repaint: bool,
257 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 pub lsp_servers: Vec<crate::editor::lsp::LspServer>,
267 pub diags: HashMap<strop_core::id::DocumentId, DocumentDiagnostics>,
268 pub hover_card: Option<String>,
269 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 pub panes: Vec<Pane>,
277 pub active_pane: usize,
278 pub layout: LayoutDir,
279 pub config: crate::config::Config,
281 pub state_dir: Option<PathBuf>,
283 pub session_policy: crate::session::SessionPolicy,
284 pub(crate) last_change: Option<strop_grammar::Command>,
286 pub(crate) last_cmd_keys: String,
288 pub(crate) last_insert: Option<String>,
289 pub(crate) recording_insert: Option<String>,
290 pub(crate) insert_count: usize,
293 pub(crate) insert_open: Option<String>,
294 pub frame_draw: Option<FrameDraw>,
298}
299
300#[derive(Debug, Clone)]
302pub struct LastSearch {
303 pub query: strop_grammar::CompiledQuery,
304 pub backward: bool,
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub struct FindPending {
311 pub ch: char,
312 pub backward: bool,
313}
314
315#[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 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
330 Self::new_in(buf, cwd)
331 }
332
333 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 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 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(®) {
509 buf.push(key);
510 }
511 }
512 self.dispatch_owned(key);
513 }
514
515 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 pub fn pending_sigil(&self) -> Option<char> {
530 self.pending.sigil()
531 }
532
533 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 pub fn mark_rows(&self) -> Vec<(char, usize, String)> {
544 let mut rows: Vec<_> = self
545 .marks
546 .iter()
547 .filter_map(|(name, (document, offset))| {
548 let doc = self.docs.get(*document)?;
549 let line = doc.buf.line_of(*offset);
550 let text: String = doc.buf.line_text(line).trim().chars().take(48).collect();
551 Some((*name, line + 1, text))
552 })
553 .collect();
554 rows.sort_by_key(|row| row.0);
555 rows
556 }
557
558 pub(crate) fn jump_mark(&mut self, mark: char) {
560 self.push_jump(); match self.marks.get(&mark).copied() {
562 Some((buf, offset)) => {
563 if self.docs.get(buf).is_some() {
564 if buf != self.current() {
565 self.switch_to(buf);
566 self.discover_git();
567 }
568 self.set_head(
569 self.buf()
570 .clamp_boundary(offset.min(self.buf().len_bytes())),
571 );
572 self.clamp_cursor();
573 }
574 }
575 None => self.message = format!("mark {mark} not set"),
576 }
577 }
578}
579
580#[cfg(any(test, feature = "test-support"))]
583pub mod test_support;
584#[cfg(test)]
585mod tests;
586#[cfg(test)]
587mod transaction_conformance;
588
589impl Drop for Editor {
590 fn drop(&mut self) {
591 for handle in std::mem::take(&mut self.worker_handles).into_values() {
593 handle.cancel(strop_core::worker::CancelReason::Shutdown);
594 }
595 for server in std::mem::take(&mut self.lsp_servers) {
596 if let Some(client) = server.client {
597 client.shutdown();
598 client.wait(Duration::from_millis(500));
599 }
600 }
601 }
602}
603
604pub fn state_json(editor: &Editor) -> String {
608 if editor.docs.is_empty() {
609 return serde_json::json!({"should_quit":editor.should_quit,"documents":0,"message":editor.message}).to_string();
610 }
611 serde_json::json!({
612 "mode": editor.mode.chip(),
613 "cursor": editor.head(),
614 "line": editor.buf().line_of(editor.head()) + 1,
615 "col": editor.buf().col_of(editor.head()) + 1,
616 "pending": editor.pending.text(),
617 "message": editor.message,
618 "extra_cursors": editor.extra_selections().iter().map(|s| s.head).collect::<Vec<_>>(),
619 "panes": editor.panes.len(),
620 "active_pane": editor.active_pane,
621 "picker": editor.picker_open(),
622 "picker_input": editor.picker.as_ref().map(|g| g.picker.input.text.clone()),
623 "picker_items": editor.picker.as_ref().map(|g| g.picker.items.len()),
624 "picker_streaming": editor.picker.as_ref().map(|g| g.picker.streaming),
625 "register": editor.register(None).text,
626 "dirty": editor.buf().dirty,
627 })
628 .to_string()
629}