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