use crate::files::MultiSource;
#[cfg(feature = "ssh")]
use crate::files::SshSource;
use crate::key;
use crate::{
buffer::{
AltCursor, BufferContext, BufferDeleted, BufferId, BufferList, BufferMessage, CutBuffer,
EditorCutBuffer, MultiBuffer, MultiCursor, Normalizations, Searchable, SelectionRange,
Source, ToggledBookmarks,
},
files::{ChooserSource, FileChooserState},
key::{Binding, CtrlBinding},
prompt::{LinePrompt, TextField},
};
use crossterm::event::Event;
use ratatui::{
layout::{Position, Rect},
widgets::StatefulWidget,
};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::num::NonZero;
use std::path::PathBuf;
#[cfg(feature = "ssh")]
use std::rc::Rc;
use std::sync::LazyLock;
static PAGE_SIZE: LazyLock<usize> = LazyLock::new(|| {
std::env::var("VLE_PAGE_SIZE")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.map(|s| s.clamp(1, 100))
.unwrap_or(25)
});
type DirMap = fn(Direction) -> Option<&'static [&'static str]>;
static MULTIPLEXER: LazyLock<DirMap> = LazyLock::new(|| {
if std::env::var("ZELLIJ").is_ok() {
|direction| {
Some(match direction {
Direction::Up => &["zellij", "action", "move-focus", "up"],
Direction::Down => &["zellij", "action", "move-focus", "down"],
Direction::Left => &["zellij", "action", "move-focus", "left"],
Direction::Right => &["zellij", "action", "move-focus", "right"],
})
}
} else if std::env::var("TMUX").is_ok() {
|direction| {
Some(match direction {
Direction::Up => &["tmux", "select-pane", "-U"],
Direction::Down => &["tmux", "select-pane", "-D"],
Direction::Left => &["tmux", "select-pane", "-L"],
Direction::Right => &["tmux", "select-pane", "-R"],
})
}
} else {
|_| None
}
});
#[derive(Default)]
pub enum EditorMode {
#[default]
Editing,
MarkSet,
VerifySave,
VerifyReload,
SplitPane,
ConfirmClose { buffer: BufferId },
SelectInside,
SelectLine { prompt: LinePrompt },
Search {
search: Search,
range: Option<SelectionRange>,
},
SearchAll { search: Search },
SingleBuffer {
cursors: MultiCursors<Vec<MultiCursor>, Vec<usize>>,
range: SingleBufferRange,
},
AllBuffers {
cursors: MultiCursors<BTreeMap<usize, Vec<MultiCursor>>, BTreeMap<usize, Vec<usize>>>,
},
Open {
chooser: Box<FileChooserState<MultiSource>>,
},
Autocomplete {
offset: usize, completions: Vec<String>, index: usize, },
SelectBuffer {
buffer_list: Vec<BufferListItem>, index: usize, },
}
pub struct Search {
pub prompt: TextField,
pub type_: SearchType,
pub mode: SearchMode,
}
pub enum SearchMode {
Editing,
Autocomplete {
offset: usize,
completions: Vec<String>,
index: usize,
},
}
#[derive(Default)]
pub enum SingleBufferRange {
#[default]
WholeFile,
UpdateLines,
Lines(SelectionRange),
}
impl From<SingleBufferRange> for Option<SelectionRange> {
fn from(range: SingleBufferRange) -> Self {
match range {
SingleBufferRange::WholeFile | SingleBufferRange::UpdateLines => None,
SingleBufferRange::Lines(lines) => Some(lines),
}
}
}
impl From<Option<SelectionRange>> for SingleBufferRange {
fn from(range: Option<SelectionRange>) -> Self {
match range {
None => Self::WholeFile,
Some(lines) => Self::Lines(lines),
}
}
}
pub struct BufferListItem {
pub buffer: BufferId,
pub modified: bool,
pub bookmarks: usize,
}
impl BufferListItem {
fn new(buffer: &BufferContext) -> Self {
Self {
modified: buffer.modified(),
buffer: buffer.id(),
bookmarks: buffer.bookmarks(),
}
}
}
impl std::fmt::Display for BufferListItem {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.buffer.fmt(f)
}
}
pub struct MultiCursors<M, O> {
pub matches: M,
pub match_idx: usize,
pub highlight: bool,
pub mode: MultiCursorMode<O>,
}
pub enum MultiCursorMode<O> {
Editing,
MarkSet,
PasteGroup {
total: usize,
},
Autocomplete {
offsets: O,
completions: Vec<String>,
index: usize,
},
}
#[derive(Copy, Clone, Default)]
pub enum SearchType {
#[default]
CaseSensitive,
CaseInsensitive,
Regex,
}
impl SearchType {
pub fn toggle_search(self) -> Self {
match self {
Self::CaseSensitive => Self::CaseInsensitive,
Self::CaseInsensitive => Self::Regex,
Self::Regex => Self::CaseSensitive,
}
}
}
impl std::fmt::Display for SearchType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::CaseSensitive => "Case Sensitive".fmt(f),
Self::CaseInsensitive => "Case Insensitive".fmt(f),
Self::Regex => "Regex".fmt(f),
}
}
}
macro_rules! keybind {
($bind:ident) => {
Event::Key(
KeyEvent {
code: key::$bind::PRIMARY_KEY,
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
} | KeyEvent {
code: key::$bind::SECONDARY_KEY,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
},
)
};
}
macro_rules! ctrl_keybind {
($bind:ident) => {
Event::Key(KeyEvent {
code: key::$bind::KEY,
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
})
};
}
macro_rules! key {
($code:ident) => {
Event::Key(KeyEvent {
code: KeyCode::$code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
})
};
(F($code:literal)) => {
Event::Key(KeyEvent {
code: KeyCode::F($code),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
})
};
($modifier:ident, $code:ident) => {
Event::Key(KeyEvent {
code: KeyCode::$code,
modifiers: KeyModifiers::$modifier,
kind: KeyEventKind::Press,
..
})
};
($code:literal) => {
Event::Key(KeyEvent {
code: KeyCode::Char($code),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
})
};
($modifier:ident, $code:literal) => {
Event::Key(KeyEvent {
code: KeyCode::Char($code),
modifiers: KeyModifiers::$modifier,
kind: KeyEventKind::Press,
..
})
};
}
#[derive(Default)]
pub struct LastSearch {
plain: Option<TextField>,
regex: Option<TextField>,
}
impl std::ops::Index<SearchType> for LastSearch {
type Output = Option<TextField>;
fn index(&self, t: SearchType) -> &Option<TextField> {
match t {
SearchType::CaseSensitive | SearchType::CaseInsensitive => &self.plain,
SearchType::Regex => &self.regex,
}
}
}
impl std::ops::IndexMut<SearchType> for LastSearch {
fn index_mut(&mut self, t: SearchType) -> &mut Option<TextField> {
match t {
SearchType::CaseSensitive | SearchType::CaseInsensitive => &mut self.plain,
SearchType::Regex => &mut self.regex,
}
}
}
#[cfg(feature = "ssh")]
struct Remote {
username: String,
hostname: String,
sftp: Rc<ssh2::Sftp>,
}
#[cfg(feature = "ssh")]
impl Remote {
fn open(
username: String,
hostname: String,
session: ssh2::Session,
) -> Result<Self, ssh2::Error> {
use std::thread::{sleep, spawn};
use std::time::Duration;
let sftp = Rc::new(session.sftp()?);
spawn(move || {
while let Ok(seconds) = session.keepalive_send() {
sleep(Duration::from_secs(seconds.into()));
}
});
Ok(Self {
username,
hostname,
sftp,
})
}
fn sftp(&self) -> Rc<ssh2::Sftp> {
Rc::clone(&self.sftp)
}
}
#[cfg(feature = "ssh")]
impl std::fmt::Display for Remote {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}@{}", self.username, self.hostname)
}
}
#[derive(Debug)]
pub enum RemoteError {
Io(std::io::Error),
#[cfg(feature = "ssh")]
Ssh(ssh2::Error),
}
impl std::fmt::Display for RemoteError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Io(i) => i.fmt(f),
#[cfg(feature = "ssh")]
Self::Ssh(s) => s.fmt(f),
}
}
}
impl std::error::Error for RemoteError {}
impl From<std::io::Error> for RemoteError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
#[cfg(feature = "ssh")]
impl From<ssh2::Error> for RemoteError {
fn from(err: ssh2::Error) -> Self {
Self::Ssh(err)
}
}
#[derive(Default)]
pub struct OpenDir {
local: Option<PathBuf>,
scratch: Option<PathBuf>,
#[cfg(feature = "ssh")]
ssh: Option<PathBuf>,
last: Option<DirTarget>,
}
impl std::ops::Index<DirTarget> for OpenDir {
type Output = Option<PathBuf>;
fn index(&self, index: DirTarget) -> &Option<PathBuf> {
match index {
DirTarget::Local => &self.local,
DirTarget::Scratch => &self.scratch,
#[cfg(feature = "ssh")]
DirTarget::Ssh => &self.ssh,
}
}
}
impl std::ops::IndexMut<DirTarget> for OpenDir {
fn index_mut(&mut self, index: DirTarget) -> &mut Option<PathBuf> {
match index {
DirTarget::Local => &mut self.local,
DirTarget::Scratch => &mut self.scratch,
#[cfg(feature = "ssh")]
DirTarget::Ssh => &mut self.ssh,
}
}
}
#[derive(Copy, Clone)]
pub enum DirTarget {
Local,
Scratch,
#[cfg(feature = "ssh")]
Ssh,
}
impl DirTarget {
#[cfg(feature = "ssh")]
pub fn toggle(&mut self) -> Self {
match self {
Self::Ssh => {
*self = Self::Local;
Self::Local
}
Self::Local => {
*self = Self::Scratch;
Self::Scratch
}
Self::Scratch => {
*self = Self::Ssh;
Self::Ssh
}
}
}
}
pub struct Editor {
layout: Layout, focused: bool, mode: EditorMode, cut_buffer: Option<EditorCutBuffer>, last_search: LastSearch, show_help: bool, show_sub_help: bool, open_dir: OpenDir, #[cfg(feature = "ssh")]
remote: Option<Remote>, }
impl Editor {
pub fn new(buffers: impl IntoIterator<Item = Source>) -> std::io::Result<Self> {
Ok(Self {
layout: Layout::Single(BufferList::new(buffers)?),
focused: true,
mode: EditorMode::default(),
cut_buffer: None,
last_search: LastSearch::default(),
show_help: false,
show_sub_help: true,
open_dir: OpenDir::default(),
#[cfg(feature = "ssh")]
remote: None,
})
}
#[cfg(feature = "ssh")]
pub fn new_remote(
buffers: impl IntoIterator<Item = Source>,
remote: ssh2::Session,
username: String,
hostname: String,
) -> Result<Self, RemoteError> {
let remote = Remote::open(username, hostname, remote)?;
Ok(Self {
mode: EditorMode::Open {
chooser: Box::new(FileChooserState::new(
MultiSource::ssh(
SshSource::open(remote.to_string(), remote.sftp()),
DirTarget::Ssh,
),
vec![], None,
)?),
},
remote: Some(remote),
..Self::new(buffers)?
})
}
pub fn at_line(mut self, LineNumber { line, column }: LineNumber) -> Self {
let buffers = self.layout.selected_buffer_list_mut();
if let Some(first) = buffers.current_mut() {
match column {
None => first.select_line(line),
Some(column) => first.select_line_and_column(line, column),
}
}
self
}
pub fn has_open_buffers(&self) -> bool {
self.layout.has_open_buffers()
}
pub fn display(&mut self, term: &mut ratatui::DefaultTerminal) -> std::io::Result<Rect> {
term.draw(|frame| {
let area = frame.area();
frame.render_stateful_widget(
EditorWidget {
focused: self.focused,
show_help: self.show_help
&& matches!(
&self.mode,
EditorMode::Editing | EditorMode::Autocomplete { .. }
),
show_sub_help: self.show_sub_help,
mode: &mut self.mode,
},
area,
&mut self.layout,
);
frame.set_cursor_position(
self.layout
.cursor_position(area, self.focused.then_some(&self.mode))
.unwrap_or_default(),
);
})
.map(|completed_frame| completed_frame.area)
}
fn update_buffer(&mut self, f: impl FnOnce(&mut crate::buffer::BufferContext)) {
self.layout.selected_buffer_list_mut().update_buf(f)
}
fn update_buffer_at(
&mut self,
f: impl FnOnce(&mut crate::buffer::BufferContext, Vec<AltCursor<'_>>),
) {
self.layout.update_current_at(f);
}
fn on_buffer<T>(
&mut self,
f: impl FnOnce(&mut crate::buffer::BufferContext) -> T,
) -> Option<T> {
self.layout.on_current(f)
}
fn on_buffer_at<T>(
&mut self,
f: impl FnOnce(&mut crate::buffer::BufferContext, Vec<AltCursor<'_>>) -> T,
) -> Option<T> {
self.layout.on_current_at(f)
}
fn perform_cut(&mut self) {
if let Some(Some(selection)) = self.layout.on_current_at(|b, a| b.take_selection(a)) {
copy_to_system_clipboard(selection.as_str());
self.cut_buffer = Some(EditorCutBuffer::Single(selection));
}
}
fn perform_copy(&mut self) {
if let Some(Some(selection)) = self.layout.on_current(|b| b.get_selection()) {
copy_to_system_clipboard(selection.as_str());
self.cut_buffer = Some(EditorCutBuffer::Single(selection));
}
}
pub fn process_event(&mut self, area: Rect, event: Event) {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
match event {
key!(Esc) => {
self.mode = EditorMode::default();
}
Event::Key(KeyEvent {
code: KeyCode::F(1),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
}) => match self.mode {
EditorMode::Editing => {
self.show_help = !self.show_help;
}
_ => {
self.show_sub_help = !self.show_sub_help;
}
},
Event::FocusGained => {
self.focused = true;
}
Event::FocusLost => {
self.focused = false;
}
event => match &mut self.mode {
EditorMode::Editing => self.process_normal_event(area, event),
EditorMode::MarkSet => {
if let Some(event) = self.process_mark_set_event(event) {
self.mode = EditorMode::default();
self.process_normal_event(area, event);
}
}
EditorMode::Autocomplete {
offset,
completions,
index,
} => match event {
key!(Tab) => {
self.layout.update_current_at(|b, a| {
let (current, next) = complete_forward(index, completions);
b.autocomplete(a, *offset, current, next);
});
}
key!(SHIFT, BackTab) => {
self.layout.update_current_at(|b, a| {
let (current, previous) = complete_backward(index, completions);
b.autocomplete(a, *offset, current, previous);
})
}
event => {
self.mode = EditorMode::default();
self.process_normal_event(area, event);
}
},
EditorMode::Search {
search:
Search {
prompt,
type_,
mode:
SearchMode::Autocomplete {
offset,
completions,
index,
},
},
range,
} => match event {
key!(Tab) => {
let (current, next) = complete_forward(index, completions);
prompt.autocomplete(*offset, current, next);
}
key!(SHIFT, BackTab) => {
let (current, previous) = complete_backward(index, completions);
prompt.autocomplete(*offset, current, previous);
}
event => {
self.mode = EditorMode::Search {
search: Search {
prompt: std::mem::take(prompt),
type_: std::mem::take(type_),
mode: SearchMode::Editing,
},
range: std::mem::take(range),
};
self.process_event(area, event);
}
},
EditorMode::SearchAll {
search:
Search {
prompt,
type_,
mode:
SearchMode::Autocomplete {
offset,
index,
completions,
},
},
} => match event {
key!(Tab) => {
let (current, next) = complete_forward(index, completions);
prompt.autocomplete(*offset, current, next);
}
key!(SHIFT, BackTab) => {
let (current, previous) = complete_backward(index, completions);
prompt.autocomplete(*offset, current, previous);
}
event => {
self.mode = EditorMode::SearchAll {
search: Search {
prompt: std::mem::take(prompt),
type_: std::mem::take(type_),
mode: SearchMode::Editing,
},
};
self.process_event(area, event);
}
},
EditorMode::SingleBuffer {
cursors:
MultiCursors {
matches,
match_idx,
mode:
MultiCursorMode::Autocomplete {
offsets,
completions,
index,
},
..
},
range,
} => match event {
key!(Tab) => {
let (current, next) = complete_forward(index, completions);
self.layout.update_current_at(|b, a| {
b.multi_autocomplete(a, matches, offsets, current, next);
});
}
key!(SHIFT, BackTab) => {
let (current, previous) = complete_backward(index, completions);
self.layout.update_current_at(|b, a| {
b.multi_autocomplete(a, matches, offsets, current, previous);
});
}
event => {
self.mode = EditorMode::SingleBuffer {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: false,
mode: MultiCursorMode::Editing,
},
range: std::mem::take(range),
};
self.process_event(area, event);
}
},
EditorMode::AllBuffers {
cursors:
MultiCursors {
matches,
match_idx,
mode:
MultiCursorMode::Autocomplete {
offsets,
completions,
index,
},
..
},
} => match event {
key!(Tab) => {
let (current, next) = complete_forward(index, completions);
self.layout.on_global_offset_at(
matches,
offsets,
|b, a, matches, offsets| {
b.multi_autocomplete(a, matches, offsets, current, next);
},
);
}
key!(SHIFT, BackTab) => {
let (current, previous) = complete_backward(index, completions);
self.layout.on_global_offset_at(
matches,
offsets,
|b, a, matches, offsets| {
b.multi_autocomplete(a, matches, offsets, current, previous);
},
);
}
event => {
self.mode = EditorMode::AllBuffers {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: false,
mode: MultiCursorMode::Editing,
},
};
self.process_event(area, event);
}
},
EditorMode::ConfirmClose { buffer } => {
let buffer = buffer.clone();
self.process_confirm_close(event, buffer)
}
EditorMode::VerifySave => self.process_verify_save(event),
EditorMode::VerifyReload => self.process_verify_reload(event),
EditorMode::SelectInside => self.process_select_inside(event),
EditorMode::SelectLine { prompt } => {
if let Some(buf) = self.layout.selected_buffer_list_mut().current_mut()
&& let Some(new_mode) = process_select_line(buf, prompt, event)
{
self.mode = new_mode;
}
}
EditorMode::Open { chooser } => {
if let Some(new_mode) = process_open_file(
&mut self.layout,
chooser,
&mut self.open_dir,
self.cut_buffer.as_mut(),
&mut self.last_search,
event,
) {
self.mode = new_mode;
}
}
EditorMode::Search {
search:
Search {
prompt,
type_,
mode: SearchMode::Editing,
},
range,
} => {
if let Some(buf) = self.layout.selected_buffer_list_mut().current_mut()
&& let Some(new_mode) = process_search(
buf,
self.cut_buffer.as_mut(),
&mut self.last_search[*type_],
prompt,
type_,
range.as_ref(),
event,
)
{
self.mode = match new_mode {
NextModeIncremental::Browse { match_idx, matches } => {
EditorMode::SingleBuffer {
cursors: MultiCursors {
matches,
match_idx,
highlight: true,
mode: MultiCursorMode::Editing,
},
range: std::mem::take(range).into(),
}
}
NextModeIncremental::Autocomplete {
offset,
completions,
index,
} => EditorMode::Search {
search: Search {
prompt: std::mem::take(prompt),
type_: std::mem::take(type_),
mode: SearchMode::Autocomplete {
offset,
completions,
index,
},
},
range: std::mem::take(range),
},
NextModeIncremental::SelectLine => EditorMode::SelectLine {
prompt: LinePrompt::default(),
},
};
}
}
EditorMode::SearchAll {
search:
Search {
prompt,
type_,
mode: SearchMode::Editing,
},
} => {
if let Some(new_mode) = process_search(
self.layout.selected_buffer_list_mut(),
self.cut_buffer.as_mut(),
&mut self.last_search[*type_],
prompt,
type_,
(),
event,
) {
self.mode = match new_mode {
NextModeIncremental::Browse { match_idx, matches } => {
EditorMode::AllBuffers {
cursors: MultiCursors {
matches,
match_idx,
highlight: true,
mode: MultiCursorMode::Editing,
},
}
}
NextModeIncremental::SelectLine => EditorMode::SelectLine {
prompt: LinePrompt::default(),
},
NextModeIncremental::Autocomplete {
offset,
completions,
index,
} => EditorMode::SearchAll {
search: Search {
prompt: std::mem::take(prompt),
type_: std::mem::take(type_),
mode: SearchMode::Autocomplete {
offset,
completions,
index,
},
},
},
};
}
}
EditorMode::SingleBuffer {
cursors:
cursors @ MultiCursors {
mode: MultiCursorMode::Editing,
..
},
range,
} => {
if let Some(Some(new_mode)) = self.layout.on_current_at(|b, a| {
process_multi_cursor(b, &mut self.cut_buffer, cursors, event, a)
.and_then(|new_mode| new_mode.into_mode(cursors, b, range))
}) {
self.mode = new_mode;
}
}
EditorMode::SingleBuffer {
cursors:
MultiCursors {
matches,
match_idx,
highlight,
mode: MultiCursorMode::MarkSet,
},
range,
} => {
match self
.layout
.on_current(|b| process_multi_cursor_mark_set(b, matches, highlight, event))
{
Some(Ok(Some(event))) => {
self.mode = EditorMode::SingleBuffer {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::Editing,
},
range: std::mem::take(range),
};
self.process_event(area, event);
}
Some(Err(())) => {
self.mode = EditorMode::SingleBuffer {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::Editing,
},
range: std::mem::take(range),
};
}
_ => { }
}
}
EditorMode::AllBuffers {
cursors:
MultiCursors {
matches,
match_idx,
highlight,
mode: MultiCursorMode::MarkSet,
},
} => {
match process_multi_cursor_mark_set(&mut self.layout, matches, highlight, event)
{
Ok(Some(event)) => {
self.mode = EditorMode::AllBuffers {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::Editing,
},
};
self.process_event(area, event);
}
Err(()) => {
self.mode = EditorMode::AllBuffers {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::Editing,
},
};
}
_ => { }
}
}
EditorMode::AllBuffers {
cursors:
cursors @ MultiCursors {
mode: MultiCursorMode::Editing,
..
},
} => {
if let Some(new_mode) = process_multi_cursor(
&mut self.layout,
&mut self.cut_buffer,
cursors,
event,
(),
) && let Some(new_mode) = new_mode.into_mode(cursors)
{
self.mode = new_mode;
}
}
EditorMode::SingleBuffer {
cursors:
MultiCursors {
matches,
match_idx,
highlight,
mode: MultiCursorMode::PasteGroup { .. },
},
range,
} => {
self.layout.update_current_at(|b, a| {
process_paste_group(b, matches, self.cut_buffer.as_mut(), event, a);
});
self.mode = EditorMode::SingleBuffer {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::Editing,
},
range: std::mem::take(range),
};
}
EditorMode::AllBuffers {
cursors:
MultiCursors {
matches,
match_idx,
highlight,
mode: MultiCursorMode::PasteGroup { .. },
},
} => {
process_paste_group(
&mut self.layout,
matches,
self.cut_buffer.as_mut(),
event,
(),
);
self.mode = EditorMode::AllBuffers {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::Editing,
},
};
}
EditorMode::SplitPane => self.process_split_pane(event),
EditorMode::SelectBuffer { buffer_list, index } => {
match process_select_buffer(
self.layout.selected_buffer_list_mut(),
index,
event,
) {
Some(SelectBuffer::Finish) => {
self.mode = EditorMode::default();
}
Some(SelectBuffer::SwapPanes(idx_a, idx_b)) => {
buffer_list.swap(idx_a, idx_b);
self.layout.swap_buffers(idx_a, idx_b);
}
Some(SelectBuffer::SaveAll) => {
match self.layout.selected_buffer_list_mut().save_all() {
Ok(Ok(saved)) => {
self.layout.on_current(|b| {
match saved {
0 => { }
1 => b.set_message("Saved 1 Buffer"),
n => b.set_message(format!("Saved {n} Buffers")),
}
set_title(b);
});
self.mode = EditorMode::default();
}
Ok(Err(err)) => {
self.layout.on_current(|b| b.set_error(err.to_string()));
}
Err(crate::buffer::Modified) => {
self.mode = EditorMode::VerifySave;
}
}
}
Some(SelectBuffer::ReloadAll) => {
let (list, mut alts) = self.layout.current_buffer_list_mut();
match list.reload_all(&mut alts) {
Ok(Ok(reloaded)) => {
self.layout.on_current(|b| {
match reloaded {
0 => { }
1 => b.set_message("Reloaded 1 Buffer"),
n => b.set_message(format!("Reloaded {n} Buffers")),
}
set_title(b);
});
self.mode = EditorMode::default();
}
Ok(Err(err)) => {
self.layout.on_current(|b| b.set_error(err.to_string()));
}
Err(crate::buffer::Modified) => {
self.mode = EditorMode::VerifyReload;
}
}
}
Some(SelectBuffer::QuitAll) => {
while let Some(buf) = self.layout.selected_buffer_list().current() {
if buf.modified() {
set_title(buf);
self.mode = EditorMode::ConfirmClose { buffer: buf.id() };
break;
} else {
self.layout.remove(buf.id());
}
}
}
Some(SelectBuffer::FindAll) => {
self.mode = EditorMode::SearchAll {
search: Search {
prompt: TextField::default(),
type_: SearchType::default(),
mode: SearchMode::Editing,
},
};
}
None => { }
}
}
},
}
}
fn process_normal_event(&mut self, area: Rect, event: Event) {
use crate::buffer::SelectionType;
use crossterm::event::{
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
MouseEventKind,
};
use std::process::Command;
match event {
keybind!(Quit) => {
if let Some(buf) = self.layout.selected_buffer_list().current() {
if buf.modified() {
self.mode = EditorMode::ConfirmClose { buffer: buf.id() };
} else {
self.layout.remove(buf.id());
if let Some(buf) = self.layout.selected_buffer_list().current() {
set_title(buf);
}
}
}
}
key!(CONTROL, PageUp) => {
if let Some(buf) = self.layout.previous_buffer() {
set_title(buf);
}
}
key!(CONTROL, PageDown) => {
if let Some(buf) = self.layout.next_buffer() {
set_title(buf);
}
}
keybind!(SplitPane) => {
self.mode = EditorMode::SplitPane;
}
Event::Key(KeyEvent {
code:
code @ KeyCode::Left
| code @ KeyCode::Right
| code @ KeyCode::Up
| code @ KeyCode::Down,
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
}) => {
match self.layout.change_pane(match code {
KeyCode::Left => Direction::Left,
KeyCode::Right => Direction::Right,
KeyCode::Up => Direction::Up,
KeyCode::Down => Direction::Down,
_ => unreachable!(),
}) {
Ok(Some(buf)) => {
set_title(buf);
}
Ok(None) => { }
Err(dir) => {
if let Some([cmd, args @ ..]) = MULTIPLEXER(dir)
&& let Err(err) = Command::new(cmd).args(args).output()
{
self.layout
.update_current_at(|buf, _| buf.set_error(err.to_string()));
}
}
}
}
Event::Key(KeyEvent {
code: KeyCode::Up,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => self.update_buffer(|b| b.cursor_up(1, modifiers.contains(KeyModifiers::SHIFT))),
Event::Key(KeyEvent {
code: KeyCode::Down,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => self.update_buffer(|b| b.cursor_down(1, modifiers.contains(KeyModifiers::SHIFT))),
Event::Key(KeyEvent {
code: KeyCode::PageUp,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => self.update_buffer(|b| {
b.cursor_up(*PAGE_SIZE, modifiers.contains(KeyModifiers::SHIFT))
}),
Event::Key(KeyEvent {
code: KeyCode::PageDown,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => self.update_buffer(|b| {
b.cursor_down(*PAGE_SIZE, modifiers.contains(KeyModifiers::SHIFT))
}),
key!(CONTROL, Home) => self.update_buffer(|b| b.cursor_to_selection_start()),
key!(CONTROL, End) => self.update_buffer(|b| {
b.cursor_to_selection_end();
}),
Event::Key(KeyEvent {
code: KeyCode::Left,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => self.update_buffer(|b| b.cursor_back(modifiers.contains(KeyModifiers::SHIFT))),
Event::Key(KeyEvent {
code: KeyCode::Right,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => self.update_buffer(|b| b.cursor_forward(modifiers.contains(KeyModifiers::SHIFT))),
Event::Key(KeyEvent {
code: KeyCode::Home,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => self.update_buffer(|b| b.cursor_home(modifiers.contains(KeyModifiers::SHIFT))),
Event::Key(KeyEvent {
code: KeyCode::End,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => self.update_buffer(|b| b.cursor_end(modifiers.contains(KeyModifiers::SHIFT))),
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => self.update_buffer_at(|b, a| b.insert_char(a, c)),
key!(Backspace) => self.update_buffer_at(|b, a| b.backspace(a)),
key!(Delete) => self.update_buffer_at(|b, a| b.delete(a)),
key!(Enter) => self.update_buffer_at(|b, a| b.newline(a)),
keybind!(WidenSelection) => self.update_buffer(|b| b.select_word_or_lines()),
ctrl_keybind!(Cut) => self.perform_cut(),
ctrl_keybind!(Copy) => self.perform_copy(),
ctrl_keybind!(Paste) => {
self.layout.update_current_at(|b, a| {
b.paste(a, &mut self.cut_buffer);
});
}
Event::Paste(pasted) => {
self.cut_buffer = Some(EditorCutBuffer::Single(pasted.into()));
self.layout.update_current_at(|b, a| {
b.paste(a, &mut self.cut_buffer);
});
}
ctrl_keybind!(Undo) => {
let _ = self
.layout
.on_all(|b| b.perform_undo_active(), |b| b.perform_undo_inactive());
}
ctrl_keybind!(Redo) => {
let _ = self
.layout
.on_all(|b| b.perform_redo_active(), |b| b.perform_redo_inactive());
}
keybind!(Save) => {
if let Some(Err(crate::buffer::Modified)) = self.on_buffer(|b| b.verified_save()) {
self.mode = EditorMode::VerifySave;
}
}
key!(Tab) => {
if let Some(Some((offset, completions))) =
self.on_buffer_at(|b, a| b.complete_or_indent(a))
{
match init_complete_forward(&completions) {
Some((index, original, replacement)) => {
self.update_buffer_at(|b, a| {
b.autocomplete(a, offset, original, replacement)
});
self.mode = EditorMode::Autocomplete {
offset,
completions,
index,
};
}
None => {
self.update_buffer(|b| b.set_error("No Completion Found"));
}
}
};
}
key!(SHIFT, BackTab) => {
if let Some(Some((offset, completions))) =
self.on_buffer_at(|b, a| b.complete_or_unindent(a))
{
match init_complete_backward(&completions) {
Some((index, original, replacement)) => {
self.update_buffer_at(|b, a| {
b.autocomplete(a, offset, original, replacement)
});
self.mode = EditorMode::Autocomplete {
offset,
completions,
index,
};
}
None => {
self.update_buffer(|b| b.set_error("No Completion Found"));
}
}
};
}
keybind!(GotoPair) => self.update_buffer(|b| b.select_matching_paren()),
keybind!(Bookmark) => self.update_buffer(|b| b.toggle_bookmark()),
keybind!(SelectInside) => {
if let Some(Err(())) = self.on_buffer(|b| b.try_select_inside()) {
self.mode = EditorMode::SelectInside;
}
}
keybind!(GotoLine) => {
self.mode = EditorMode::SelectLine {
prompt: LinePrompt::default(),
};
}
keybind!(Find) => {
if let Some(Ok(find)) = self.on_buffer(|b| match b.selection_range() {
Some(SelectionType::Term(selection)) => {
use crate::buffer::Normalizations;
(match Normalizations::try_from(selection) {
Ok(normalizations) => {
b.all_matches(None, normalizations).map_err(|_| ())
}
Err(selection) => b.all_matches(None, selection).map_err(|_| ()),
})
.map(|(match_idx, matches)| {
EditorMode::SingleBuffer {
cursors: MultiCursors {
matches,
match_idx,
highlight: true,
mode: MultiCursorMode::Editing,
},
range: SingleBufferRange::WholeFile,
}
})
}
Some(SelectionType::Range(range)) => Ok(EditorMode::Search {
search: Search {
prompt: TextField::default(),
type_: SearchType::default(),
mode: SearchMode::Editing,
},
range: Some(range),
}),
None => Ok(EditorMode::Search {
search: Search {
prompt: TextField::default(),
type_: SearchType::default(),
mode: SearchMode::Editing,
},
range: None,
}),
}) {
self.mode = find;
}
}
#[cfg(not(feature = "ssh"))]
keybind!(Open) => {
match FileChooserState::new(
MultiSource::local(),
self.layout.selected_buffer_list().scratch_buffers(),
self.open_dir[DirTarget::Local].clone(),
) {
Ok(chooser) => {
self.mode = EditorMode::Open {
chooser: Box::new(chooser),
}
}
Err(err) => {
self.update_buffer(|b| b.set_error(err.to_string()));
}
}
}
#[cfg(feature = "ssh")]
keybind!(Open) => match self.remote.as_ref() {
None => match FileChooserState::new(
MultiSource::local(),
self.layout.selected_buffer_list().scratch_buffers(),
self.open_dir[DirTarget::Local].clone(),
) {
Ok(chooser) => {
self.mode = EditorMode::Open {
chooser: Box::new(chooser),
}
}
Err(err) => {
self.update_buffer(|b| b.set_error(err.to_string()));
}
},
Some(remote) => match FileChooserState::new(
MultiSource::ssh(
SshSource::open(remote.to_string(), remote.sftp()),
self.open_dir.last.unwrap_or(DirTarget::Ssh),
),
self.layout.selected_buffer_list().scratch_buffers(),
self.open_dir[self.open_dir.last.unwrap_or(DirTarget::Ssh)].clone(),
) {
Ok(chooser) => {
self.mode = EditorMode::Open {
chooser: Box::new(chooser),
}
}
Err(err) => self.update_buffer(|b| b.set_error(err.to_string())),
},
},
keybind!(Reload) => {
match self.on_buffer_at(|b, a| b.verified_reload(a)) {
Some(Err(crate::buffer::Modified)) => {
self.mode = EditorMode::VerifyReload;
}
Some(Ok(Err(err))) => {
self.update_buffer(|b| b.set_error(err.to_string()));
}
None | Some(Ok(Ok(()))) => { }
}
}
keybind!(UpdateLines) | key!(CONTROL, 'r') => {
if let Some(matches) = self.on_buffer(|b| b.selection_cursors())
&& let Some(match_idx) = matches.len().checked_sub(1)
{
self.mode = EditorMode::SingleBuffer {
cursors: MultiCursors {
matches,
match_idx,
highlight: false,
mode: MultiCursorMode::Editing,
},
range: SingleBufferRange::UpdateLines,
};
}
}
ctrl_keybind!(Mark) => {
self.mode = EditorMode::MarkSet;
}
key!(CONTROL, '5') => {
let buffer_list = self.layout.selected_buffer_list();
let index = buffer_list.current_index();
let buffer_list = buffer_list.buffers().map(BufferListItem::new).collect();
self.mode = EditorMode::SelectBuffer { buffer_list, index };
}
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
..
}) => {
self.update_buffer(|b| b.cursor_down(1, modifiers.contains(KeyModifiers::SHIFT)));
}
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
..
}) => {
self.update_buffer(|b| b.cursor_up(1, modifiers.contains(KeyModifiers::SHIFT)));
}
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollLeft,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
..
}) => {
self.update_buffer(|b| b.cursor_back(modifiers.contains(KeyModifiers::SHIFT)));
}
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollRight,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
..
}) => {
self.update_buffer(|b| b.cursor_forward(modifiers.contains(KeyModifiers::SHIFT)));
}
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column,
row,
..
}) => {
self.layout
.set_cursor_focus(area, Position { y: row, x: column });
if let Some(buf) = self.layout.selected_buffer_list().current() {
set_title(buf);
}
}
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Right),
column,
row,
..
}) => {
self.layout
.set_cursor_focus(area, Position { y: row, x: column });
if let Some(buf) = self.layout.selected_buffer_list().current() {
set_title(buf);
}
self.update_buffer(|b| b.select_word_or_lines());
}
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Middle),
column,
row,
..
}) => {
self.layout
.set_cursor_focus(area, Position { y: row, x: column });
if let Some(buf) = self.layout.selected_buffer_list().current() {
set_title(buf);
}
self.layout.update_current_at(|b, a| {
b.paste(a, &mut self.cut_buffer);
});
}
_ => { }
}
}
pub fn process_mark_set_event(&mut self, event: Event) -> Option<Event> {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
match event {
Event::Key(KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| b.cursor_up(1, true));
None
}
Event::Key(KeyEvent {
code: KeyCode::Down,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| b.cursor_down(1, true));
None
}
Event::Key(KeyEvent {
code: KeyCode::PageUp,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| {
b.cursor_up(*PAGE_SIZE, true);
});
None
}
Event::Key(KeyEvent {
code: KeyCode::PageDown,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| {
b.cursor_down(*PAGE_SIZE, true);
});
None
}
Event::Key(KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| b.cursor_back(true));
None
}
Event::Key(KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| b.cursor_forward(true));
None
}
Event::Key(KeyEvent {
code: KeyCode::Home,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| b.cursor_home(true));
None
}
Event::Key(KeyEvent {
code: KeyCode::End,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| b.cursor_end(true));
None
}
ctrl_keybind!(Mark) => {
self.mode = EditorMode::default();
None
}
event => Some(event),
}
}
fn process_confirm_close(&mut self, event: Event, buffer_id: BufferId) {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
match event {
key!('y') => {
self.layout.remove(buffer_id);
if let Some(buf) = self.layout.selected_buffer_list().current() {
set_title(buf);
}
self.mode = EditorMode::default();
}
key!('n') => {
self.mode = EditorMode::default();
}
_ => { }
}
}
fn process_verify_save(&mut self, event: Event) {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
match event {
key!('y') => {
self.update_buffer(|b| {
let _ = b.save();
});
self.mode = EditorMode::default();
}
key!('n') => {
self.mode = EditorMode::default();
}
_ => { }
}
}
fn process_verify_reload(&mut self, event: Event) {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
match event {
key!('y') => {
self.update_buffer_at(|b, a| match b.reload(a) {
Ok(()) => {
b.set_message("Reloaded");
}
Err(err) => {
b.set_error(err.to_string());
}
});
self.mode = EditorMode::default();
}
key!('n') => {
self.mode = EditorMode::default();
}
_ => { }
}
}
fn process_split_pane(&mut self, event: Event) {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::process::Command;
match event {
key!(Up) => {
self.layout.split_pane(Direction::Up);
self.mode = EditorMode::default();
}
key!(Down) => {
self.layout.split_pane(Direction::Down);
self.mode = EditorMode::default();
}
key!(Left) => {
self.layout.split_pane(Direction::Left);
self.mode = EditorMode::default();
}
key!(Right) => {
self.layout.split_pane(Direction::Right);
self.mode = EditorMode::default();
}
key!(Delete) => {
self.layout.delete_current_pane();
if let Some(buf) = self.layout.selected_buffer_list().current() {
set_title(buf);
}
self.mode = EditorMode::default();
}
key!(CONTROL, Delete) => {
self.layout.delete_other_panes();
if let Some(buf) = self.layout.selected_buffer_list().current() {
set_title(buf);
}
self.mode = EditorMode::default();
}
key!(SHIFT, Left) => {
let _ = self.layout.swap_pane(Direction::Left);
}
key!(SHIFT, Right) => {
let _ = self.layout.swap_pane(Direction::Right);
}
key!(SHIFT, Up) => {
let _ = self.layout.swap_pane(Direction::Up);
}
key!(SHIFT, Down) => {
let _ = self.layout.swap_pane(Direction::Down);
}
Event::Key(KeyEvent {
code:
code @ KeyCode::Left
| code @ KeyCode::Right
| code @ KeyCode::Up
| code @ KeyCode::Down,
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
}) => {
match self.layout.change_pane(match code {
KeyCode::Left => Direction::Left,
KeyCode::Right => Direction::Right,
KeyCode::Up => Direction::Up,
KeyCode::Down => Direction::Down,
_ => unreachable!(),
}) {
Ok(Some(buf)) => {
set_title(buf);
}
Ok(None) => { }
Err(dir) => {
if let Some([cmd, args @ ..]) = MULTIPLEXER(dir)
&& let Err(err) = Command::new(cmd).args(args).output()
{
self.layout
.update_current_at(|buf, _| buf.set_error(err.to_string()));
}
}
}
}
key!('+') => {
self.layout.update_ratio(|ours, theirs, buf| {
*ours = (*ours + 1).clamp(1, 10);
buf.set_message(format!("Ratio {ours}:{theirs}"));
});
}
key!('-') => {
self.layout.update_ratio(|ours, theirs, buf| {
*ours = ours.saturating_sub(1).clamp(1, 10);
buf.set_message(format!("Ratio {ours}:{theirs}"));
});
}
key!(Enter) => {
self.mode = EditorMode::default();
}
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) if let Some(mut index) = char_to_index(c) => {
if let Ok(()) = self.layout.select_by_index(&mut index) {
self.mode = EditorMode::default();
}
}
_ => { }
}
}
fn process_select_inside(&mut self, event: Event) {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
match event {
Event::Key(KeyEvent {
code: KeyCode::Char('(' | ')'),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| b.select_inside(('(', ')'), Some((')', '('))));
self.mode = EditorMode::default();
}
Event::Key(KeyEvent {
code: KeyCode::Char('[' | ']'),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| b.select_inside(('[', ']'), Some((']', '['))));
self.mode = EditorMode::default();
}
Event::Key(KeyEvent {
code: KeyCode::Char('{' | '}'),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| b.select_inside(('{', '}'), Some(('}', '{'))));
self.mode = EditorMode::default();
}
Event::Key(KeyEvent {
code: KeyCode::Char('<' | '>'),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
}) => {
self.update_buffer(|b| b.select_inside(('<', '>'), Some(('>', '<'))));
self.mode = EditorMode::default();
}
key!('"') => {
self.update_buffer(|b| b.select_inside(('"', '"'), None));
self.mode = EditorMode::default();
}
key!('\'') => {
self.update_buffer(|b| b.select_inside(('\'', '\''), None));
self.mode = EditorMode::default();
}
_ => { }
}
}
pub fn auto_save(&mut self) -> bool {
if matches!(self.mode, EditorMode::Editing) {
self.layout
.selected_buffer_list_mut()
.buffers_mut()
.fold(false, |saved, buf| {
if buf.modified() {
matches!(buf.verified_save(), Ok(Ok(()))) | saved
} else {
saved
}
})
} else {
false
}
}
}
fn process_select_line(
buffer: &mut BufferContext,
prompt: &mut LinePrompt,
event: Event,
) -> Option<EditorMode> {
use crate::prompt::Digit;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
match event {
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
}) => {
if let Ok(d) = Digit::try_from(c) {
prompt.push(d);
}
None
}
Event::Paste(pasted) => match pasted.split_once(':') {
None => match pasted.parse::<usize>() {
Ok(line) => {
buffer.select_line(line.saturating_sub(1));
Some(EditorMode::default())
}
Err(_) => {
buffer.set_error("Invalid Line Number");
None
}
},
Some((line, column)) => match line.parse::<usize>() {
Ok(line) => match column.parse::<usize>() {
Ok(col) => {
buffer
.select_line_and_column(line.saturating_sub(1), col.saturating_sub(1));
Some(EditorMode::default())
}
Err(_) => {
buffer.set_error("Invalid Column Number");
None
}
},
Err(_) => {
buffer.set_error("Invalid Line Number");
None
}
},
},
key!(Backspace) => {
prompt.pop();
None
}
key!(Enter) => {
if prompt.is_empty() {
Some(EditorMode::default())
} else {
match prompt.line_and_column() {
(line, None) => buffer.select_line(line.saturating_sub(1)),
(line, Some(col)) => {
buffer.select_line_and_column(line.saturating_sub(1), col.saturating_sub(1))
}
}
Some(EditorMode::default())
}
}
key!(Home) => {
buffer.select_line(0);
Some(EditorMode::default())
}
key!(End) => {
buffer.select_line(buffer.last_line());
Some(EditorMode::default())
}
keybind!(Find) => Some(EditorMode::Search {
search: Search {
prompt: TextField::default(),
type_: SearchType::default(),
mode: SearchMode::Editing,
},
range: None,
}),
key!(Up) => {
buffer.previous_bookmark();
None
}
key!(Down) => {
buffer.next_bookmark();
None
}
key!(Delete) => {
buffer.delete_bookmark();
None
}
_ => {
None }
}
}
fn process_open_file<S: ChooserSource>(
layout: &mut Layout,
chooser: &mut FileChooserState<S>,
open_dir: &mut OpenDir,
cut_buffer: Option<&mut EditorCutBuffer>,
last_search: &mut LastSearch,
event: Event,
) -> Option<EditorMode> {
use crossterm::event::{
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind,
};
match event {
key!(Up)
| Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
..
}) => {
chooser.arrow_up();
None
}
key!(Down)
| Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
..
}) => {
chooser.arrow_down();
None
}
key!(Home) => {
chooser.home();
None
}
key!(End) => {
chooser.end();
None
}
key!(PageUp) => {
chooser.page_up();
None
}
key!(PageDown) => {
chooser.page_down();
None
}
key!(Left) => {
chooser.arrow_left();
None
}
key!(Right) => {
chooser.arrow_right();
None
}
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
chooser.insert_char(c);
None
}
key!(Backspace) => {
chooser.backspace();
None
}
key!(Delete) => {
chooser.delete();
None
}
key!(Tab) => {
chooser.toggle_selected();
None
}
key!(SHIFT, BackTab) => {
chooser.toggle_all_selected();
None
}
key!(CONTROL, 'h') => {
chooser.toggle_show_hidden();
None
}
key!(Enter) => {
for selected in chooser.select(last_search)? {
if let Err(()) = layout.add(selected) {
open_dir[chooser.target()] = Some(chooser.selected_dir().to_path_buf());
open_dir.last = Some(chooser.target());
return Some(EditorMode::default());
}
}
if let Some(buf) = layout.selected_buffer_list().current() {
set_title(buf);
}
open_dir[chooser.target()] = Some(chooser.selected_dir().to_path_buf());
open_dir.last = Some(chooser.target());
Some(EditorMode::default())
}
keybind!(Open) => {
if let Err(err) = chooser.toggle_source(open_dir)
&& let Some(buf) = layout.selected_buffer_list_mut().current_mut()
{
buf.set_error(err.to_string());
}
None
}
ctrl_keybind!(Paste) => {
chooser.paste(|| {
let c = cut_buffer?;
let b = c.paste_and_rotate();
Some(if b.multi_line() {
match Normalizations::try_from(b.as_str().to_string()) {
Err(term) => PasteContents::MultiLine(term),
Ok(normalized) => PasteContents::MultiLineNormalized(normalized),
}
} else {
PasteContents::SingleLine(b.as_str().to_string())
})
});
None
}
Event::Paste(pasted) => {
chooser.paste(|| Some(PasteContents::SingleLine(pasted)));
None
}
keybind!(Find) => {
chooser.toggle_search(last_search);
None
}
_ => None, }
}
pub enum PasteContents {
SingleLine(String),
MultiLine(String),
MultiLineNormalized(Normalizations),
}
enum NextModeIncremental<M> {
Browse {
match_idx: usize,
matches: M,
},
SelectLine,
Autocomplete {
offset: usize,
completions: Vec<String>,
index: usize,
},
}
fn process_search<'s, S: Searchable<'s>>(
source: &mut S,
cut_buffer: Option<&mut EditorCutBuffer>,
last_search: &mut Option<TextField>,
prompt: &mut TextField,
type_: &mut SearchType,
range: S::Range,
event: Event,
) -> Option<NextModeIncremental<S::Output>> {
use crate::buffer::{CaseInsensitiveNormalizations, Normalizations};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
static NOT_FOUND: &str = "Not Found";
match event {
ctrl_keybind!(Paste) => {
let c = cut_buffer?;
let b = c.paste_and_rotate();
if b.multi_line() {
match Normalizations::try_from(b.as_str().to_string()) {
Err(term) => match source.all_multiline_matches(range, term) {
Ok((match_idx, matches)) => {
*last_search = Some(std::mem::take(prompt));
Some(NextModeIncremental::Browse { match_idx, matches })
}
Err(_) => {
source.set_error(NOT_FOUND);
None
}
},
Ok(normalizations) => match source.all_multiline_matches(range, normalizations)
{
Ok((match_idx, matches)) => {
*last_search = Some(std::mem::take(prompt));
Some(NextModeIncremental::Browse { match_idx, matches })
}
Err(_) => {
source.set_error(NOT_FOUND);
None
}
},
}
} else {
prompt.paste(b.as_str());
None
}
}
key!(Tab) => {
if prompt.is_empty() {
prompt.reset();
*type_ = type_.toggle_search();
None
} else {
let (offset, search) = prompt.autocomplete_word()?;
let completions = source.search_autocomplete_matches(search);
match init_complete_forward(&completions) {
Some((index, original, replacement)) => {
prompt.autocomplete(offset, original, replacement);
Some(NextModeIncremental::Autocomplete {
offset,
completions,
index,
})
}
None => {
source.set_error("No Completions Found");
None
}
}
}
}
key!(SHIFT, BackTab) => {
if prompt.is_empty() {
prompt.reset();
*type_ = type_.toggle_search();
None
} else {
let (offset, search) = prompt.autocomplete_word()?;
let completions = source.search_autocomplete_matches(search);
match init_complete_backward(&completions) {
Some((index, original, replacement)) => {
prompt.autocomplete(offset, original, replacement);
Some(NextModeIncremental::Autocomplete {
offset,
completions,
index,
})
}
None => {
source.set_error("No Completions Found");
None
}
}
}
}
key!(Enter) => match type_ {
SearchType::CaseSensitive => match Normalizations::try_from(prompt.value()?) {
Err(term) => match source.all_matches(range, term) {
Ok((match_idx, matches)) => {
*last_search = Some(std::mem::take(prompt));
Some(NextModeIncremental::Browse { match_idx, matches })
}
Err(_) => {
source.set_error(NOT_FOUND);
None
}
},
Ok(normalizations) => match source.all_matches(range, normalizations) {
Ok((match_idx, matches)) => {
*last_search = Some(std::mem::take(prompt));
Some(NextModeIncremental::Browse { match_idx, matches })
}
Err(_) => {
source.set_error(NOT_FOUND);
None
}
},
},
SearchType::CaseInsensitive => match Normalizations::try_from(prompt.value()?) {
Err(term) => match fancy_regex::RegexBuilder::new(&fancy_regex::escape(&term))
.case_insensitive(true)
.build()
{
Ok(regex) => match source.all_matches(range, regex) {
Ok((match_idx, matches)) => {
*last_search = Some(std::mem::take(prompt));
Some(NextModeIncremental::Browse { match_idx, matches })
}
Err(_) => {
source.set_error(NOT_FOUND);
None
}
},
Err(err) => {
source.set_error(err.to_string());
None
}
},
Ok(normalizations) => match source
.all_matches(range, CaseInsensitiveNormalizations::from(normalizations))
{
Ok((match_idx, matches)) => {
*last_search = Some(std::mem::take(prompt));
Some(NextModeIncremental::Browse { match_idx, matches })
}
Err(_) => {
source.set_error(NOT_FOUND);
None
}
},
},
SearchType::Regex => match prompt.value()?.parse::<fancy_regex::Regex>() {
Ok(regex) => match source.all_matches(range, regex) {
Ok((match_idx, matches)) => {
*last_search = Some(std::mem::take(prompt));
Some(NextModeIncremental::Browse { match_idx, matches })
}
Err(_) => {
source.set_error(NOT_FOUND);
None
}
},
Err(err) => {
source.set_error(err.to_string());
None
}
},
},
keybind!(GotoLine) => Some(NextModeIncremental::SelectLine),
keybind!(Find) => {
if prompt.is_empty()
&& let Some(last) = last_search
{
*prompt = last.clone();
} else {
prompt.reset();
}
None
}
event => {
prompt.process_event(event);
None
}
}
}
enum NextMultiCursorMode<O> {
Default,
Find,
Mark,
PasteGroup {
total: usize,
},
Autocomplete {
offsets: O,
completions: Vec<String>,
index: usize,
},
SingleSelection,
}
impl NextMultiCursorMode<Vec<usize>> {
fn into_mode(
self,
MultiCursors {
matches,
match_idx,
highlight,
..
}: &mut MultiCursors<Vec<MultiCursor>, Vec<usize>>,
buffer: &mut BufferContext,
range: &mut SingleBufferRange,
) -> Option<EditorMode> {
match self {
Self::Default => Some(EditorMode::default()),
Self::Find => Some(EditorMode::Search {
search: Search {
prompt: TextField::default(),
type_: SearchType::default(),
mode: SearchMode::Editing,
},
range: std::mem::take(range).into(),
}),
Self::Mark => Some(EditorMode::SingleBuffer {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::MarkSet,
},
range: std::mem::take(range),
}),
Self::PasteGroup { total } => Some(EditorMode::SingleBuffer {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::PasteGroup { total },
},
range: std::mem::take(range),
}),
Self::Autocomplete {
offsets,
completions,
index,
} => Some(EditorMode::SingleBuffer {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: false,
mode: MultiCursorMode::Autocomplete {
offsets,
completions,
index,
},
},
range: std::mem::take(range),
}),
Self::SingleSelection => {
if matches!(range, SingleBufferRange::UpdateLines)
&& let Some(start) = matches.first()
&& let Some(end) = matches.last()
{
buffer.set_selection(start.start(), end.end());
Some(EditorMode::default())
} else {
None
}
}
}
}
}
impl NextMultiCursorMode<BTreeMap<usize, Vec<usize>>> {
fn into_mode(
self,
MultiCursors {
matches,
match_idx,
highlight,
..
}: &mut MultiCursors<
BTreeMap<usize, Vec<MultiCursor>>,
BTreeMap<usize, Vec<usize>>,
>,
) -> Option<EditorMode> {
match self {
Self::Default => Some(EditorMode::default()),
Self::Find => Some(EditorMode::SearchAll {
search: Search {
prompt: TextField::default(),
type_: SearchType::default(),
mode: SearchMode::Editing,
},
}),
Self::Mark => Some(EditorMode::AllBuffers {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::MarkSet,
},
}),
Self::PasteGroup { total } => Some(EditorMode::AllBuffers {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::PasteGroup { total },
},
}),
Self::Autocomplete {
offsets,
completions,
index,
} => Some(EditorMode::AllBuffers {
cursors: MultiCursors {
matches: std::mem::take(matches),
match_idx: std::mem::take(match_idx),
highlight: std::mem::take(highlight),
mode: MultiCursorMode::Autocomplete {
offsets,
completions,
index,
},
},
}),
Self::SingleSelection => None, }
}
}
fn process_multi_cursor<'a, M: MultiBuffer<'a>>(
buffer: &mut M,
cut_buffer: &mut Option<EditorCutBuffer>,
MultiCursors {
matches,
match_idx,
highlight,
..
}: &mut MultiCursors<M::Matches, M::Offsets>,
event: Event,
alt: M::Alt,
) -> Option<NextMultiCursorMode<M::Offsets>> {
use crossterm::event::{
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind,
};
match event {
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
*highlight = false;
buffer.multi_insert_char(alt, matches, c);
None
}
Event::Paste(pasted) => {
*highlight = false;
buffer.multi_insert_string(alt, matches, &pasted);
None
}
key!(Backspace) => {
*highlight = false;
buffer.multi_backspace(alt, matches);
None
}
key!(Delete) => {
*highlight = false;
buffer.multi_delete(alt, matches);
None
}
key!(CONTROL, Delete) => {
*highlight = true;
match buffer.delete_buffer(matches, match_idx) {
BufferDeleted::BuffersRemain => None,
BufferDeleted::NoBuffers => Some(NextMultiCursorMode::Default),
}
}
keybind!(Find) => Some(NextMultiCursorMode::Find),
keybind!(SelectInside) => {
*highlight = false;
buffer.multi_select_inside(matches, *match_idx);
None
}
key!(Enter) => Some(NextMultiCursorMode::Default),
Event::Key(KeyEvent {
code: KeyCode::Left,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
*highlight = false;
buffer.multi_cursor_back(matches, modifiers.contains(KeyModifiers::SHIFT));
None
}
Event::Key(KeyEvent {
code: KeyCode::Right,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
*highlight = false;
buffer.multi_cursor_forward(matches, modifiers.contains(KeyModifiers::SHIFT));
None
}
Event::Key(KeyEvent {
code: KeyCode::Home,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
*highlight = false;
buffer.multi_cursor_home(matches, modifiers.contains(KeyModifiers::SHIFT));
None
}
Event::Key(KeyEvent {
code: KeyCode::End,
modifiers: modifiers @ KeyModifiers::NONE | modifiers @ KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
*highlight = false;
buffer.multi_cursor_end(matches, modifiers.contains(KeyModifiers::SHIFT));
None
}
ctrl_keybind!(Paste) => match M::paste_group_count(matches) {
Some(total) => Some(NextMultiCursorMode::PasteGroup { total: total.get() }),
None => {
if let Some(cut) = cut_buffer {
buffer.multi_paste(alt, matches, cut);
}
None
}
},
ctrl_keybind!(Copy) => {
let copied = buffer.multi_cursor_copy(matches);
if !copied.is_empty() {
match same(copied.iter().map(|b| b.as_str())) {
Some(copied) => copy_to_system_clipboard(copied),
None => copy_to_system_clipboard(
copied
.iter()
.map(|b| b.as_str().to_string() + "\n")
.collect::<String>()
.as_str(),
),
}
buffer.set_message(match copied.len() {
1 => std::borrow::Cow::from("Copied 1 Item"),
n => std::borrow::Cow::from(format!("Copied {n} Items")),
});
*highlight = false;
*cut_buffer = Some(EditorCutBuffer::Multiple(copied));
}
None
}
ctrl_keybind!(Cut) => {
let cut = buffer.multi_cursor_cut(alt, matches);
if !cut.is_empty() {
match same(cut.iter().map(|b| b.as_str())) {
Some(cut) => copy_to_system_clipboard(cut),
None => copy_to_system_clipboard(
cut.iter()
.map(|b| b.as_str().to_string() + "\n")
.collect::<String>()
.as_str(),
),
}
buffer.set_message(match cut.len() {
1 => std::borrow::Cow::from("Cut 1 Item"),
n => std::borrow::Cow::from(format!("Cut {n} Items")),
});
*highlight = false;
*cut_buffer = Some(EditorCutBuffer::Multiple(cut));
}
None
}
keybind!(WidenSelection) => {
*highlight = false;
buffer.multi_cursor_widen(matches);
None
}
keybind!(Bookmark) => {
*highlight = false;
let toggled = buffer.toggle_bookmarks(matches);
if let Ok(msg) = toggled.try_into() {
buffer.set_buffer_message(msg);
}
None
}
key!(Tab) => {
let (offsets, completions) = buffer.multi_autocomplete_matches(matches)?;
match init_complete_forward(&completions) {
Some((index, original, replacement)) => {
buffer.multi_autocomplete(alt, matches, &offsets, original, replacement);
Some(NextMultiCursorMode::Autocomplete {
offsets,
completions,
index,
})
}
None => {
buffer.set_error("No Completions Found");
None
}
}
}
key!(SHIFT, BackTab) => {
let (offsets, completions) = buffer.multi_autocomplete_matches(matches)?;
match init_complete_backward(&completions) {
Some((index, original, replacement)) => {
buffer.multi_autocomplete(alt, matches, &offsets, original, replacement);
Some(NextMultiCursorMode::Autocomplete {
offsets,
completions,
index,
})
}
None => {
buffer.set_error("No Completions Found");
None
}
}
}
Event::Key(KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
})
| Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
..
}) => {
*highlight = true;
buffer.previous_match(matches, match_idx);
None
}
Event::Key(KeyEvent {
code: KeyCode::Down,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
})
| Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
..
}) => {
*highlight = true;
buffer.next_match(matches, match_idx);
None
}
ctrl_keybind!(Mark) => Some(NextMultiCursorMode::Mark),
keybind!(UpdateLines) => Some(NextMultiCursorMode::SingleSelection),
ctrl_keybind!(Undo) => {
*highlight = false;
buffer.perform_undo(alt, matches);
None
}
ctrl_keybind!(Redo) => {
*highlight = false;
buffer.perform_redo(alt, matches);
None
}
_ => None,
}
}
fn process_multi_cursor_mark_set<'a, M: MultiBuffer<'a>>(
source: &mut M,
matches: &mut M::Matches,
highlight: &mut bool,
event: Event,
) -> Result<Option<Event>, ()> {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
match event {
Event::Key(KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
*highlight = false;
source.multi_cursor_back(matches, true);
Ok(None)
}
Event::Key(KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
*highlight = false;
source.multi_cursor_forward(matches, true);
Ok(None)
}
Event::Key(KeyEvent {
code: KeyCode::Home,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
*highlight = false;
source.multi_cursor_home(matches, true);
Ok(None)
}
Event::Key(KeyEvent {
code: KeyCode::End,
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => {
*highlight = false;
source.multi_cursor_end(matches, true);
Ok(None)
}
ctrl_keybind!(Mark) => Err(()),
event => Ok(Some(event)),
}
}
fn process_paste_group<'a, M: MultiBuffer<'a>>(
source: &mut M,
matches: &mut M::Matches,
cut_buffer: Option<&mut EditorCutBuffer>,
event: Event,
alt: M::Alt,
) {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
match event {
Event::Key(KeyEvent {
code: KeyCode::Char(c @ '0'..='9'),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
}) => {
let group = match c {
'0' => 0,
'1' => 1,
'2' => 2,
'3' => 3,
'4' => 4,
'5' => 5,
'6' => 6,
'7' => 7,
'8' => 8,
'9' => 9,
_ => unreachable!(),
};
source.multi_insert_group(alt, matches, group);
}
ctrl_keybind!(Paste) => {
if let Some(cut) = cut_buffer {
source.multi_paste(alt, matches, cut);
}
}
_ => { }
}
}
enum SelectBuffer {
Finish,
SwapPanes(usize, usize),
SaveAll,
FindAll,
ReloadAll,
QuitAll,
}
fn process_select_buffer(
buffer_list: &mut BufferList,
index: &mut usize,
event: Event,
) -> Option<SelectBuffer> {
use crossterm::event::{
KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind,
};
const PAGE_SIZE: usize = 5;
match event {
key!(Up)
| Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
..
}) => match index.checked_sub(1) {
Some(new_index) => {
*index = new_index;
None
}
None => {
if let Some(new_index) = buffer_list.len().checked_sub(1) {
*index = new_index;
}
None
}
},
key!(Down)
| Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
..
}) => {
*index = (*index + 1) % buffer_list.len();
None
}
key!(CONTROL, Up) => match index.checked_sub(1) {
Some(new_index) => Some(SelectBuffer::SwapPanes(
std::mem::replace(index, new_index),
new_index,
)),
None => {
let new_index = buffer_list.len().checked_sub(1)?;
Some(SelectBuffer::SwapPanes(
std::mem::replace(index, new_index),
new_index,
))
}
},
key!(CONTROL, Down) => {
let new_index = (*index + 1) % buffer_list.len();
Some(SelectBuffer::SwapPanes(
std::mem::replace(index, new_index),
new_index,
))
}
key!(PageDown) => {
*index = (*index + PAGE_SIZE).min(buffer_list.len().saturating_sub(1));
None
}
key!(PageUp) => {
*index = index.saturating_sub(PAGE_SIZE);
None
}
key!(Home) => {
*index = 0;
None
}
key!(End) => {
if let Some(max) = buffer_list.len().checked_sub(1) {
*index = max;
}
None
}
key!(Enter) => buffer_list
.select_buffer(*index)
.ok()
.map(|_| SelectBuffer::Finish),
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
..
}) => buffer_list
.select_buffer(char_to_index(c)?)
.ok()
.map(|_| SelectBuffer::Finish),
keybind!(Save) => Some(SelectBuffer::SaveAll),
keybind!(Find) => Some(SelectBuffer::FindAll),
keybind!(Reload) => Some(SelectBuffer::ReloadAll),
keybind!(Quit) => Some(SelectBuffer::QuitAll),
_ => None, }
}
#[derive(Default)]
enum HorizontalPos {
#[default]
Top,
Bottom,
}
#[derive(Default)]
enum VerticalPos {
#[default]
Left,
Right,
}
enum Layout {
Single(BufferList),
Horizontal {
top: Box<Layout>,
top_fill: u16,
bottom: Box<Layout>,
bottom_fill: u16,
active: HorizontalPos,
},
Vertical {
left: Box<Layout>,
left_fill: u16,
right: Box<Layout>,
right_fill: u16,
active: VerticalPos,
},
}
impl Default for Layout {
fn default() -> Self {
Self::Single(BufferList::default())
}
}
impl Layout {
fn has_open_buffers(&self) -> bool {
let mut s = self;
loop {
match s {
Self::Single(b) => break !b.is_empty(),
Self::Horizontal { top: b, .. } | Self::Vertical { left: b, .. } => {
s = b;
}
}
}
}
fn add(&mut self, path: Source) -> Result<(), ()> {
fn add(layout: &mut Layout, ctx: BufferContext, active: bool) {
match layout {
Layout::Single(b) => {
b.push(ctx, active);
}
Layout::Horizontal {
active: HorizontalPos::Top,
top: current,
bottom: inactive,
..
}
| Layout::Horizontal {
active: HorizontalPos::Bottom,
bottom: current,
top: inactive,
..
}
| Layout::Vertical {
active: VerticalPos::Left,
left: current,
right: inactive,
..
}
| Layout::Vertical {
active: VerticalPos::Right,
right: current,
left: inactive,
..
} => {
add(current, ctx.clone(), active);
add(inactive, ctx, false);
}
}
}
fn add_err(layout: &mut Layout, error: String) {
if let Some(ctx) = layout.selected_buffer_list_mut().current_mut() {
ctx.set_error(error);
}
}
self.selected_buffer_list_mut()
.select_by_source(&path)
.or_else(|()| match BufferContext::open(path) {
Ok(ctx) => {
add(self, ctx, true);
Ok(())
}
Err(err) => {
add_err(self, err.to_string());
Err(())
}
})
}
fn remove(&mut self, buffer: BufferId) {
match self {
Self::Single(buf) => buf.remove(&buffer),
Self::Horizontal {
top: x, bottom: y, ..
}
| Self::Vertical {
left: x, right: y, ..
} => {
x.remove(buffer.clone());
y.remove(buffer);
}
}
}
fn swap_buffers(&mut self, a: usize, b: usize) {
match self {
Self::Single(buffer) => {
buffer.swap_buffers(a, b);
}
Self::Horizontal {
top: buf_a,
bottom: buf_b,
..
}
| Self::Vertical {
left: buf_a,
right: buf_b,
..
} => {
buf_a.swap_buffers(a, b);
buf_b.swap_buffers(a, b);
}
}
}
fn selected_buffer_list(&self) -> &BufferList {
let mut current = self;
loop {
match current {
Self::Single(buffer) => break buffer,
Self::Horizontal {
top: buffer,
active: HorizontalPos::Top,
..
}
| Self::Horizontal {
bottom: buffer,
active: HorizontalPos::Bottom,
..
}
| Self::Vertical {
left: buffer,
active: VerticalPos::Left,
..
}
| Self::Vertical {
right: buffer,
active: VerticalPos::Right,
..
} => {
current = buffer;
}
}
}
}
fn selected_buffer_list_mut(&mut self) -> &mut BufferList {
let mut current = self;
loop {
match current {
Self::Single(buffer) => break buffer,
Self::Horizontal {
top: buffer,
active: HorizontalPos::Top,
..
}
| Self::Horizontal {
bottom: buffer,
active: HorizontalPos::Bottom,
..
}
| Self::Vertical {
left: buffer,
active: VerticalPos::Left,
..
}
| Self::Vertical {
right: buffer,
active: VerticalPos::Right,
..
} => {
current = buffer;
}
}
}
}
fn current_buffer_mut(
&mut self,
) -> Option<(usize, &mut crate::buffer::BufferContext, Vec<AltCursor<'_>>)> {
match self {
Self::Single(buffer) => Some((buffer.current_index(), buffer.current_mut()?, vec![])),
Self::Horizontal {
active: HorizontalPos::Top,
top: active,
bottom: inactive,
..
}
| Self::Horizontal {
active: HorizontalPos::Bottom,
bottom: active,
top: inactive,
..
}
| Self::Vertical {
active: VerticalPos::Left,
left: active,
right: inactive,
..
}
| Self::Vertical {
active: VerticalPos::Right,
right: active,
left: inactive,
..
} => {
let (buffer_idx, buf, alts) = active.current_buffer_mut()?;
Some((
buffer_idx,
buf,
concat_vec(alts, inactive.alt_cursors(buffer_idx)),
))
}
}
}
fn alt_cursors(&mut self, buffer_idx: usize) -> Vec<AltCursor<'_>> {
match self {
Self::Single(buffer) => match buffer.get_mut(buffer_idx) {
Some(buf) => vec![buf.alt_cursor()],
None => vec![],
},
Self::Horizontal {
top: first,
bottom: second,
..
}
| Self::Vertical {
left: first,
right: second,
..
} => concat_vec(
first.alt_cursors(buffer_idx),
second.alt_cursors(buffer_idx),
),
}
}
fn update_current_at<F>(&mut self, f: F)
where
F: FnOnce(&mut crate::buffer::BufferContext, Vec<AltCursor<'_>>),
{
if let Some((_, buf, alts)) = self.current_buffer_mut() {
f(buf, alts);
}
}
fn on_current<T, F>(&mut self, f: F) -> Option<T>
where
F: FnOnce(&mut crate::buffer::BufferContext) -> T,
{
self.selected_buffer_list_mut().on_buf(f)
}
fn on_current_at<T, F>(&mut self, f: F) -> Option<T>
where
F: FnOnce(&mut crate::buffer::BufferContext, Vec<AltCursor<'_>>) -> T,
{
self.current_buffer_mut().map(|(_, buf, alts)| f(buf, alts))
}
fn on_all<F, G>(&mut self, on_active: F, on_rest: G) -> Result<usize, ()>
where
F: FnOnce(&mut BufferContext) -> Result<(), ()>,
G: Fn(&mut BufferContext) + Copy,
{
match self {
Self::Single(b) => {
let index = b.current_index();
on_active(b.get_mut(index).ok_or(())?).map(|()| index)
}
Self::Horizontal {
active: HorizontalPos::Top,
top: active,
bottom: inactive,
..
}
| Self::Horizontal {
active: HorizontalPos::Bottom,
bottom: active,
top: inactive,
..
}
| Self::Vertical {
active: VerticalPos::Left,
left: active,
right: inactive,
..
}
| Self::Vertical {
active: VerticalPos::Right,
right: active,
left: inactive,
..
} => {
let index = active.on_all(on_active, on_rest)?;
inactive.on_rest(index, on_rest);
Ok(index)
}
}
}
fn on_rest<F>(&mut self, index: usize, on_rest: F)
where
F: Fn(&mut BufferContext) + Copy,
{
match self {
Self::Single(b) => {
if let Some(buf) = b.get_mut(index) {
on_rest(buf);
}
}
Self::Horizontal {
top: a, bottom: b, ..
}
| Self::Vertical {
left: a, right: b, ..
} => {
a.on_rest(index, on_rest);
b.on_rest(index, on_rest);
}
}
}
fn on_global(
&mut self,
matches: &mut BTreeMap<usize, Vec<MultiCursor>>,
mut f: impl FnMut(&mut BufferContext, &mut [MultiCursor]),
) {
let buffer_list = self.selected_buffer_list_mut();
matches.iter_mut().for_each(|(idx, matches)| {
if let Some(buf) = buffer_list.get_mut(*idx) {
f(buf, matches);
}
});
}
fn on_global_at(
&mut self,
matches: &mut BTreeMap<usize, Vec<MultiCursor>>,
mut f: impl FnMut(&mut BufferContext, Vec<AltCursor<'_>>, &mut [MultiCursor]),
) {
let (buffer_list, mut alts) = self.current_buffer_list_mut();
matches.iter_mut().for_each(|(idx, matches)| {
if let Some(buf) = buffer_list.get_mut(*idx) {
f(
buf,
alts.iter_mut()
.filter_map(|a| a.get_mut(*idx).map(|b| b.alt_cursor()))
.collect(),
matches,
);
}
});
}
fn on_global_offset_at(
&mut self,
matches: &mut BTreeMap<usize, Vec<MultiCursor>>,
offsets: &BTreeMap<usize, Vec<usize>>,
mut f: impl FnMut(&mut BufferContext, Vec<AltCursor<'_>>, &mut [MultiCursor], &[usize]),
) {
let (buffer_list, mut alts) = self.current_buffer_list_mut();
matches.iter_mut().for_each(|(idx, matches)| {
if let Some(buf) = buffer_list.get_mut(*idx)
&& let Some(offsets) = offsets.get(idx)
{
f(
buf,
alts.iter_mut()
.filter_map(|a| a.get_mut(*idx).map(|b| b.alt_cursor()))
.collect(),
matches,
offsets,
);
}
})
}
fn current_buffer_list_mut(
&mut self,
) -> (
&mut crate::buffer::BufferList,
Vec<&mut crate::buffer::BufferList>,
) {
match self {
Self::Single(buffer_list) => (buffer_list, vec![]),
Self::Horizontal {
active: HorizontalPos::Top,
top: active,
bottom: inactive,
..
}
| Self::Horizontal {
active: HorizontalPos::Bottom,
bottom: active,
top: inactive,
..
}
| Self::Vertical {
active: VerticalPos::Left,
left: active,
right: inactive,
..
}
| Self::Vertical {
active: VerticalPos::Right,
right: active,
left: inactive,
..
} => {
let (buffer_list, mut alts) = active.current_buffer_list_mut();
alts.extend(inactive.alt_buffer_lists());
(buffer_list, alts)
}
}
}
fn alt_buffer_lists(&mut self) -> Vec<&mut crate::buffer::BufferList> {
match self {
Self::Single(buffer_list) => vec![buffer_list],
Self::Horizontal {
top: first,
bottom: second,
..
}
| Self::Vertical {
left: first,
right: second,
..
} => {
let mut buffer_lists = first.alt_buffer_lists();
buffer_lists.extend(second.alt_buffer_lists());
buffer_lists
}
}
}
fn previous_buffer(&mut self) -> Option<&BufferContext> {
let buf_list = self.selected_buffer_list_mut();
buf_list.previous_buffer();
buf_list.current()
}
fn next_buffer(&mut self) -> Option<&BufferContext> {
let buf_list = self.selected_buffer_list_mut();
buf_list.next_buffer();
buf_list.current()
}
fn change_pane(&mut self, direction: Direction) -> Result<Option<&BufferContext>, Direction> {
match (self, direction) {
(Self::Single(_), direction) => Err(direction),
(
Self::Horizontal {
active: active @ HorizontalPos::Bottom,
bottom,
top,
..
},
direction @ Direction::Up,
) => bottom.change_pane(direction).or_else(|_| {
*active = HorizontalPos::Top;
Ok(top.selected_buffer_list().current())
}),
(
Self::Horizontal {
active: active @ HorizontalPos::Top,
top,
bottom,
..
},
direction @ Direction::Down,
) => top.change_pane(direction).or_else(|_| {
*active = HorizontalPos::Bottom;
Ok(bottom.selected_buffer_list().current())
}),
(
Self::Vertical {
active: active @ VerticalPos::Left,
left,
right,
..
},
direction @ Direction::Right,
) => left.change_pane(direction).or_else(|_| {
*active = VerticalPos::Right;
Ok(right.selected_buffer_list().current())
}),
(
Self::Vertical {
active: active @ VerticalPos::Right,
right,
left,
..
},
direction @ Direction::Left,
) => right.change_pane(direction).or_else(|_| {
*active = VerticalPos::Left;
Ok(left.selected_buffer_list().current())
}),
(
Self::Horizontal {
active: HorizontalPos::Bottom,
bottom: active,
..
}
| Self::Horizontal {
active: HorizontalPos::Top,
top: active,
..
}
| Self::Vertical {
active: VerticalPos::Left,
left: active,
..
}
| Self::Vertical {
active: VerticalPos::Right,
right: active,
..
},
direction,
) => active.change_pane(direction),
}
}
fn swap_pane(&mut self, direction: Direction) -> Result<(), &mut BufferList> {
match (self, direction) {
(Self::Single(buflist), _) => Err(buflist),
(
Self::Horizontal {
active: active @ HorizontalPos::Bottom,
bottom,
top,
..
},
direction @ Direction::Up,
) => bottom.swap_pane(direction).or_else(|buflist| {
*active = HorizontalPos::Top;
std::mem::swap(top.selected_buffer_list_mut(), buflist);
Ok(())
}),
(
Self::Horizontal {
active: active @ HorizontalPos::Top,
top,
bottom,
..
},
direction @ Direction::Down,
) => top.swap_pane(direction).or_else(|buflist| {
*active = HorizontalPos::Bottom;
std::mem::swap(bottom.selected_buffer_list_mut(), buflist);
Ok(())
}),
(
Self::Vertical {
active: active @ VerticalPos::Left,
left,
right,
..
},
direction @ Direction::Right,
) => left.swap_pane(direction).or_else(|buflist| {
*active = VerticalPos::Right;
std::mem::swap(right.selected_buffer_list_mut(), buflist);
Ok(())
}),
(
Self::Vertical {
active: active @ VerticalPos::Right,
right,
left,
..
},
direction @ Direction::Left,
) => right.swap_pane(direction).or_else(|buflist| {
*active = VerticalPos::Left;
std::mem::swap(left.selected_buffer_list_mut(), buflist);
Ok(())
}),
(
Self::Horizontal {
active: HorizontalPos::Bottom,
bottom: active,
..
}
| Self::Horizontal {
active: HorizontalPos::Top,
top: active,
..
}
| Self::Vertical {
active: VerticalPos::Left,
left: active,
..
}
| Self::Vertical {
active: VerticalPos::Right,
right: active,
..
},
direction,
) => active.swap_pane(direction),
}
}
fn split_pane(&mut self, direction: Direction) {
let mut current = self;
loop {
match current {
Self::Single(buffer) => match direction {
Direction::Up => {
*current = Self::Horizontal {
top: Box::new(Self::Single(buffer.clone())),
bottom: Box::new(Self::Single(std::mem::take(buffer))),
active: HorizontalPos::Top,
top_fill: 1,
bottom_fill: 1,
};
break;
}
Direction::Down => {
*current = Self::Horizontal {
top: Box::new(Self::Single(buffer.clone())),
bottom: Box::new(Self::Single(std::mem::take(buffer))),
active: HorizontalPos::Bottom,
top_fill: 1,
bottom_fill: 1,
};
break;
}
Direction::Left => {
*current = Self::Vertical {
left: Box::new(Self::Single(buffer.clone())),
right: Box::new(Self::Single(std::mem::take(buffer))),
active: VerticalPos::Left,
left_fill: 1,
right_fill: 1,
};
break;
}
Direction::Right => {
*current = Self::Vertical {
left: Box::new(Self::Single(buffer.clone())),
right: Box::new(Self::Single(std::mem::take(buffer))),
active: VerticalPos::Right,
left_fill: 1,
right_fill: 1,
};
break;
}
},
Self::Horizontal {
active: HorizontalPos::Top,
top: active,
..
}
| Self::Horizontal {
active: HorizontalPos::Bottom,
bottom: active,
..
}
| Self::Vertical {
active: VerticalPos::Left,
left: active,
..
}
| Self::Vertical {
active: VerticalPos::Right,
right: active,
..
} => {
current = active;
}
}
}
}
fn delete_current_pane(&mut self) {
match self {
Self::Single(_) => { }
Self::Horizontal {
active: HorizontalPos::Top,
top: active,
bottom: remaining,
..
}
| Self::Horizontal {
active: HorizontalPos::Bottom,
bottom: active,
top: remaining,
..
}
| Self::Vertical {
active: VerticalPos::Left,
left: active,
right: remaining,
..
}
| Self::Vertical {
active: VerticalPos::Right,
right: active,
left: remaining,
..
} => {
if matches!(&**active, Layout::Single(_)) {
*self = std::mem::take(remaining);
} else {
active.delete_current_pane();
}
}
}
}
fn delete_other_panes(&mut self) {
let current = std::mem::take(self).into_selected_buffer_list();
*self = Layout::Single(current);
}
fn into_selected_buffer_list(self) -> BufferList {
match self {
Self::Single(buffer) => buffer,
Self::Horizontal {
top: buffer,
active: HorizontalPos::Top,
..
}
| Self::Horizontal {
bottom: buffer,
active: HorizontalPos::Bottom,
..
}
| Self::Vertical {
left: buffer,
active: VerticalPos::Left,
..
}
| Self::Vertical {
right: buffer,
active: VerticalPos::Right,
..
} => buffer.into_selected_buffer_list(),
}
}
fn cursor_position(&self, area: Rect, mode: Option<&EditorMode>) -> Option<Position> {
use ratatui::layout::Constraint::{Length, Min};
use ratatui::layout::Layout;
let area = match self.selected_buffer_list().has_tabs() {
true => {
let [_, widget_area] = Layout::vertical([Length(1), Min(0)]).areas(area);
widget_area
}
false => area,
};
self.cursor_position_inner(area, mode)
}
fn cursor_position_inner(&self, area: Rect, mode: Option<&EditorMode>) -> Option<Position> {
use crate::buffer::BufferWidget;
use ratatui::layout::Constraint::{Length, Min};
use ratatui::layout::{Constraint, Layout};
fn apply_position(
area: Rect,
(row, col): (usize, usize),
mode: Option<&EditorMode>,
) -> Option<Position> {
use ratatui::widgets::Block;
let [text_area, _] =
Layout::horizontal([Min(0), Length(1)]).areas(Block::bordered().inner(area));
match mode {
Some(EditorMode::SelectLine { .. }) => Some(Position {
x: text_area.x + text_area.width,
y: text_area.y.saturating_sub(1),
}),
Some(
EditorMode::Search {
search: Search { prompt, .. },
..
}
| EditorMode::SearchAll {
search: Search { prompt, .. },
},
) => {
let [_, dialog_area, _] =
Layout::vertical([Min(0), Length(3), Min(0)]).areas(text_area);
let dialog_area = Block::bordered().inner(dialog_area);
Some(Position {
x: dialog_area.x + (prompt.cursor_column() as u16).min(dialog_area.width),
y: dialog_area.y,
})
}
Some(EditorMode::Open { chooser }) => {
let (x, y) = chooser.cursor_position(text_area);
Some(Position { x, y })
}
Some(EditorMode::SelectBuffer { .. }) => None,
_ => {
let x = (col + usize::from(text_area.x)).min(
(text_area.x
+ text_area
.width
.saturating_sub(crate::buffer::BufferWidget::RIGHT_MARGIN))
.into(),
);
let y = (row + usize::from(text_area.y))
.min((text_area.y + text_area.height).into());
Some(Position {
x: u16::try_from(x).ok()?,
y: u16::try_from(y).ok()?,
})
}
}
}
match self {
Self::Single(buf) => buf
.cursor_viewport_position(BufferWidget::viewport_height(area))
.and_then(|pos| apply_position(area, pos, mode)),
Self::Horizontal {
top,
active: HorizontalPos::Top,
top_fill,
bottom_fill,
..
} => {
let [top_area, _] =
Layout::vertical(Constraint::from_fills([*top_fill, *bottom_fill])).areas(area);
top.cursor_position_inner(top_area, mode)
}
Self::Horizontal {
bottom,
active: HorizontalPos::Bottom,
top_fill,
bottom_fill,
..
} => {
let [_, bottom_area] =
Layout::vertical(Constraint::from_fills([*top_fill, *bottom_fill])).areas(area);
bottom.cursor_position_inner(bottom_area, mode)
}
Self::Vertical {
left,
active: VerticalPos::Left,
left_fill,
right_fill,
..
} => {
let [left_area, _] =
Layout::horizontal(Constraint::from_fills([*left_fill, *right_fill]))
.areas(area);
left.cursor_position_inner(left_area, mode)
}
Self::Vertical {
right,
active: VerticalPos::Right,
left_fill,
right_fill,
..
} => {
let [_, right_area] =
Layout::horizontal(Constraint::from_fills([*left_fill, *right_fill]))
.areas(area);
right.cursor_position_inner(right_area, mode)
}
}
}
fn set_cursor_focus(&mut self, mut area: Rect, position: Position) {
use ratatui::layout::{
Constraint::{Length, Min},
Layout,
};
if let Some((_, tabs)) = self.selected_buffer_list().tabs() {
let [tabs_area, layout_area] = Layout::vertical([Length(1), Min(0)]).areas(area);
if tabs_area.contains(position) {
let mut col = position.x;
for (index, tab) in tabs.into_iter().enumerate() {
use unicode_width::UnicodeWidthStr;
let tab_width = tab.width() as u16;
if col <= tab_width {
self.selected_buffer_list_mut().set_index(index);
return;
} else {
col = match col.checked_sub(tab_width) {
Some(col) => col,
None => return,
};
}
}
return;
}
area = layout_area;
}
self.set_cursor_focus_inner(area, position);
}
fn set_cursor_focus_inner(&mut self, area: Rect, position: Position) {
use ratatui::layout::{Constraint, Layout};
match self {
Self::Single(buffer) => {
buffer.set_cursor_focus(area, position);
}
Self::Horizontal {
top,
bottom,
active,
top_fill,
bottom_fill,
} => {
let [top_area, bottom_area] =
Layout::vertical(Constraint::from_fills([*top_fill, *bottom_fill])).areas(area);
if top_area.contains(position) {
*active = HorizontalPos::Top;
top.set_cursor_focus_inner(top_area, position);
} else if bottom_area.contains(position) {
*active = HorizontalPos::Bottom;
bottom.set_cursor_focus_inner(bottom_area, position);
}
}
Self::Vertical {
left,
right,
active,
left_fill,
right_fill,
} => {
let [left_area, right_area] =
Layout::horizontal(Constraint::from_fills([*left_fill, *right_fill]))
.areas(area);
if left_area.contains(position) {
*active = VerticalPos::Left;
left.set_cursor_focus_inner(left_area, position);
} else if right_area.contains(position) {
*active = VerticalPos::Right;
right.set_cursor_focus_inner(right_area, position);
}
}
}
}
fn update_ratio<F>(&mut self, f: F)
where
F: FnOnce(&mut u16, u16, &mut BufferContext),
{
let mut current = self;
loop {
match current {
Self::Single(_) => break,
Self::Horizontal {
active: HorizontalPos::Top,
top: selected,
top_fill: selected_fill,
bottom_fill: other_fill,
..
}
| Self::Horizontal {
active: HorizontalPos::Bottom,
bottom: selected,
bottom_fill: selected_fill,
top_fill: other_fill,
..
}
| Self::Vertical {
active: VerticalPos::Left,
left: selected,
left_fill: selected_fill,
right_fill: other_fill,
..
}
| Self::Vertical {
active: VerticalPos::Right,
right: selected,
right_fill: selected_fill,
left_fill: other_fill,
..
} => {
if let Self::Single(buflist) = &mut **selected {
if let Some(ctx) = buflist.current_mut() {
f(selected_fill, *other_fill, ctx);
}
break;
} else {
current = selected;
}
}
}
}
}
fn select_by_index(&mut self, index: &mut usize) -> Result<(), ()> {
match self {
Self::Single(_) => {
if *index == 0 {
Ok(())
} else {
*index -= 1;
Err(())
}
}
Self::Horizontal {
top,
bottom,
active,
..
} => {
if top.select_by_index(index).is_ok() {
*active = HorizontalPos::Top;
Ok(())
} else if bottom.select_by_index(index).is_ok() {
*active = HorizontalPos::Bottom;
Ok(())
} else {
Err(())
}
}
Self::Vertical {
left,
right,
active,
..
} => {
if left.select_by_index(index).is_ok() {
*active = VerticalPos::Left;
Ok(())
} else if right.select_by_index(index).is_ok() {
*active = VerticalPos::Right;
Ok(())
} else {
Err(())
}
}
}
}
}
impl MultiBuffer<'_> for Layout {
type Matches = BTreeMap<usize, Vec<MultiCursor>>;
type Offsets = BTreeMap<usize, Vec<usize>>;
type Alt = ();
fn multi_insert_char(&mut self, _alt: Self::Alt, matches: &mut Self::Matches, c: char) {
self.on_global_at(matches, |buffer, alt, matches| {
buffer.multi_insert_char(alt, matches, c);
});
}
fn multi_insert_string(&mut self, _alt: Self::Alt, matches: &mut Self::Matches, s: &str) {
self.on_global_at(matches, |buffer, alt, matches| {
buffer.multi_insert_string(alt, matches, s);
});
}
fn multi_backspace(&mut self, _alt: Self::Alt, matches: &mut Self::Matches) {
self.on_global_at(matches, |buffer, alt, matches| {
buffer.multi_backspace(alt, matches);
})
}
fn multi_delete(&mut self, _alt: Self::Alt, matches: &mut Self::Matches) {
self.on_global_at(matches, |buffer, alt, matches| {
buffer.multi_delete(alt, matches);
});
}
fn delete_buffer(
&mut self,
matches: &mut Self::Matches,
match_idx: &mut usize,
) -> BufferDeleted {
use std::collections::btree_map::Entry;
let buffer_list = self.selected_buffer_list_mut();
let buffer_index = buffer_list.current_index();
let Entry::Occupied(mut buffer_matches) = matches.entry(buffer_index) else {
return BufferDeleted::BuffersRemain;
};
buffer_matches.get_mut().remove(*match_idx);
match buffer_matches.get().len().checked_sub(1) {
Some(max) => {
*match_idx = (*match_idx).min(max);
if let Some(buf) = buffer_list.get_mut(buffer_index)
&& let Some(m) = buffer_matches.get().get(*match_idx)
{
buf.set_cursor(m.cursor());
}
BufferDeleted::BuffersRemain
}
None => {
use core::ops::Bound;
buffer_matches.remove();
if let Some((next_idx, next_cursors)) = matches
.range((Bound::Excluded(buffer_index), Bound::Unbounded))
.next()
.or_else(|| matches.first_key_value())
&& let Some(r) = next_cursors.first()
&& let Ok(buffer) = buffer_list.select_buffer(*next_idx)
{
*match_idx = 0;
buffer.set_cursor(r.cursor());
BufferDeleted::BuffersRemain
} else {
BufferDeleted::NoBuffers
}
}
}
}
fn multi_select_inside(&mut self, matches: &mut Self::Matches, selected: usize) {
self.on_global(matches, |buffer, matches| {
buffer.multi_select_inside(matches, selected);
})
}
fn multi_cursor_back(&mut self, matches: &mut Self::Matches, selecting: bool) {
self.on_global(matches, |buffer, matches| {
buffer.multi_cursor_back(matches, selecting);
});
}
fn multi_cursor_forward(&mut self, matches: &mut Self::Matches, selecting: bool) {
self.on_global(matches, |buffer, matches| {
buffer.multi_cursor_forward(matches, selecting);
});
}
fn multi_cursor_home(&mut self, matches: &mut Self::Matches, selecting: bool) {
self.on_global(matches, |buffer, matches| {
buffer.multi_cursor_home(matches, selecting);
});
}
fn multi_cursor_end(&mut self, matches: &mut Self::Matches, selecting: bool) {
self.on_global(matches, |buffer, matches| {
buffer.multi_cursor_end(matches, selecting);
});
}
fn paste_group_count(matches: &Self::Matches) -> Option<NonZero<usize>> {
matches
.values()
.flat_map(|m| m.iter().map(|m| m.paste_group_count()))
.max()
.flatten()
}
fn multi_paste(
&mut self,
_alt: Self::Alt,
matches: &mut Self::Matches,
cut: &mut EditorCutBuffer,
) {
self.on_global_at(matches, |buf, alt, matches| {
buf.multi_paste(alt, matches, cut);
});
}
fn multi_cursor_copy(&mut self, matches: &mut Self::Matches) -> Vec<CutBuffer> {
let mut copied = vec![];
self.on_global(matches, |buffer, matches| {
copied.extend(BufferContext::multi_cursor_copy(buffer, matches));
});
copied
}
fn multi_cursor_cut(&mut self, _alt: Self::Alt, matches: &mut Self::Matches) -> Vec<CutBuffer> {
let mut cut = vec![];
self.on_global_at(matches, |buffer, alt, matches| {
cut.extend(BufferContext::multi_cursor_cut(buffer, alt, matches));
});
cut
}
fn multi_insert_group(
&mut self,
_alt: Self::Alt,
matches: &mut Self::Matches,
group_num: usize,
) {
self.on_global_at(matches, |buf, alt, matches| {
buf.multi_insert_group(alt, matches, group_num);
});
}
fn previous_match(&mut self, matches: &mut Self::Matches, match_idx: &mut usize) {
let buffer_list = self.selected_buffer_list_mut();
let buffer_index = buffer_list.current_index();
if let Some(buffer_cursors) = matches.get(&buffer_index) {
match match_idx.checked_sub(1) {
Some(new_idx) => {
*match_idx = new_idx;
if let Some(r) = buffer_cursors.get(new_idx)
&& let Some(buffer) = buffer_list.get_mut(buffer_index)
{
buffer.set_cursor(r.cursor());
}
}
None => {
use core::ops::Bound;
if let Some((prev_idx, prev_cursors)) = matches
.range((Bound::Unbounded, Bound::Excluded(buffer_index)))
.next_back()
.or_else(|| matches.last_key_value())
&& let Some(r) = prev_cursors.last()
&& let Ok(buffer) = buffer_list.select_buffer(*prev_idx)
{
*match_idx = prev_cursors.len() - 1;
buffer.set_cursor(r.cursor());
}
}
}
}
}
fn next_match(&mut self, matches: &mut Self::Matches, match_idx: &mut usize) {
let buffer_list = self.selected_buffer_list_mut();
let buffer_index = buffer_list.current_index();
if let Some(buffer_cursors) = matches.get(&buffer_index) {
if (*match_idx + 1) < buffer_cursors.len() {
*match_idx += 1;
if let Some(r) = buffer_cursors.get(*match_idx)
&& let Some(buffer) = buffer_list.get_mut(buffer_index)
{
buffer.set_cursor(r.cursor());
}
} else {
use core::ops::Bound;
if let Some((next_idx, next_cursors)) = matches
.range((Bound::Excluded(buffer_index), Bound::Unbounded))
.next()
.or_else(|| matches.first_key_value())
&& let Some(r) = next_cursors.first()
&& let Ok(buffer) = buffer_list.select_buffer(*next_idx)
{
*match_idx = 0;
buffer.set_cursor(r.cursor());
}
}
}
}
fn multi_cursor_widen(&mut self, matches: &mut Self::Matches) {
self.on_global(matches, |buffer, matches| {
buffer.multi_cursor_widen(matches);
});
}
fn toggle_bookmarks(&mut self, matches: &mut Self::Matches) -> ToggledBookmarks {
let mut toggled = ToggledBookmarks::default();
self.on_global(matches, |buffer, matches| {
toggled += buffer.toggle_bookmarks(matches.iter().map(|m| m.cursor()));
});
toggled
}
fn multi_autocomplete_matches(
&self,
matches: &mut Self::Matches,
) -> Option<(Self::Offsets, Vec<String>)> {
self.selected_buffer_list()
.multi_autocomplete_matches(matches)
}
fn multi_autocomplete(
&mut self,
_alt: Self::Alt,
matches: &mut Self::Matches,
offsets: &Self::Offsets,
original: &str,
replacement: &str,
) {
self.on_global_offset_at(matches, offsets, |buffer, alt, matches, offsets| {
buffer.multi_autocomplete(alt, matches, offsets, original, replacement);
});
}
fn perform_undo(&mut self, _alt: Self::Alt, matches: &mut Self::Matches) {
self.on_global_at(matches, |buf, alt, matches| {
BufferContext::perform_multi_undo(buf, alt, matches);
});
}
fn perform_redo(&mut self, _alt: Self::Alt, matches: &mut Self::Matches) {
self.on_global_at(matches, |buf, alt, matches| {
BufferContext::perform_multi_undo(buf, alt, matches);
});
}
fn set_buffer_message(&mut self, message: BufferMessage) {
if let Some(buf) = self.selected_buffer_list_mut().current_mut() {
buf.set_buffer_message(message);
}
}
fn set_message<S: Into<Cow<'static, str>>>(&mut self, msg: S) {
if let Some(buf) = self.selected_buffer_list_mut().current_mut() {
buf.set_message(msg)
}
}
fn set_error<S: Into<Cow<'static, str>>>(&mut self, msg: S) {
if let Some(buf) = self.selected_buffer_list_mut().current_mut() {
buf.set_error(msg)
}
}
}
#[derive(Eq, PartialEq, Hash)]
enum Direction {
Up,
Down,
Left,
Right,
}
struct EditorWidget<'e> {
focused: bool,
mode: &'e mut EditorMode,
show_help: bool,
show_sub_help: bool,
}
impl StatefulWidget for EditorWidget<'_> {
type State = Layout;
fn render(
self,
mut area: ratatui::layout::Rect,
buf: &mut ratatui::buffer::Buffer,
layout: &mut Layout,
) {
fn highlight_selected(
tabs: Vec<String>,
matches: &BTreeMap<usize, Vec<MultiCursor>>,
) -> ratatui::widgets::Tabs<'_> {
use ratatui::style::Style;
use ratatui::text::{Line, Span};
ratatui::widgets::Tabs::new(tabs.into_iter().enumerate().map(|(buf_idx, tab)| {
if matches.contains_key(&buf_idx) {
Line::from(Span::styled(tab, Style::new().blue()))
} else {
Line::from(tab)
}
}))
}
let Self {
mode,
show_help,
show_sub_help,
focused,
} = self;
if let Some((index, tabs)) = layout.selected_buffer_list().tabs() {
use ratatui::{
layout::{
Constraint::{Length, Min},
Layout,
},
style::Style,
widgets::{Tabs, Widget},
};
let [tabs_area, layout_area] = Layout::vertical([Length(1), Min(0)]).areas(area);
(match mode {
EditorMode::AllBuffers {
cursors: MultiCursors { matches, .. },
} => highlight_selected(tabs, matches),
_ => Tabs::new(tabs),
})
.highlight_style(if self.focused {
Style::default().bold()
} else {
Style::default()
})
.divider("")
.padding("", "")
.select(index)
.render(tabs_area, buf);
area = layout_area;
}
let multiple_panes = !matches!(layout, Layout::Single(_));
match matches!(&mode, EditorMode::SplitPane) {
true => LayoutWidget {
mode,
show_help,
show_sub_help,
focused,
multiple_panes,
pane_indices: &mut ('1'..='9').chain(std::iter::once('0').chain('A'..='Z')),
}
.render(area, buf, layout),
false => LayoutWidget {
mode,
show_help,
show_sub_help,
focused,
multiple_panes,
pane_indices: &mut std::iter::empty(),
}
.render(area, buf, layout),
}
}
}
struct LayoutWidget<'e, 'i, I> {
focused: bool,
mode: &'e mut EditorMode,
show_help: bool,
show_sub_help: bool,
multiple_panes: bool,
pane_indices: &'i mut I,
}
impl<I: std::iter::FusedIterator<Item = char>> StatefulWidget for LayoutWidget<'_, '_, I> {
type State = Layout;
fn render(
self,
area: ratatui::layout::Rect,
buf: &mut ratatui::buffer::Buffer,
layout: &mut Layout,
) {
use crate::buffer::BufferWidget;
let Self {
mode,
show_help,
show_sub_help,
focused,
multiple_panes,
pane_indices,
} = self;
match layout {
Layout::Single(single) => {
let multiple_buffers = single.multiple_buffers();
let buffer_idx = single.current_index();
if let Some(buffer) = single.current_mut() {
BufferWidget {
focused,
mode: focused.then_some(mode),
show_help: show_help
.then(|| buffer.help_options(multiple_buffers, multiple_panes)),
show_sub_help,
buffer_idx,
pane_idx: pane_indices.next(),
}
.render(area, buf, buffer);
}
}
Layout::Horizontal {
active,
top,
bottom,
top_fill,
bottom_fill,
} => {
use ratatui::layout::{Constraint, Layout};
let [top_area, bottom_area] =
Layout::vertical(Constraint::from_fills([*top_fill, *bottom_fill])).areas(area);
match active {
HorizontalPos::Top => {
LayoutWidget {
focused,
mode,
show_help,
show_sub_help,
multiple_panes,
pane_indices,
}
.render(top_area, buf, top);
LayoutWidget {
focused: false,
mode,
show_help: false,
show_sub_help: false,
multiple_panes: false,
pane_indices,
}
.render(bottom_area, buf, bottom);
}
HorizontalPos::Bottom => {
LayoutWidget {
focused: false,
mode,
show_help: false,
show_sub_help: false,
multiple_panes: false,
pane_indices,
}
.render(top_area, buf, top);
LayoutWidget {
focused,
mode,
show_help,
show_sub_help,
multiple_panes,
pane_indices,
}
.render(bottom_area, buf, bottom);
}
}
}
Layout::Vertical {
active,
left,
right,
left_fill,
right_fill,
} => {
use ratatui::layout::{Constraint, Layout};
let [left_area, right_area] =
Layout::horizontal(Constraint::from_fills([*left_fill, *right_fill]))
.areas(area);
match active {
VerticalPos::Left => {
LayoutWidget {
focused,
mode,
show_help,
show_sub_help,
multiple_panes,
pane_indices,
}
.render(left_area, buf, left);
LayoutWidget {
focused: false,
mode,
show_help: false,
show_sub_help: false,
multiple_panes: false,
pane_indices,
}
.render(right_area, buf, right);
}
VerticalPos::Right => {
LayoutWidget {
focused: false,
mode,
show_help: false,
show_sub_help: false,
multiple_panes: false,
pane_indices,
}
.render(left_area, buf, left);
LayoutWidget {
focused,
mode,
show_help,
show_sub_help,
multiple_panes,
pane_indices,
}
.render(right_area, buf, right);
}
}
}
}
}
}
#[derive(Clone, Debug)]
pub struct LineNumber {
line: usize,
column: Option<usize>,
}
impl std::str::FromStr for LineNumber {
type Err = InvalidLine;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once(':') {
Some((line, column)) => Ok(Self {
line: line
.parse::<usize>()
.map_err(|_| InvalidLine)?
.saturating_sub(1),
column: Some(
column
.parse::<usize>()
.map_err(|_| InvalidLine)?
.saturating_sub(1),
),
}),
None => Ok(Self {
line: s
.parse::<usize>()
.map_err(|_| InvalidLine)?
.saturating_sub(1),
column: None,
}),
}
}
}
#[derive(Debug)]
pub struct InvalidLine;
impl std::error::Error for InvalidLine {}
impl std::fmt::Display for InvalidLine {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
"invalid line".fmt(f)
}
}
fn init_complete_forward<T, R>(completions: &[T]) -> Option<(usize, &R, &R)>
where
T: AsRef<R>,
R: ?Sized,
{
if let Some(original) = completions.first()
&& let Some(replacement) = completions.get(1)
{
Some((1, original.as_ref(), replacement.as_ref()))
} else {
None
}
}
fn complete_forward<'c, T, R>(index: &mut usize, completions: &'c [T]) -> (&'c R, &'c R)
where
T: AsRef<R>,
R: ?Sized,
{
let next_index = (*index + 1) % completions.len();
(
completions[std::mem::replace(index, next_index)].as_ref(),
completions[next_index].as_ref(),
)
}
fn init_complete_backward<T, R>(completions: &[T]) -> Option<(usize, &R, &R)>
where
T: AsRef<R>,
R: ?Sized,
{
if let Some(original) = completions.first()
&& let Some(index) = completions.len().checked_sub(1)
&& index != 0
&& let Some(replacement) = completions.get(index)
{
Some((index, original.as_ref(), replacement.as_ref()))
} else {
None
}
}
fn complete_backward<'c, T, R>(index: &mut usize, completions: &'c [T]) -> (&'c R, &'c R)
where
T: AsRef<R>,
R: ?Sized,
{
let previous_index = index.checked_sub(1).unwrap_or(completions.len() - 1);
(
completions[std::mem::replace(index, previous_index)].as_ref(),
completions[previous_index].as_ref(),
)
}
fn char_to_index(c: char) -> Option<usize> {
match c {
c @ '1'..='9' => Some((u32::from(c) - u32::from('1')) as usize),
'0' => Some(9),
c @ 'a'..='z' => Some((u32::from(c) - u32::from('a')) as usize + 10),
c @ 'A'..='Z' => Some((u32::from(c) - u32::from('A')) as usize + 10),
_ => None,
}
}
fn set_title<D: std::fmt::Display>(d: D) {
use crossterm::{execute, terminal::SetTitle};
let _ = execute!(std::io::stdout(), SetTitle(format!("vle {d}")),);
}
fn concat_vec<T>(mut a: Vec<T>, b: Vec<T>) -> Vec<T> {
if a.is_empty() {
b
} else {
a.extend(b);
a
}
}
fn copy_to_system_clipboard(selection: &str) {
use crossterm::{clipboard::CopyToClipboard, execute};
let _ = execute!(
std::io::stdout(),
CopyToClipboard::to_clipboard_from(selection)
);
}
fn same<T: Eq>(iter: impl IntoIterator<Item = T>) -> Option<T> {
let mut iter = iter.into_iter();
let first = iter.next()?;
iter.all(|i| i == first).then_some(first)
}