mod buffer_list;
mod completion;
mod eval;
mod input;
mod jump;
mod lsp_apply;
mod lsp_coordinator;
mod lsp_request;
mod open;
mod runtime;
mod sleeping;
mod toast;
mod types;
mod workers;
pub use completion::CompletionState;
pub use jump::JumpState;
pub use lsp_coordinator::{LspCoordinator, LspEventOutcome};
pub use sleeping::SleepingBuffer;
pub use toast::{Level, Toast};
pub use types::Selection;
use crate::buffer_ref::BufferRef;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::mpsc::Sender;
use crate::action::{InsertKey, LastChange, LastFind, Token};
#[derive(Debug)]
pub struct InsertRecording {
pub trigger: crate::action::Expr,
pub keys: Vec<InsertKey>,
}
use crate::config::{Config, EditorConfig};
use crate::editor::{Buffer, Cursor};
use crate::event::AppEvent;
use crate::editor::SearchState;
use crate::finder::{self, PreviewLru};
use crate::mode::Mode;
use crate::prompt::PromptController;
use crate::syntax::Loader;
pub use crate::prompt::Prompt;
const MRU_CAP: usize = 64;
pub struct App {
pub buffer: Buffer,
pub mode: Mode,
pub prompt: PromptController,
pub search: SearchState,
pub toast: Toast,
pub tokens: Vec<Token>,
pub visual_anchor: Option<Cursor>,
pub config: Config,
pub loader: Arc<Mutex<Loader>>,
pub preview_lru: RefCell<PreviewLru>,
pub preview_tx: std::sync::mpsc::Sender<PathBuf>,
pub last_preview_request: RefCell<Option<PathBuf>>,
pub startup_cwd: PathBuf,
pub lsp: LspCoordinator,
pub event_tx: Sender<AppEvent>,
pub open_gen: u64,
pub opened_paths: Vec<BufferRef>,
pub sleeping: HashMap<BufferRef, SleepingBuffer>,
pub last_find: Option<LastFind>,
pub last_change: Option<LastChange>,
pub recording: Option<InsertRecording>,
pub visual_g_pending: bool,
pub jump_state: Option<JumpState>,
pub completion: Option<CompletionState>,
pub clipboard: Option<arboard::Clipboard>,
pub should_quit: bool,
}
impl App {
pub fn new(
config: Config,
loader: Loader,
event_tx: Sender<AppEvent>,
startup_cwd: PathBuf,
) -> Self {
let lsp = LspCoordinator::new(event_tx.clone(), startup_cwd.clone());
let loader = Arc::new(Mutex::new(loader));
let (preview_tx, preview_rx) = std::sync::mpsc::channel::<PathBuf>();
let preview_emit_tx = event_tx.clone();
finder::spawn_preview_worker(
Arc::clone(&loader),
config.languages.clone(),
preview_rx,
Box::new(move |entry| {
let _ = preview_emit_tx.send(AppEvent::PreviewReady(entry));
}),
);
Self {
buffer: Buffer::new(),
mode: Mode::Normal,
prompt: PromptController::new(),
search: SearchState::default(),
toast: Toast::info(""),
tokens: Vec::new(),
visual_anchor: None,
config,
loader,
preview_lru: RefCell::new(PreviewLru::new(16)),
preview_tx,
last_preview_request: RefCell::new(None),
startup_cwd,
lsp,
event_tx,
open_gen: 0,
opened_paths: vec![BufferRef::Scratch],
sleeping: HashMap::new(),
last_find: None,
last_change: None,
recording: None,
visual_g_pending: false,
jump_state: None,
completion: None,
clipboard: None,
should_quit: false,
}
}
pub(super) fn record_opened(&mut self, r: BufferRef) {
self.opened_paths.retain(|x| x != &r);
self.opened_paths.push(r);
while self.opened_paths.len() > MRU_CAP {
let evicted = self.opened_paths.remove(0);
self.sleeping.remove(&evicted);
}
}
pub fn selection(&self) -> Option<Selection> {
types::selection(self.mode, self.visual_anchor, self.buffer.cursor)
}
pub fn toast_remaining(&self) -> Option<std::time::Duration> {
if self.toast.text().is_empty() {
return None;
}
if self.toast.level() == Level::Error {
return Some(std::time::Duration::from_secs(3600));
}
const TTL: std::time::Duration = std::time::Duration::from_secs(3);
let elapsed = self.toast.shown_at().elapsed();
if elapsed >= TTL {
None
} else {
Some(TTL - elapsed)
}
}
pub fn clear_toast(&mut self) {
self.toast = Toast::info("");
}
pub fn cursor_visual_col(&self) -> usize {
let tab_width = self.effective_editor().tab_width.max(1);
let line = &self.buffer.lines[self.buffer.cursor.row];
let mut v = 0usize;
for ch in line.chars().take(self.buffer.cursor.col) {
if ch == '\t' {
v += tab_width - (v % tab_width);
} else {
v += 1;
}
}
v
}
pub fn visual_row_offset(&self, row: usize) -> Option<u16> {
let scroll = self.buffer.scroll.get();
if row < scroll {
return None;
}
let cursor_row = self.buffer.cursor.row;
let mut diag_rows: std::collections::HashSet<usize> =
std::collections::HashSet::new();
if let Some(diags) = self.current_diagnostics() {
for d in diags {
let r = d.range.start.line as usize;
if r != cursor_row && d.severity != crate::lsp::Severity::Error {
continue;
}
diag_rows.insert(r);
}
}
let mut y: u16 = 0;
for r in scroll..row {
y = y.saturating_add(1);
if diag_rows.contains(&r) {
y = y.saturating_add(1);
}
}
Some(y)
}
pub(super) fn indent_settings(&self) -> crate::editor::IndentSettings {
let eff = self.effective_editor();
crate::editor::IndentSettings {
width: eff.indent_width.max(1),
use_tabs: eff.use_tabs,
}
}
pub fn effective_editor(&self) -> EditorConfig {
let base = self.config.editor;
let Some(path) = self.buffer.path.as_ref() else {
return base;
};
let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
return base;
};
let Some(lang) = self.config.languages.by_extension(ext) else {
return base;
};
base.overlay(&lang.editor)
}
}
pub(super) fn root_cause(e: &anyhow::Error) -> String {
e.chain()
.last()
.map(|x| x.to_string())
.unwrap_or_else(|| e.to_string())
}
pub(super) fn is_command_not_found(e: &anyhow::Error) -> bool {
e.chain().any(|c| {
c.downcast_ref::<std::io::Error>()
.is_some_and(|io| io.kind() == std::io::ErrorKind::NotFound)
})
}