use std::path::{Path, PathBuf};
use anyhow::Result;
use super::mode::Mode;
use crate::clipboard::Clipboard;
use crate::config::{self, Config};
use crate::editor::buffer::{Buffer, BufferId};
use crate::editor::cursor::{Motion, Position};
use crate::editor::document::Document;
use crate::editor::edit::Edit;
use crate::editor::window::{Window, WindowId, Windows};
use crate::filesystem::tree::Tree;
use crate::search::Search;
use crate::theme::Theme;
#[derive(Debug, Clone, Default)]
pub struct Status {
pub text: String,
pub is_error: bool,
}
#[derive(Debug)]
pub struct App {
pub mode: Mode,
pub config: Config,
pub theme: Theme,
pub buffers: Vec<Buffer>,
pub windows: Windows,
pub command_line: String,
pub status: Status,
pub clipboard: Clipboard,
pub search: Search,
pub tree: Option<Tree>,
pub tree_selected: usize,
pub tree_visible: bool,
pub popup: Option<(String, String)>,
quit: bool,
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
impl App {
#[must_use]
pub fn new() -> Self {
let (config, config_error) = Config::load();
let (theme, theme_error) = Theme::load(&config.theme, &config::themes_dir());
let mut app = Self::assemble(config, theme);
if let Some(message) = config_error.or(theme_error) {
app.error(message);
}
app
}
#[cfg(test)]
#[must_use]
pub fn with_config(config: Config) -> Self {
let theme = Theme::builtin(&config.theme);
Self::assemble(config, theme)
}
fn assemble(config: Config, theme: Theme) -> Self {
let clipboard = Clipboard::new(config.system_clipboard);
Self {
mode: Mode::default(),
config,
theme,
buffers: vec![Buffer::empty()],
windows: Windows::new(Window::new(0)),
command_line: String::new(),
status: Status::default(),
clipboard,
search: Search::default(),
tree: None,
tree_selected: 0,
tree_visible: false,
popup: None,
quit: false,
}
}
#[must_use]
pub fn buffer(&self) -> &Buffer {
&self.buffers[self.windows.focused().buffer]
}
pub fn buffer_mut(&mut self) -> &mut Buffer {
let index = self.windows.focused().buffer;
&mut self.buffers[index]
}
#[must_use]
pub fn window(&self) -> &Window {
self.windows.focused()
}
pub fn window_mut(&mut self) -> &mut Window {
self.windows.focused_mut()
}
pub fn edit(&mut self) -> Edit<'_> {
let index = self.windows.focused().buffer;
let Self {
buffers, windows, ..
} = self;
Edit::new(&mut buffers[index], windows.focused_mut())
}
pub fn edit_and_config(&mut self) -> (Edit<'_>, &Config) {
let index = self.windows.focused().buffer;
let Self {
buffers,
windows,
config,
..
} = self;
(
Edit::new(&mut buffers[index], windows.focused_mut()),
config,
)
}
pub fn move_cursors(&mut self, motion: Motion, extend: bool, allow_eol: bool) {
let index = self.windows.focused().buffer;
let Self {
buffers, windows, ..
} = self;
windows
.focused_mut()
.move_cursors(motion, &buffers[index].document, extend, allow_eol);
}
pub fn clamp_cursors(&mut self, allow_eol: bool) {
let index = self.windows.focused().buffer;
let Self {
buffers, windows, ..
} = self;
windows
.focused_mut()
.clamp_cursors(&buffers[index].document, allow_eol);
}
pub fn clamp_windows_on(&mut self, buffer: BufferId) {
let Self {
buffers, windows, ..
} = self;
for (_, window) in windows.iter_mut() {
if window.buffer == buffer {
window.clamp_cursors(&buffers[buffer].document, false);
}
}
}
pub fn open(&mut self, path: PathBuf) -> Result<()> {
if let Some(index) = self.index_of(&path) {
self.windows.focused_mut().show(index);
return Ok(());
}
let buffer = Buffer::new(Document::open(path)?);
if self.buffers.len() == 1 && self.is_scratch(0) {
self.buffers[0] = buffer;
for (_, window) in self.windows.iter_mut() {
window.reset(0);
}
} else {
self.buffers.push(buffer);
let last = self.buffers.len() - 1;
self.windows.focused_mut().show(last);
}
Ok(())
}
pub fn close_active(&mut self) {
let index = self.windows.focused().buffer;
if self.buffers.len() == 1 {
self.buffers[0] = Buffer::empty();
for (_, window) in self.windows.iter_mut() {
window.reset(0);
}
return;
}
self.buffers.remove(index);
let fallback = index.min(self.buffers.len() - 1);
for (_, window) in self.windows.iter_mut() {
window.buffer_removed(index, fallback);
}
}
pub fn cycle_buffer(&mut self, forward: bool) {
let count = self.buffers.len();
let current = self.windows.focused().buffer;
let next = if forward {
(current + 1) % count
} else {
(current + count - 1) % count
};
self.windows.focused_mut().show(next);
}
#[must_use]
pub fn has_unsaved_changes(&self) -> bool {
self.buffers.iter().any(|b| b.document.is_dirty())
}
pub fn scroll_window(&mut self, id: WindowId, delta: isize) {
let index = self.windows.get(id).buffer;
let scrolloff = self.config.scrolloff;
let Self {
buffers, windows, ..
} = self;
let document = &buffers[index].document;
let window = windows.get_mut(id);
window.view.scroll_lines(delta, document.last_line());
let height = usize::from(window.area.height).max(1);
let margin = scrolloff.min(height.saturating_sub(1) / 2);
let top = window.view.top_line;
let bottom = (top + height - 1).min(document.last_line());
let lowest = (top + margin).min(bottom);
let highest = bottom.saturating_sub(margin).max(lowest);
let head = window.cursor().head;
let line = head.line.clamp(lowest, highest);
if line != head.line {
let goal = window.cursor().goal_col();
let position = document.clamp(Position::new(line, goal), false);
window.cursor_mut().move_to(position, false);
window.cursor_mut().set_goal_col(goal);
}
}
pub fn info(&mut self, text: impl Into<String>) {
self.status = Status {
text: text.into(),
is_error: false,
};
}
pub fn error(&mut self, text: impl Into<String>) {
self.status = Status {
text: text.into(),
is_error: true,
};
}
pub fn clear_status(&mut self) {
self.status = Status::default();
}
pub fn show_popup(&mut self, title: impl Into<String>, body: impl Into<String>) {
self.popup = Some((title.into(), body.into()));
}
pub fn tree_root(&self) -> PathBuf {
self.buffer()
.document
.path()
.and_then(Path::parent)
.map(Path::to_path_buf)
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
}
pub fn quit(&mut self) {
self.quit = true;
}
#[must_use]
pub const fn should_quit(&self) -> bool {
self.quit
}
fn index_of(&self, path: &Path) -> Option<BufferId> {
self.buffers
.iter()
.position(|b| b.document.path() == Some(path))
}
fn is_scratch(&self, index: BufferId) -> bool {
let document = &self.buffers[index].document;
document.path().is_none() && !document.is_dirty() && document.len_chars() == 0
}
}