use crate::editor::{EditorMode, MultiCursorMode, MultiCursors, Search, SearchMode};
use crate::endings::LineEndings;
use crate::syntax::Syntax;
use radix_trie::Trie;
use ratatui::{
layout::{Position, Rect},
widgets::StatefulWidget,
};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::num::NonZero;
use std::ops::Range;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::LazyLock;
use std::time::SystemTime;
pub static SPACES_PER_TAB: LazyLock<usize> = LazyLock::new(|| {
std::env::var("VLE_SPACES_PER_TAB")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.map(|s| s.clamp(1, 16))
.unwrap_or(4)
});
pub static TAB_SUBSTITUTION: LazyLock<String> =
LazyLock::new(|| std::iter::repeat_n(' ', *SPACES_PER_TAB).collect());
static ALWAYS_TAB: LazyLock<bool> = LazyLock::new(|| std::env::var("VLE_ALWAYS_TAB").is_ok());
pub enum Source {
Local(PathBuf),
Scratch {
path: PathBuf,
data: ropey::Rope,
},
#[cfg(feature = "ssh")]
Ssh {
sftp: Rc<ssh2::Sftp>,
path: PathBuf,
},
Tutorial,
Test,
}
impl PartialEq for Source {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Local(x), Self::Local(y))
| (Self::Scratch { path: x, .. }, Self::Scratch { path: y, .. }) => x == y,
#[cfg(feature = "ssh")]
(Self::Ssh { sftp: s1, path: x }, Self::Ssh { sftp: s2, path: y }) => {
Rc::ptr_eq(s1, s2) && x == y
}
(Self::Tutorial, Self::Tutorial) => true,
(Self::Test, Self::Test) => true,
_ => false,
}
}
}
impl Eq for Source {}
impl From<PathBuf> for Source {
fn from(s: PathBuf) -> Self {
Self::Local(s)
}
}
impl Source {
fn name(&self) -> Cow<'_, str> {
match self {
Self::Local(path) | Self::Scratch { path, .. } => path.to_string_lossy(),
#[cfg(feature = "ssh")]
Self::Ssh { path, .. } => path.to_string_lossy(),
Self::Tutorial => "Welcome!".into(),
Self::Test => "Terminal Test".into(),
}
}
fn short_name(&self) -> Cow<'_, str> {
match self {
Self::Local(path) | Self::Scratch { path, .. } => path
.file_prefix()
.map(|s| s.to_string_lossy())
.unwrap_or_else(|| "???".into()),
#[cfg(feature = "ssh")]
Self::Ssh { path, .. } => path
.file_prefix()
.map(|s| s.to_string_lossy())
.unwrap_or_else(|| "???".into()),
Self::Tutorial => "Welcome!".into(),
Self::Test => "Terminal Test".into(),
}
}
pub fn file_name(&self) -> Option<Cow<'_, str>> {
match self {
Self::Local(path) | Self::Scratch { path, .. } => {
path.file_name().map(|s| s.to_string_lossy())
}
#[cfg(feature = "ssh")]
Self::Ssh { path, .. } => path.file_name().map(|s| s.to_string_lossy()),
Self::Tutorial => None,
Self::Test => None,
}
}
pub fn extension(&self) -> Option<&str> {
match self {
Self::Local(path) | Self::Scratch { path, .. } => {
path.extension().and_then(|s| s.to_str())
}
#[cfg(feature = "ssh")]
Self::Ssh { path, .. } => path.extension().and_then(|s| s.to_str()),
Self::Tutorial => None,
Self::Test => None,
}
}
fn read_string(&self, endings: LineEndings) -> std::io::Result<(Option<SystemTime>, String)> {
match self {
Self::Local(path) => {
let s = std::fs::File::open(path).and_then(|f| endings.reader_to_string(f))?;
Ok((path.metadata().and_then(|m| m.modified()).ok(), s))
}
Self::Scratch { data, .. } => Ok((None, data.clone().into())),
#[cfg(feature = "ssh")]
Self::Ssh { sftp, path } => match sftp.open(path) {
Ok(mut f) => {
let s = endings.reader_to_string(&mut f)?;
Ok((
f.stat().ok().and_then(|stat| stat.mtime).and_then(|secs| {
SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(secs))
}),
s,
))
}
Err(e) => Err(e.into()),
},
Self::Tutorial => Ok((
None,
include_str!("tutorial.txt").replacen("VERSION", env!("CARGO_PKG_VERSION"), 1),
)),
Self::Test => Ok((None, include_str!("test.txt").to_string())),
}
}
fn read_data(&self) -> std::io::Result<(Option<SystemTime>, ropey::Rope, LineEndings)> {
use std::fs::File;
match self {
Self::Local(path) => match File::open(path) {
Ok(mut f) => {
let (endings, rope) = LineEndings::reader_to_rope(&mut f)?;
Ok((f.metadata().and_then(|m| m.modified()).ok(), rope, endings))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Ok((None, "\n".into(), LineEndings::default()))
}
Err(e) => Err(e),
},
Self::Scratch { data, .. } => Ok((None, data.clone(), LineEndings::default())),
#[cfg(feature = "ssh")]
Self::Ssh { sftp, path } => match sftp.open(path) {
Ok(mut f) => {
let (endings, rope) = LineEndings::reader_to_rope(&mut f)?;
Ok((
f.stat().ok().and_then(|stat| stat.mtime).and_then(|secs| {
SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(secs))
}),
rope,
endings,
))
}
Err(e) if e.code() == ssh2::ErrorCode::SFTP(2) => {
Ok((None, "\n".into(), LineEndings::default()))
}
Err(e) => Err(e.into()),
},
Self::Tutorial | Self::Test => self
.read_string(LineEndings::default())
.map(|(t, s)| (t, ropey::Rope::from(s), LineEndings::default())),
}
}
fn save_data(
&mut self,
data: &ropey::Rope,
endings: LineEndings,
) -> std::io::Result<Option<SystemTime>> {
use std::fs::File;
use std::io::{BufWriter, Write};
match self {
Self::Local(path) => File::create(path).map(BufWriter::new).and_then(|mut f| {
endings.rope_to_writer(data, &mut f)?;
f.flush()?;
Ok(f.get_mut().metadata().and_then(|m| m.modified()).ok())
}),
Self::Scratch { data: scratch, .. } => {
*scratch = data.clone();
Ok(None)
}
#[cfg(feature = "ssh")]
Self::Ssh { sftp, path } => match sftp.create(path) {
Ok(mut f) => {
endings.rope_to_writer(data, &mut f)?;
f.flush()?;
Ok(f.stat().ok().and_then(|stat| stat.mtime).and_then(|secs| {
SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(secs))
}))
}
Err(e) => Err(e.into()),
},
Self::Tutorial | Self::Test => Ok(None),
}
}
fn last_modified(&self) -> Option<SystemTime> {
match self {
Self::Local(path) => path.metadata().and_then(|m| m.modified()).ok(),
#[cfg(feature = "ssh")]
Self::Ssh { sftp, path } => {
sftp.stat(path)
.ok()
.and_then(|stat| stat.mtime)
.and_then(|secs| {
SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(secs))
})
}
Self::Tutorial | Self::Test | Self::Scratch { .. } => None,
}
}
pub fn contains<S: SearchTerm>(&self, term: S) -> bool {
use std::io::BufRead;
match self {
Self::Local(path) => match std::fs::File::open(path) {
Ok(f) => std::io::BufReader::new(f)
.lines()
.map_while(Result::ok)
.any(|l| term.match_ranges(&l).next().is_some()),
Err(_) => false,
},
Self::Scratch { data, .. } => data
.lines()
.any(|l| term.match_ranges(&Cow::from(l)).next().is_some()),
#[cfg(feature = "ssh")]
Self::Ssh { sftp, path } => match sftp.open(path) {
Ok(f) => std::io::BufReader::new(f)
.lines()
.map_while(Result::ok)
.any(|l| term.match_ranges(&l).next().is_some()),
Err(_) => false,
},
Self::Tutorial => include_str!("tutorial.txt")
.replacen("VERSION", env!("CARGO_PKG_VERSION"), 1)
.lines()
.any(|l| term.match_ranges(l).next().is_some()),
Self::Test => include_str!("test.txt")
.lines()
.any(|l| term.match_ranges(l).next().is_some()),
}
}
pub fn contains_multiline<S: SearchTerm>(&self, term: S) -> bool {
match self.read_data() {
Ok((_, rope, _)) => term.match_ranges(&String::from(rope)).next().is_some(),
Err(_) => false,
}
}
}
mod private {
use crate::buffer::{AltCursor, Buffer, MainCursor, MultiCursor, Toggle};
use ratatui::text::Span;
use std::borrow::Cow;
use std::cell::{Ref, RefCell, RefMut};
use std::collections::{BTreeMap, VecDeque};
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
pub struct Rope {
rope: ropey::Rope, saved: ropey::Rope, modified: bool, }
impl From<ropey::Rope> for Rope {
fn from(rope: ropey::Rope) -> Self {
Self {
saved: rope.clone(),
rope,
modified: false,
}
}
}
impl Rope {
pub fn modified(&self) -> bool {
self.modified
}
pub fn save(&mut self) {
self.saved = self.rope.clone();
self.modified = false;
}
pub fn get_mut(&mut self) -> RopeHandle<'_> {
RopeHandle {
rope: &mut self.rope,
saved: &mut self.saved,
modified: &mut self.modified,
}
}
}
impl Deref for Rope {
type Target = ropey::Rope;
fn deref(&self) -> &ropey::Rope {
&self.rope
}
}
pub struct RopeHandle<'r> {
rope: &'r mut ropey::Rope,
saved: &'r mut ropey::Rope,
modified: &'r mut bool,
}
impl Deref for RopeHandle<'_> {
type Target = ropey::Rope;
fn deref(&self) -> &ropey::Rope {
self.rope
}
}
impl DerefMut for RopeHandle<'_> {
fn deref_mut(&mut self) -> &mut ropey::Rope {
self.rope
}
}
impl std::ops::Drop for RopeHandle<'_> {
fn drop(&mut self) {
*self.modified = self.rope != self.saved;
}
}
#[derive(Clone)]
pub struct BufferCell(Rc<RefCell<Buffer>>);
impl BufferCell {
pub fn id(&self) -> crate::buffer::BufferId {
crate::buffer::BufferId(Rc::clone(&self.0))
}
pub fn borrow_mut(&self) -> RefMut<'_, Buffer> {
self.0.borrow_mut()
}
pub fn borrow(&self) -> Ref<'_, Buffer> {
self.0.borrow()
}
pub fn borrow_update(
&self,
main: MainCursor<'_>,
alt: &mut [AltCursor<'_>],
) -> RefMut<'_, Buffer> {
let mut buf = self.0.borrow_mut();
if buf.perform_update() {
main.perform_update();
alt.iter_mut().for_each(|a| a.perform_update());
}
buf
}
pub fn borrow_multi_update(
&self,
main: MainCursor<'_>,
alt: &mut [AltCursor<'_>],
cursors: &mut [MultiCursor],
) -> RefMut<'_, Buffer> {
let mut buf = self.0.borrow_mut();
if buf.perform_update() {
main.perform_update();
alt.iter_mut().for_each(|a| a.perform_update());
cursors.iter_mut().for_each(|m| m.perform_update());
}
buf
}
pub fn borrow_move(&self) -> MoveHandle<'_> {
MoveHandle(self.0.borrow_mut())
}
}
impl From<Buffer> for BufferCell {
fn from(buffer: Buffer) -> Self {
BufferCell(Rc::new(RefCell::new(buffer)))
}
}
pub struct MoveHandle<'b>(RefMut<'b, Buffer>);
impl Deref for MoveHandle<'_> {
type Target = Buffer;
fn deref(&self) -> &Buffer {
&self.0
}
}
impl DerefMut for MoveHandle<'_> {
fn deref_mut(&mut self) -> &mut Buffer {
&mut self.0
}
}
impl Drop for MoveHandle<'_> {
fn drop(&mut self) {
self.0.undo_finished = true;
}
}
#[derive(Clone, Default)]
pub struct Bookmarks(BTreeMap<usize, ()>);
impl Bookmarks {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter(&self) -> impl Iterator<Item = usize> {
self.0.keys().copied()
}
pub fn range<R>(&self, range: R) -> impl DoubleEndedIterator<Item = usize>
where
R: std::ops::RangeBounds<usize>,
{
self.0.range(range).map(|(b, ())| *b)
}
pub fn toggle(&mut self, cursor: usize) -> Toggle {
use std::collections::btree_map::Entry;
match self.0.entry(cursor) {
Entry::Vacant(v) => {
v.insert(());
Toggle::Inserted
}
Entry::Occupied(o) => {
let () = o.remove();
Toggle::Removed
}
}
}
pub fn remove(&mut self, cursor: usize) -> Result<(), ()> {
self.0.remove(&cursor).ok_or(())
}
pub fn next_after(&self, cursor: usize) -> Option<usize> {
use std::ops::Bound;
self.0
.range((Bound::Excluded(cursor), Bound::Unbounded))
.next()
.or_else(|| self.0.first_key_value())
.map(|(k, ())| k)
.copied()
}
pub fn next_before(&self, cursor: usize) -> Option<usize> {
use std::ops::Bound;
self.0
.range((Bound::Unbounded, Bound::Excluded(cursor)))
.next_back()
.or_else(|| self.0.last_key_value())
.map(|(k, ())| k)
.copied()
}
pub fn get_mut(&mut self) -> BookmarksHandle<'_> {
BookmarksHandle(&mut self.0)
}
}
pub struct BookmarksHandle<'m>(&'m mut BTreeMap<usize, ()>);
impl BookmarksHandle<'_> {
pub fn update_ge(&mut self, cursor: usize, mut update: impl FnMut(&mut usize)) {
let mut updated = self
.0
.extract_if(cursor.., |_, _| true)
.map(|(mut pos, ())| {
update(&mut pos);
(pos, ())
})
.collect();
self.0.append(&mut updated);
}
pub fn remove<R>(&mut self, range: R)
where
R: std::ops::RangeBounds<usize>,
{
self.0.extract_if(range, |_, _| true).for_each(drop);
}
pub fn extract<R>(&mut self, range: R) -> impl Iterator<Item = usize>
where
R: std::ops::RangeBounds<usize>,
{
self.0.extract_if(range, |_, _| true).map(|(b, ())| b)
}
pub fn add(&mut self, bookmarks: impl Iterator<Item = usize>) {
self.0.extend(bookmarks.into_iter().map(|b| (b, ())));
}
}
pub struct Secondary<'a, 'b> {
alt_cursor_selections: Vec<(&'a mut usize, Option<&'a mut usize>)>,
bookmarks: BookmarksHandle<'b>,
cursor: usize,
}
impl<'a, 'b> Secondary<'a, 'b> {
pub fn new(alt: Vec<AltCursor<'a>>, bookmarks: BookmarksHandle<'b>) -> Self {
Self {
alt_cursor_selections: alt
.into_iter()
.map(|a| (a.cursor, a.selection.as_mut()))
.collect(),
bookmarks,
cursor: 0,
}
}
pub fn ge(alt: Vec<AltCursor<'a>>, bookmarks: BookmarksHandle<'b>, cursor: usize) -> Self {
Self::filtered(alt, bookmarks, cursor, |a| a >= cursor)
}
pub fn gt(alt: Vec<AltCursor<'a>>, bookmarks: BookmarksHandle<'b>, cursor: usize) -> Self {
Self::filtered(alt, bookmarks, cursor, |a| a > cursor)
}
fn filtered(
alt: Vec<AltCursor<'a>>,
bookmarks: BookmarksHandle<'b>,
cursor: usize,
mut f: impl FnMut(usize) -> bool,
) -> Self {
Self {
alt_cursor_selections: alt
.into_iter()
.filter_map(|a| {
Some((
f(*a.cursor).then_some(a.cursor)?,
a.selection.as_mut().filter(|s| f(**s)),
))
})
.collect(),
bookmarks,
cursor,
}
}
pub fn update(&mut self, mut f: impl FnMut(&mut usize)) {
self.alt_cursor_selections
.iter_mut()
.for_each(|(cursor, selection)| {
f(cursor);
if let Some(selection) = selection {
f(selection);
}
});
self.bookmarks.update_ge(self.cursor, f);
}
pub fn add_bookmarks(&mut self, bookmarks: impl Iterator<Item = usize>) {
self.bookmarks.add(bookmarks);
}
pub fn extract_bookmarks<R>(&mut self, range: R) -> impl Iterator<Item = usize>
where
R: std::ops::RangeBounds<usize>,
{
self.bookmarks.extract(range)
}
pub fn remove<R>(&mut self, range: R) -> R
where
R: std::ops::RangeBounds<usize> + Clone,
{
self.bookmarks.remove(range.clone());
range
}
pub fn inc(&mut self, chars: usize) -> usize {
self.update(|a| *a += chars);
chars
}
}
impl std::ops::AddAssign<usize> for Secondary<'_, '_> {
fn add_assign(&mut self, rhs: usize) {
self.update(|c| {
*c += rhs;
})
}
}
impl std::ops::SubAssign<usize> for Secondary<'_, '_> {
fn sub_assign(&mut self, rhs: usize) {
self.update(|c| {
*c -= rhs;
})
}
}
pub struct SpanDeque<'q, 's> {
spans: &'q mut VecDeque<Span<'s>>,
queued: usize,
}
impl<'q, 's> SpanDeque<'q, 's> {
pub fn new(spans: &'q mut VecDeque<Span<'s>>) -> Self {
Self {
queued: spans.len(),
spans,
}
}
fn pop_front(&mut self) -> Option<Span<'s>> {
let queued = self.queued.checked_sub(1)?;
let span = self.spans.pop_front()?;
self.queued = queued;
Some(span)
}
fn push_front(&mut self, span: Span<'s>) {
self.queued += 1;
self.spans.push_front(span);
}
fn push_back(&mut self, span: Span<'s>) {
self.spans.push_back(span);
}
pub fn extract(&mut self, mut characters: usize, map: impl Fn(Span<'s>) -> Span<'s>) {
fn nth_or<T, I>(mut iter: I, mut n: usize) -> Result<T, usize>
where
I: Iterator<Item = T>,
{
while n > 0 {
iter.next().ok_or(n)?;
n -= 1;
}
iter.next().ok_or(0)
}
fn split_cow(s: Cow<'_, str>, split_point: usize) -> (Cow<'_, str>, Cow<'_, str>) {
match s {
Cow::Borrowed(slice) => {
let (start, end) = slice.split_at(split_point);
(Cow::Borrowed(start), Cow::Borrowed(end))
}
Cow::Owned(mut string) => {
let suffix = string.split_off(split_point);
(Cow::Owned(string), Cow::Owned(suffix))
}
}
}
while characters > 0 {
let Some(span) = self.pop_front() else {
return;
};
match nth_or(span.content.char_indices(), characters) {
Ok((split_point, _)) => {
let (prefix, suffix) = split_cow(span.content, split_point);
self.push_front(Span {
style: span.style,
content: suffix,
});
self.push_back(map(Span {
style: span.style,
content: prefix,
}));
return;
}
Err(c) => {
self.push_back(map(span));
characters = c;
}
}
}
}
pub fn extract_bytes(&mut self, mut bytes: usize, map: impl Fn(Span<'s>) -> Span<'s>) {
fn split_cow(s: Cow<'_, str>, bytes: usize) -> (Cow<'_, str>, Cow<'_, str>) {
let split_point = if bytes < s.len() {
bytes
} else {
return (s, "".into());
};
match s {
Cow::Borrowed(slice) => {
let (start, end) = slice.split_at(split_point);
(Cow::Borrowed(start), Cow::Borrowed(end))
}
Cow::Owned(mut string) => {
let suffix = string.split_off(split_point);
(Cow::Owned(string), Cow::Owned(suffix))
}
}
}
while bytes > 0 {
let Some(span) = self.pop_front() else {
return;
};
let span_width = span.content.len();
if span_width <= bytes {
bytes -= span_width;
self.push_back(map(span));
} else {
let (prefix, suffix) = split_cow(span.content, bytes);
self.push_front(Span {
style: span.style,
content: suffix,
});
self.push_back(map(Span {
style: span.style,
content: prefix,
}));
return;
}
}
}
}
impl<'q, 's> Drop for SpanDeque<'q, 's> {
fn drop(&mut self) {
self.spans.rotate_left(self.queued);
}
}
}
use private::Secondary;
pub struct Buffer {
source: Source, endings: LineEndings, saved: Option<SystemTime>, rope: private::Rope, undo: Vec<BufferState>, undo_finished: bool, redo: Vec<BufferState>, syntax: Box<dyn Syntax>, tabs_required: bool, tab_substitution: String, bookmarks: private::Bookmarks, }
impl Buffer {
fn source(&self) -> &Source {
&self.source
}
fn open(source: Source) -> std::io::Result<Self> {
let (saved, rope, endings) = source.read_data()?;
let syntax = crate::syntax::syntax(&source);
Ok(Self {
tab_substitution: std::iter::repeat_n(' ', *SPACES_PER_TAB).collect(),
rope: rope.into(),
endings,
saved,
tabs_required: *ALWAYS_TAB || syntax.tabs_required(),
syntax,
source,
undo: vec![],
undo_finished: true,
redo: vec![],
bookmarks: private::Bookmarks::default(),
})
}
fn tutorial() -> Self {
Self {
rope: ropey::Rope::from(include_str!("tutorial.txt").replacen(
"VERSION",
env!("CARGO_PKG_VERSION"),
1,
))
.into(),
endings: LineEndings::default(),
saved: None,
syntax: Box::new(crate::syntax::Tutorial),
tab_substitution: std::iter::repeat_n(' ', *SPACES_PER_TAB).collect(),
tabs_required: *ALWAYS_TAB || crate::syntax::Tutorial.tabs_required(),
source: Source::Tutorial,
undo: vec![],
undo_finished: true,
redo: vec![],
bookmarks: private::Bookmarks::default(),
}
}
fn reload(
&mut self,
cursor: &mut usize,
selection: &mut Option<usize>,
alt: Vec<AltCursor<'_>>,
) -> std::io::Result<()> {
let (saved, reloaded) = self.source.read_string(self.endings)?;
patch_rope(
&mut self.rope.get_mut(),
reloaded,
cursor,
selection,
Secondary::new(alt, self.bookmarks.get_mut()),
);
self.rope.save();
self.saved = saved;
self.undo_finished = true;
Ok(())
}
fn save(&mut self) -> std::io::Result<()> {
self.saved = {
let mut rope = self.rope.get_mut();
let len_chars = rope.len_chars();
if let Some(last_char) = len_chars.checked_sub(1)
&& rope.get_char(last_char) != Some('\n')
{
rope.insert_char(len_chars, '\n');
}
self.source.save_data(&rope, self.endings)?
};
self.rope.save();
self.undo_finished = true;
Ok(())
}
fn total_lines(&self) -> usize {
self.rope.len_lines()
}
pub fn modified(&self) -> bool {
self.rope.modified()
}
pub fn last_modified(&self) -> Option<SystemTime> {
self.source.last_modified()
}
pub fn last_saved(&self) -> Option<SystemTime> {
self.saved
}
pub fn rope_bookmarks_mut(
&mut self,
) -> (private::RopeHandle<'_>, private::BookmarksHandle<'_>) {
(self.rope.get_mut(), self.bookmarks.get_mut())
}
pub fn has_bookmarks(&self) -> bool {
!self.bookmarks.is_empty()
}
pub fn perform_update(&mut self) -> bool {
if std::mem::take(&mut self.undo_finished) {
self.undo.push(BufferState {
rope: self.rope.clone(),
bookmarks: self.bookmarks.clone(),
});
self.redo.clear();
true
} else {
false
}
}
}
#[derive(Clone)]
pub struct BufferId(Rc<RefCell<Buffer>>);
impl Eq for BufferId {}
impl PartialEq for BufferId {
fn eq(&self, rhs: &BufferId) -> bool {
Rc::ptr_eq(&self.0, &rhs.0)
}
}
impl std::hash::Hash for BufferId {
fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
std::ptr::hash(Rc::as_ptr(&self.0), h);
}
}
impl std::fmt::Display for BufferId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.borrow().source().name().fmt(f)
}
}
pub enum FindMode {
WholeFile,
Selected,
InSelection,
}
impl From<FindMode> for crate::help::Keybinding {
fn from(mode: FindMode) -> Self {
crate::help::keybind::<crate::key::Find>(match mode {
FindMode::WholeFile => "Find in File",
FindMode::Selected => "Find Selected Text",
FindMode::InSelection => "Find in Selected Lines",
})
}
}
#[derive(Copy, Clone, Default)]
pub enum CursorPos {
#[default]
Other, InWord, AfterWord, AtParen, }
pub struct Help {
find: FindMode,
cursor_pos: CursorPos,
has_bookmarks: bool,
has_selection: bool,
multiple_buffers: bool,
multiple_panes: bool,
}
macro_rules! borrow_update {
($self:ident, $alt:ident) => {
$self.buffer.borrow_update(
MainCursor {
cursor: $self.cursor,
cursor_column: $self.cursor_column,
selection: $self.selection,
undo: &mut $self.undo,
redo: &mut $self.redo,
},
&mut $alt,
)
};
}
macro_rules! borrow_multi_update {
($self:ident, $alt:ident, $matches:ident) => {
$self.buffer.borrow_multi_update(
MainCursor {
cursor: $self.cursor,
cursor_column: $self.cursor_column,
selection: $self.selection,
undo: &mut $self.undo,
redo: &mut $self.redo,
},
&mut $alt,
$matches,
)
};
}
#[derive(Clone)]
pub struct BufferContext {
buffer: private::BufferCell, cursor: usize, cursor_column: usize, selection: Option<usize>, message: Option<BufferMessage>, undo: Vec<BufferContextState>, redo: Vec<BufferContextState>, }
impl BufferContext {
pub fn id(&self) -> BufferId {
self.buffer.id()
}
pub fn modified(&self) -> bool {
self.buffer.borrow().modified()
}
pub fn open(source: Source) -> std::io::Result<Self> {
Buffer::open(source).map(|b| b.into())
}
pub fn reload(&mut self, alt: Vec<AltCursor<'_>>) -> std::io::Result<()> {
self.buffer
.borrow_mut()
.reload(&mut self.cursor, &mut self.selection, alt)
}
pub fn verified_reload(
&mut self,
alt: Vec<AltCursor<'_>>,
) -> Result<std::io::Result<()>, Modified> {
if self.buffer.borrow().modified() {
Err(Modified)
} else {
Ok(self.reload(alt))
}
}
pub fn save(&mut self) -> std::io::Result<()> {
self.buffer.borrow_mut().save().inspect_err(|err| {
self.message = Some(BufferMessage::Error(err.to_string().into()));
})
}
pub fn verified_save(&mut self) -> Result<std::io::Result<()>, Modified> {
let mut buf = self.buffer.borrow_mut();
if let Some(saved) = buf.last_saved()
&& let Some(modified) = buf.last_modified()
&& modified > saved
{
Err(Modified)
} else {
Ok(buf.save().inspect_err(|err| {
self.message = Some(BufferMessage::Error(err.to_string().into()));
}))
}
}
pub fn set_cursor(&mut self, cursor: usize) {
self.cursor = cursor;
self.cursor_column = cursor_column(&self.buffer.borrow_move().rope, self.cursor);
}
fn cursor_position(&self) -> Option<(usize, usize)> {
use unicode_width::UnicodeWidthChar;
let rope = &self.buffer.borrow().rope;
let line = rope.try_char_to_line(self.cursor).ok()?;
let line_start = rope.try_line_to_char(line).ok()?;
Some((
line,
rope.chars_at(line_start)
.take(self.cursor.checked_sub(line_start)?)
.map(|c| match c {
'\t' => *SPACES_PER_TAB,
c => c.width().unwrap_or(0),
})
.sum(),
))
}
fn set_cursor_focus(&mut self, area: Rect, position: Position) {
use ratatui::{
layout::{
Constraint::{Length, Min},
Layout,
},
widgets::Block,
};
let [text_area, _] =
Layout::horizontal([Min(0), Length(1)]).areas(Block::bordered().inner(area));
if !text_area.contains(position) {
return;
}
let buffer = self.buffer.borrow();
let rope = &buffer.rope;
let row = position.y.saturating_sub(text_area.y);
let col = position.x.saturating_sub(text_area.x);
let current_line = rope.try_char_to_line(self.cursor).ok();
let viewport_height: usize = text_area.height.into();
let (viewport_line, top_margin): (usize, usize) = current_line
.map(|line| match line.checked_sub(viewport_height / 2) {
Some(start) => (start, 0),
None => (0, viewport_height / 2 - line),
})
.unwrap_or_default();
let line = viewport_line + usize::from(row).saturating_sub(top_margin);
let starting_col = self
.cursor_position()
.map(|(_, col)| {
col.saturating_sub(
text_area
.width
.saturating_sub(BufferWidget::RIGHT_MARGIN)
.into(),
) as u16
})
.unwrap_or(0);
let mut desired_col: usize = (starting_col + col).into();
self.cursor_column = desired_col;
let col_chars = rope
.try_line_to_char(line)
.map(|line_start| {
rope.chars_at(line_start)
.take_while(|c| {
use unicode_width::UnicodeWidthChar;
desired_col = match desired_col.checked_sub(match c {
'\t' => *SPACES_PER_TAB,
c => c.width().unwrap_or(0),
}) {
Some(col) => col,
None => return false,
};
true
})
.count()
})
.unwrap_or(0);
self.cursor = (rope.try_line_to_char(line).unwrap_or(rope.len_chars()) + col_chars).min(
rope.try_line_to_char(line + 1)
.unwrap_or(rope.len_chars())
.saturating_sub(1),
);
self.selection = None;
}
pub fn cursor_up(&mut self, lines: usize, selecting: bool) {
let buf = self.buffer.borrow_move();
if let Ok(current_line) = buf.rope.try_char_to_line(self.cursor) {
let previous_line = current_line.saturating_sub(lines);
if let Some((prev_start, prev_end)) = line_char_range(&buf.rope, previous_line) {
update_selection(&mut self.selection, self.cursor, selecting);
self.cursor =
apply_cursor_column(&buf.rope, self.cursor_column, prev_start, prev_end);
}
}
}
pub fn cursor_down(&mut self, lines: usize, selecting: bool) {
let buf = self.buffer.borrow_move();
if let Ok(current_line) = buf.rope.try_char_to_line(self.cursor) {
let next_line = (current_line + lines).min(buf.rope.len_lines().saturating_sub(1));
if let Some((next_start, next_end)) = line_char_range(&buf.rope, next_line) {
update_selection(&mut self.selection, self.cursor, selecting);
self.cursor =
apply_cursor_column(&buf.rope, self.cursor_column, next_start, next_end);
}
}
}
pub fn cursor_back(&mut self, selecting: bool) {
let buf = self.buffer.borrow_move();
update_selection(&mut self.selection, self.cursor, selecting);
self.cursor = self.cursor.saturating_sub(
buf.rope
.chars_at(self.cursor)
.reversed()
.take_while(|c| is_grapheme_part(*c))
.count()
+ 1,
);
self.cursor_column = cursor_column(&buf.rope, self.cursor);
}
pub fn cursor_forward(&mut self, selecting: bool) {
let buf = self.buffer.borrow_move();
update_selection(&mut self.selection, self.cursor, selecting);
if self.cursor < buf.rope.len_chars() {
self.cursor = self.cursor
+ buf
.rope
.chars_at(self.cursor + 1)
.take_while(|c| is_grapheme_part(*c))
.count()
+ 1;
}
self.cursor_column = cursor_column(&buf.rope, self.cursor);
}
pub fn cursor_home(&mut self, selecting: bool) {
let buf = self.buffer.borrow_move();
if let Ok(current_line) = buf.rope.try_char_to_line(self.cursor)
&& let Some((home, _)) = line_char_range(&buf.rope, current_line)
{
use unicode_width::UnicodeWidthChar;
let indent_char = if buf.tabs_required { '\t' } else { ' ' };
update_selection(&mut self.selection, self.cursor, selecting);
match line_chars(&buf.rope, self.cursor) {
Some(iter) => {
let mut iter = iter.peekable();
let mut indent = home;
let mut cursor_column = 0;
while let Some(c) = iter.next_if(|c| *c == indent_char) {
indent += 1;
cursor_column += match c {
'\t' => *SPACES_PER_TAB,
c => c.width().unwrap_or(1),
};
}
if self.cursor == indent {
self.cursor = home;
self.cursor_column = 0;
} else {
self.cursor = indent;
self.cursor_column = cursor_column;
}
}
None => {
self.cursor = home;
self.cursor_column = 0;
}
}
}
}
pub fn cursor_end(&mut self, selecting: bool) {
let buf = self.buffer.borrow_move();
if let Ok(current_line) = buf.rope.try_char_to_line(self.cursor)
&& let Some((_, end)) = line_char_range(&buf.rope, current_line)
{
update_selection(&mut self.selection, self.cursor, selecting);
self.cursor = end;
self.cursor_column = cursor_column(&buf.rope, self.cursor);
}
}
pub fn last_line(&self) -> usize {
self.buffer.borrow().rope.len_lines().saturating_sub(1)
}
pub fn select_line(&mut self, line: usize) {
let buf = self.buffer.borrow_move();
match buf.rope.try_line_to_char(line) {
Ok(cursor) => {
self.cursor_column = 0;
self.cursor = cursor;
self.selection = None;
}
Err(_) => {
self.message = Some(BufferMessage::Error("invalid line".into()));
}
}
}
pub fn select_line_and_column(&mut self, line: usize, column: usize) {
let buf = self.buffer.borrow_move();
if let Ok(line_start) = buf.rope.try_line_to_char(line)
&& let Ok(next_line_start) = buf.rope.try_line_to_char(line + 1)
{
let start = (line_start + column).min(next_line_start.saturating_sub(1));
self.cursor = start
+ buf
.rope
.chars_at(start)
.take_while(|c| is_grapheme_part(*c))
.count();
self.cursor_column = cursor_column(&buf.rope, self.cursor);
self.selection = None;
} else {
self.message = Some(BufferMessage::Error("invalid line".into()));
}
}
pub fn insert_char(&mut self, mut alt: Vec<AltCursor<'_>>, c: char) {
use unicode_width::UnicodeWidthChar;
let mut buf = borrow_update!(self, alt);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
match &mut self.selection {
Some(selection) => match c {
'(' => {
perform_surround(
&mut rope,
&mut self.cursor,
selection,
&mut Secondary::new(alt, bookmarks),
['(', ')'],
);
self.cursor_column = cursor_column(&rope, self.cursor);
}
'[' => {
perform_surround(
&mut rope,
&mut self.cursor,
selection,
&mut Secondary::new(alt, bookmarks),
['[', ']'],
);
self.cursor_column = cursor_column(&rope, self.cursor);
}
'{' => {
perform_surround(
&mut rope,
&mut self.cursor,
selection,
&mut Secondary::new(alt, bookmarks),
['{', '}'],
);
self.cursor_column = cursor_column(&rope, self.cursor);
}
'<' => {
perform_surround(
&mut rope,
&mut self.cursor,
selection,
&mut Secondary::new(alt, bookmarks),
['<', '>'],
);
self.cursor_column = cursor_column(&rope, self.cursor);
}
'\"' => {
perform_surround(
&mut rope,
&mut self.cursor,
selection,
&mut Secondary::new(alt, bookmarks),
['\"', '\"'],
);
self.cursor_column = cursor_column(&rope, self.cursor);
}
'\'' => {
perform_surround(
&mut rope,
&mut self.cursor,
selection,
&mut Secondary::new(alt, bookmarks),
['\'', '\''],
);
self.cursor_column = cursor_column(&rope, self.cursor);
}
_ => {
let mut alt = Secondary::ge(alt, bookmarks, self.cursor.min(*selection));
zap_selection(
&mut rope,
&mut self.cursor,
&mut self.cursor_column,
*selection,
&mut alt,
);
self.selection = None;
rope.insert_char(self.cursor, c);
self.cursor += alt.inc(1);
self.cursor_column += c.width().unwrap_or(1);
}
},
None => {
insert_char_or_pair(
&mut rope,
self.cursor,
&mut Secondary::new(alt, bookmarks),
c,
);
self.cursor += 1;
self.cursor_column += c.width().unwrap_or(1);
}
}
}
pub fn paste(&mut self, mut alt: Vec<AltCursor<'_>>, cut_buffer: &mut Option<EditorCutBuffer>) {
match self.selection.as_mut() {
None => {
let mut buf = borrow_update!(self, alt);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::ge(alt, bookmarks, self.cursor);
match cut_buffer {
Some(cut_buffer) => {
let pasted = cut_buffer.paste_and_rotate();
if rope.try_insert(self.cursor, &pasted.data).is_ok() {
let old_cursor = self.cursor;
self.cursor += alt.inc(pasted.chars_len);
alt.add_bookmarks(pasted.bookmarks.iter().map(|b| old_cursor + b));
self.cursor_column = cursor_column(&rope, self.cursor);
}
}
None => { }
}
}
Some(selection) => {
if let Some(cut_pasted) = cut_buffer.as_mut().and_then(|c| c.primary_mut()) {
let mut buf = self.buffer.borrow_update(
MainCursor {
cursor: self.cursor,
cursor_column: self.cursor_column,
selection: Some(*selection),
undo: &mut self.undo,
redo: &mut self.redo,
},
&mut alt,
);
let (selection_start, selection_end) = reorder(self.cursor, *selection);
let cut_range = selection_start..selection_end;
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::ge(alt, bookmarks, selection_start);
if let Some(cut) = rope.get_slice(cut_range.clone()).map(|slice| {
CutBuffer::new(
slice,
alt.extract_bookmarks(cut_range.clone())
.map(|b| b - cut_range.start),
)
}) {
rope.remove(cut_range.clone());
alt.update(|pos| {
if (cut_range.clone()).contains(pos) {
*pos = selection_start;
} else {
*pos -= selection_end - selection_start;
}
});
self.cursor = selection_start;
let pasted = std::mem::replace(cut_pasted, cut);
*cut_buffer = Some(EditorCutBuffer::Single(std::mem::take(cut_pasted)));
if rope.try_insert(self.cursor, &pasted.data).is_ok() {
let old_cursor = self.cursor;
alt += pasted.chars_len;
alt.add_bookmarks(pasted.bookmarks.iter().map(|b| old_cursor + b));
self.selection = Some(selection_start);
self.cursor = selection_start + pasted.chars_len;
self.cursor_column = cursor_column(&rope, self.cursor);
}
self.message = Some(BufferMessage::Notice(
"swapped cut buffer with selection".into(),
));
}
}
}
}
}
pub fn newline(&mut self, mut alt: Vec<AltCursor<'_>>) {
let mut buf = borrow_update!(self, alt);
let indent_char = if buf.tabs_required { '\t' } else { ' ' };
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = match self.selection.take() {
Some(selection) => {
let mut secondary = Secondary::ge(alt, bookmarks, self.cursor.min(selection));
zap_selection(
&mut rope,
&mut self.cursor,
&mut self.cursor_column,
selection,
&mut secondary,
);
secondary
}
None => Secondary::ge(alt, bookmarks, self.cursor),
};
let (indent, all_indent) = match line_start_to_cursor(&rope, self.cursor) {
Some(iter) => {
let mut iter = iter.peekable();
let mut indent = 0;
while iter.next_if(|c| *c == indent_char).is_some() {
indent += 1;
}
(indent, iter.next().is_none())
}
None => (0, false),
};
if all_indent {
rope.insert_char(self.cursor - indent, '\n');
self.cursor += alt.inc(1);
} else {
rope.insert_char(self.cursor, '\n');
self.cursor += alt.inc(1);
self.cursor_column = 0;
for _ in 0..indent {
rope.insert_char(self.cursor, indent_char);
self.cursor += alt.inc(1);
self.cursor_column += 1;
}
}
}
pub fn backspace(&mut self, mut alt: Vec<AltCursor<'_>>) {
let mut buf = borrow_update!(self, alt);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
match self.selection.take() {
None => {
if let Ok((removed, _)) = backspace_or_un_pair(
&mut rope,
self.cursor,
&mut Secondary::new(alt, bookmarks),
) {
self.cursor -= removed;
self.cursor_column = cursor_column(&rope, self.cursor);
}
}
Some(current_selection) => {
let mut alt = Secondary::ge(alt, bookmarks, self.cursor.min(current_selection));
zap_selection(
&mut rope,
&mut self.cursor,
&mut self.cursor_column,
current_selection,
&mut alt,
);
}
}
}
pub fn delete(&mut self, mut alt: Vec<AltCursor<'_>>) {
let buf = &mut borrow_update!(self, alt);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
match &mut self.selection {
None => {
let mut alt = Secondary::gt(alt, bookmarks, self.cursor);
let to_delete = rope
.chars_at(self.cursor)
.skip(1)
.take_while(|c| is_grapheme_part(*c))
.count()
+ 1;
if rope
.try_remove(alt.remove(self.cursor..(self.cursor + to_delete)))
.is_ok()
{
alt -= to_delete;
}
}
Some(selection) => {
let mut secondary = Secondary::ge(alt, bookmarks, self.cursor.min(*selection));
match delete_surround(
&mut rope,
&mut self.cursor,
selection,
&mut secondary,
|_| true,
) {
Ok(()) => {
self.cursor_column = cursor_column(&rope, self.cursor);
}
Err(()) => {
zap_selection(
&mut rope,
&mut self.cursor,
&mut self.cursor_column,
*selection,
&mut secondary,
);
self.selection = None;
}
}
}
}
}
pub fn selection_range(&self) -> Option<SelectionType> {
let (selection_start, selection_end) = reorder(self.cursor, self.selection?);
if selection_start == selection_end {
return None;
}
let buf = self.buffer.borrow();
let rope = &buf.rope;
let start_line = rope.try_char_to_line(selection_start).ok()?;
let end_line = rope.try_char_to_line(selection_end).ok()?;
if start_line == end_line {
rope.get_slice(selection_start..selection_end)
.map(|r| SelectionType::Term(r.into()))
} else {
Some(SelectionType::Range(SelectionRange {
start: start_line,
lines: NonZero::new((end_line - start_line) + 1)?,
}))
}
}
pub fn get_selection(&mut self) -> Option<CutBuffer> {
let selection = self.selection.take()?;
let (selection_start, selection_end) = reorder(self.cursor, selection);
let buffer = self.buffer.borrow();
Some(CutBuffer::new(
buffer.rope.get_slice(selection_start..selection_end)?,
buffer
.bookmarks
.range(selection_start..selection_end)
.map(|b| b - selection_start),
))
}
pub fn set_selection(&mut self, start: usize, end: usize) {
self.selection = Some(start);
self.cursor = end;
self.cursor_column = cursor_column(&self.buffer.borrow_move().rope, self.cursor);
}
pub fn take_selection(&mut self, mut alt: Vec<AltCursor<'_>>) -> Option<CutBuffer> {
let selection = self.selection.take()?;
let (selection_start, selection_end) = reorder(self.cursor, selection);
let mut buf = borrow_update!(self, alt);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::ge(alt, bookmarks, selection_start);
rope.get_slice(selection_start..selection_end)
.map(|r| {
CutBuffer::new(
r,
alt.extract_bookmarks(selection_start..selection_end)
.map(|b| b - selection_start),
)
})
.inspect(|_| {
rope.remove(selection_start..selection_end);
self.cursor = selection_start;
self.cursor_column = cursor_column(&rope, self.cursor);
alt.update(|pos| {
if (selection_start..selection_end).contains(pos) {
*pos = selection_start;
} else {
*pos -= selection_end - selection_start;
}
});
})
}
pub fn perform_undo_active(&mut self) -> Result<(), ()> {
let mut buf = self.buffer.borrow_mut();
match (buf.undo.pop(), self.undo.pop()) {
(Some(mut buf_state), Some(mut ctx_state)) => {
use std::ops::DerefMut;
std::mem::swap(buf.rope.get_mut().deref_mut(), &mut buf_state.rope);
std::mem::swap(&mut buf.bookmarks, &mut buf_state.bookmarks);
buf.redo.push(buf_state);
buf.undo_finished = true;
std::mem::swap(&mut self.cursor, &mut ctx_state.cursor);
std::mem::swap(&mut self.cursor_column, &mut ctx_state.cursor_column);
std::mem::swap(&mut self.selection, &mut ctx_state.selection);
self.redo.push(ctx_state);
Ok(())
}
_ => {
self.message = Some(BufferMessage::Notice("nothing left to undo".into()));
Err(())
}
}
}
pub fn perform_undo_inactive(&mut self) {
if let Some(mut ctx_state) = self.undo.pop() {
std::mem::swap(&mut self.cursor, &mut ctx_state.cursor);
std::mem::swap(&mut self.cursor_column, &mut ctx_state.cursor_column);
std::mem::swap(&mut self.selection, &mut ctx_state.selection);
self.redo.push(ctx_state);
}
}
pub fn perform_redo_active(&mut self) -> Result<(), ()> {
let mut buf = self.buffer.borrow_mut();
match (buf.redo.pop(), self.redo.pop()) {
(Some(mut buf_state), Some(mut ctx_state)) => {
use std::ops::DerefMut;
std::mem::swap(buf.rope.get_mut().deref_mut(), &mut buf_state.rope);
std::mem::swap(&mut buf.bookmarks, &mut buf_state.bookmarks);
buf.undo.push(buf_state);
buf.undo_finished = true;
std::mem::swap(&mut self.cursor, &mut ctx_state.cursor);
std::mem::swap(&mut self.cursor_column, &mut ctx_state.cursor_column);
std::mem::swap(&mut self.selection, &mut ctx_state.selection);
self.undo.push(ctx_state);
Ok(())
}
_ => {
self.message = Some(BufferMessage::Notice("nothing left to redo".into()));
Err(())
}
}
}
pub fn perform_redo_inactive(&mut self) {
if let Some(mut ctx_state) = self.redo.pop() {
std::mem::swap(&mut self.cursor, &mut ctx_state.cursor);
std::mem::swap(&mut self.cursor_column, &mut ctx_state.cursor_column);
std::mem::swap(&mut self.selection, &mut ctx_state.selection);
self.undo.push(ctx_state);
}
}
pub fn complete_or_indent(
&mut self,
mut alt: Vec<AltCursor<'_>>,
) -> Option<(usize, Vec<String>)> {
match self.selection {
None => {
if let matches @ Some(_) = self.autocomplete_matches() {
return matches;
}
let mut buf = borrow_update!(self, alt);
let indent = match buf.tabs_required {
false => buf.tab_substitution.clone(),
true => "\t".to_string(),
};
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
if let Ok(line_start) = rope
.try_char_to_line(self.cursor)
.and_then(|line| rope.try_line_to_char(line))
{
let mut alt = Secondary::ge(alt, bookmarks, line_start);
rope.insert(line_start, &indent);
self.cursor += indent.len();
alt += indent.len();
}
None
}
selection_opt @ Some(_) => {
use std::convert::Infallible;
let mut buf = borrow_update!(self, alt);
let indent = match buf.tabs_required {
false => buf.tab_substitution.clone(),
true => "\t".to_string(),
};
let indent_chars = indent.chars().count();
let mut indent_lines = selected_lines(&buf.rope, self.cursor, selection_opt)
.filter_map(|line| (!line.is_empty()).then(|| line.into()))
.collect::<Vec<_>>();
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::new(alt, bookmarks);
multicursor_update(
&mut indent_lines,
|m| {
m.indent(&mut rope, &mut alt, &indent, indent_chars);
Ok::<_, Infallible>(())
},
|m, ()| {
*m += indent_chars;
},
);
if let Some(start) = indent_lines.first()
&& let Some(end) = indent_lines.last()
{
self.selection = Some(start.range.start);
self.cursor = end.range.end;
}
None
}
}
}
pub fn complete_or_unindent(
&mut self,
mut alt: Vec<AltCursor<'_>>,
) -> Option<(usize, Vec<String>)> {
match self.selection {
None => {
if let matches @ Some(_) = self.autocomplete_matches() {
return matches;
}
let mut buf = borrow_update!(self, alt);
let indent = match buf.tabs_required {
false => buf.tab_substitution.clone(),
true => "\t".to_string(),
};
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
if let Some(line_start) = rope
.try_char_to_line(self.cursor)
.ok()
.and_then(|line| rope.try_line_to_char(line).ok())
&& rope
.chars_at(line_start)
.take(indent.len())
.eq(indent.chars())
{
let mut alt = Secondary::ge(alt, bookmarks, line_start);
let to_remove = line_start..line_start + indent.len();
rope.remove(alt.remove(to_remove.clone()));
if to_remove.contains(&self.cursor) {
self.cursor = line_start;
self.cursor_column = 0;
} else {
self.cursor -= to_remove.end - to_remove.start;
self.cursor_column = cursor_column(&rope, self.cursor);
}
alt.update(|pos| {
if (line_start..line_start + indent.len()).contains(pos) {
*pos = line_start;
} else {
*pos -= indent.len();
}
});
}
None
}
selection_opt @ Some(_) => {
let mut buf = borrow_update!(self, alt);
let indent = match buf.tabs_required {
false => buf.tab_substitution.clone(),
true => "\t".to_string(),
};
let indent_chars = indent.chars().count();
let mut unindent_lines = selected_lines(&buf.rope, self.cursor, selection_opt)
.filter_map(|line| (!line.is_empty()).then(|| line.into()))
.collect::<Vec<_>>();
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
if unindent_lines
.iter()
.all(|l: &MultiCursor| l.can_unindent(&rope, &indent, indent_chars))
{
let mut alt = Secondary::new(alt, bookmarks);
multicursor_update(
&mut unindent_lines,
|m| m.un_indent(&mut rope, &mut alt, indent_chars),
|m, ()| {
*m -= indent_chars;
},
);
if let Some(start) = unindent_lines.first()
&& let Some(end) = unindent_lines.last()
{
self.selection = Some(start.range.start);
self.cursor = end.range.end;
}
}
None
}
}
}
pub fn selection_cursors(&mut self) -> Vec<MultiCursor> {
let buf = self.buffer.borrow_move();
let lines = selected_lines(&buf.rope, self.cursor, self.selection.take())
.filter_map(|SelectedLine { start, end }| {
(start != end).then_some(MultiCursor {
range: start..end,
cursor: start,
..MultiCursor::default()
})
})
.collect::<Vec<_>>();
self.cursor = match lines.last() {
Some(MultiCursor { cursor, .. }) => *cursor,
None => return vec![],
};
lines
}
pub fn select_inside(&mut self, (start, end): (char, char), stack: Option<(char, char)>) {
let buf = self.buffer.borrow();
let (stack_back, stack_forward) = match stack {
Some((back, forward)) => (Some(back), Some(forward)),
None => (None, None),
};
match self.selection {
Some(selection) => {
let (sel_start, sel_end) = reorder(self.cursor, selection);
if let (Some(start), Some(end)) = (
select_next_char::<false>(&buf.rope, sel_start, start, stack_back),
select_next_char::<true>(&buf.rope, sel_end, end, stack_forward),
) {
self.selection = Some(start);
self.cursor = end;
}
}
None => {
if let (Some(start), Some(end)) = (
select_next_char::<false>(&buf.rope, self.cursor, start, stack_back),
select_next_char::<true>(&buf.rope, self.cursor, end, stack_forward),
) {
self.selection = Some(start);
self.cursor = end;
}
}
}
}
pub fn cursor_to_selection_start(&mut self) {
let buf = self.buffer.borrow_move();
if let Some(selection) = &mut self.selection
&& self.cursor > *selection
{
std::mem::swap(selection, &mut self.cursor);
self.cursor_column = cursor_column(&buf.rope, self.cursor);
}
}
pub fn cursor_to_selection_end(&mut self) {
let buf = self.buffer.borrow_move();
if let Some(selection) = &mut self.selection
&& self.cursor < *selection
{
std::mem::swap(selection, &mut self.cursor);
self.cursor_column = cursor_column(&buf.rope, self.cursor);
}
}
pub fn select_matching_paren(&mut self) {
let buf = self.buffer.borrow_move();
if let Some(new_pos) = buf.rope.get_char(self.cursor).and_then(|c| match c {
'(' => select_next_char::<true>(&buf.rope, self.cursor + 1, ')', Some('(')),
')' => select_next_char::<false>(&buf.rope, self.cursor, '(', Some(')'))
.map(|c| c.saturating_sub(1)),
'{' => select_next_char::<true>(&buf.rope, self.cursor + 1, '}', Some('{')),
'}' => select_next_char::<false>(&buf.rope, self.cursor, '{', Some('}'))
.map(|c| c.saturating_sub(1)),
'[' => select_next_char::<true>(&buf.rope, self.cursor + 1, ']', Some('[')),
']' => select_next_char::<false>(&buf.rope, self.cursor, '[', Some(']'))
.map(|c| c.saturating_sub(1)),
'<' => select_next_char::<true>(&buf.rope, self.cursor + 1, '>', Some('<')),
'>' => select_next_char::<false>(&buf.rope, self.cursor, '<', Some('>'))
.map(|c| c.saturating_sub(1)),
_ => None,
}) {
self.cursor = new_pos;
self.selection = None;
}
}
pub fn try_select_inside(&mut self) -> Result<(), ()> {
let buf = self.buffer.borrow();
try_select_inside(&buf.rope, &mut self.cursor, &mut self.selection, |_| true).inspect(
|()| {
self.cursor_column = cursor_column(&buf.rope, self.cursor);
},
)
}
pub fn select_word_or_lines(&mut self) {
let buf = &mut self.buffer.borrow_move();
let rope = &buf.rope;
match self.selection {
None => {
match rope.get_char(self.cursor) {
Some(c) if is_word(c) => {
let word_start = rope
.chars_at(self.cursor)
.reversed()
.position(|c| !is_word_part(c))
.and_then(|pos| self.cursor.checked_sub(pos))
.unwrap_or(0);
let word_end = rope
.chars_at(self.cursor)
.position(|c| !is_word_part(c))
.map(|pos| self.cursor + pos)
.unwrap_or(rope.len_chars());
if word_start != word_end {
self.selection = Some(word_start);
self.cursor = word_end;
self.cursor_column = cursor_column(rope, self.cursor);
}
}
_ => {
if let Some((start, end)) = rope
.try_char_to_line(self.cursor)
.ok()
.and_then(|line| line_char_range(rope, line))
{
self.selection = Some(start);
self.cursor = end;
self.cursor_column = cursor_column(rope, self.cursor);
}
}
}
}
Some(selection) => {
if selection < self.cursor {
if let Some(start) = rope
.try_char_to_line(selection)
.ok()
.and_then(|line| rope.try_line_to_char(line).ok())
&& let Some(end) = rope
.try_char_to_line(self.cursor)
.ok()
.and_then(|line| rope.try_line_to_char(line + 1).ok())
{
self.selection = Some(start);
self.cursor = end - 1;
}
} else {
if let Some(start) = rope
.try_char_to_line(self.cursor)
.ok()
.and_then(|line| rope.try_line_to_char(line).ok())
&& let Some(end) = rope
.try_char_to_line(selection)
.ok()
.and_then(|line| rope.try_line_to_char(line + 1).ok())
{
self.cursor = start;
self.selection = Some(end - 1);
}
}
}
}
}
fn autocomplete_matches(&self) -> Option<(usize, Vec<String>)> {
let buf = &mut self.buffer.borrow();
let rope = &buf.rope;
if let Some(c) = rope.get_char(self.cursor)
&& is_word(c)
{
return None;
}
let prefix_chars = rope
.chars_at(self.cursor)
.reversed()
.take_while(|c| is_word_part(*c))
.collect::<Vec<_>>();
if prefix_chars.is_empty() {
return None;
}
let prefix_start = self.cursor.checked_sub(prefix_chars.len())?;
Some((
prefix_start,
autocomplete_matches(rope, prefix_chars.into_iter().rev().collect()),
))
}
pub fn multi_autocomplete_matches(
&self,
cursors: &[MultiCursor],
) -> Option<(Vec<usize>, Vec<String>)> {
let buf = &mut self.buffer.borrow();
let rope = &buf.rope;
let mut offsets = Vec::with_capacity(cursors.len());
let mut prefix = None;
for cursor in cursors {
let (offset, p) = cursor.autocomplete_prefix(rope)?;
offsets.push(offset);
match &mut prefix {
None => {
prefix = Some(p);
}
Some(prefix) => {
if prefix != &p {
return None;
}
}
}
}
Some((offsets, autocomplete_matches(rope, prefix?)))
}
pub fn autocomplete(
&mut self,
mut alt: Vec<AltCursor<'_>>,
offset: usize,
original: &str,
replacement: &str,
) {
let mut buf = borrow_update!(self, alt);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::ge(alt, bookmarks, offset);
let original_chars = original.chars().count();
let cut_range = offset..offset + original_chars;
rope.remove(alt.remove(cut_range.clone()));
alt -= original_chars;
let replacement_chars = replacement.chars().count();
rope.insert(offset, replacement);
alt += replacement_chars;
self.cursor = offset + replacement_chars;
self.cursor_column = cursor_column(&rope, self.cursor);
self.selection = None;
}
pub fn multi_autocomplete(
&mut self,
mut alt: Vec<AltCursor<'_>>,
matches: &mut [MultiCursor],
mut offsets: &[usize],
original: &str,
replacement: &str,
) {
let original_chars = original.chars().count();
let replacement_chars = replacement.chars().count();
let mut buf = borrow_multi_update!(self, alt, matches);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::new(alt, bookmarks);
multicursor_update(
matches,
|m| {
let offset = offsets.split_off_first().ok_or(())?;
m.autocomplete(
&mut rope,
&mut self.cursor,
&mut alt,
*offset,
original_chars,
replacement,
replacement_chars,
);
Ok::<_, ()>(())
},
|r, ()| {
*r -= original_chars;
*r += replacement_chars;
},
);
}
pub fn multi_insert_char(
&mut self,
mut alt: Vec<AltCursor<'_>>,
matches: &mut [MultiCursor],
c: char,
) {
use std::convert::Infallible;
let mut buf = borrow_multi_update!(self, alt, matches);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::new(alt, bookmarks);
multicursor_update(
matches,
|m| Ok::<_, Infallible>(m.insert_char(&mut rope, &mut self.cursor, &mut alt, c)),
|r, (zapped, inserted)| {
*r -= zapped;
*r += inserted;
},
);
}
pub fn multi_insert_group(
&mut self,
mut alt: Vec<AltCursor<'_>>,
matches: &mut [MultiCursor],
group_num: usize,
) {
let mut buf = borrow_multi_update!(self, alt, matches);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::new(alt, bookmarks);
multicursor_update(
matches,
|m| {
m.insert_group(&mut rope, &mut self.cursor, &mut alt, group_num)
.ok_or(())
},
|r, (zapped, inserted)| {
*r -= zapped;
*r += inserted;
},
);
}
pub fn multi_insert_string(
&mut self,
alt: Vec<AltCursor<'_>>,
matches: &mut [MultiCursor],
s: &str,
) {
self.multi_insert_strings(alt, matches, std::iter::repeat((s.chars().count(), s)))
}
pub fn multi_paste(
&mut self,
mut alt: Vec<AltCursor<'_>>,
matches: &mut [MultiCursor],
cut: &mut EditorCutBuffer,
) {
let mut buf = borrow_multi_update!(self, alt, matches);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::new(alt, bookmarks);
match cut {
EditorCutBuffer::Single(cut) => {
multicursor_update(
matches,
|m| {
let zapped = m.paste_single(&mut rope, &mut self.cursor, &mut alt, cut);
Ok::<_, std::convert::Infallible>(zapped)
},
|r, zapped| {
*r -= zapped;
*r += cut.chars_len;
},
);
}
EditorCutBuffer::Multiple(cuts) => {
let mut cuts_iter = cuts.iter_mut();
let mut pasted = 0;
multicursor_update(
matches,
|m| match cuts_iter.next() {
Some(cut) => {
pasted += 1;
Ok(m.paste_multiple(&mut rope, &mut self.cursor, &mut alt, cut))
}
None => {
m.selection = None;
Err(())
}
},
|r, (zapped, s_len)| {
*r -= zapped;
*r += s_len;
},
);
let cuts_len = cuts.len();
cuts.rotate_left(pasted % cuts_len);
}
}
}
pub fn multi_insert_strings<'s>(
&mut self,
mut alt: Vec<AltCursor<'_>>,
matches: &mut [MultiCursor],
mut strings: impl std::iter::FusedIterator<Item = (usize, &'s str)>,
) {
let mut buf = borrow_multi_update!(self, alt, matches);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::new(alt, bookmarks);
multicursor_update(
matches,
|m| {
let (s_len, s) = strings.next().ok_or(())?;
let zapped = m.insert_str(&mut rope, &mut self.cursor, &mut alt, s, s_len);
Ok::<_, ()>((zapped, s_len))
},
|r, (zapped, s_len)| {
*r -= zapped;
*r += s_len;
},
);
}
pub fn multi_backspace(&mut self, mut alt: Vec<AltCursor<'_>>, matches: &mut [MultiCursor]) {
let mut buf = borrow_multi_update!(self, alt, matches);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::new(alt, bookmarks);
multicursor_update(
matches,
|m| m.backspace(&mut rope, &mut self.cursor, &mut alt),
|r, removed| {
*r -= removed;
},
);
}
pub fn multi_delete(&mut self, mut alt: Vec<AltCursor<'_>>, matches: &mut [MultiCursor]) {
let mut buf = borrow_multi_update!(self, alt, matches);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::new(alt, bookmarks);
multicursor_update(
matches,
|m| m.delete(&mut rope, &mut self.cursor, &mut alt),
|r, removed| {
*r -= removed;
},
);
}
pub fn multi_cursor_back(&mut self, matches: &mut [MultiCursor], selecting: bool) {
matches.iter_mut().for_each(|m| {
m.cursor_back(
&mut self.cursor,
&mut self.cursor_column,
&self.buffer.borrow_move().rope,
selecting,
)
});
}
pub fn multi_cursor_forward(&mut self, matches: &mut [MultiCursor], selecting: bool) {
matches.iter_mut().for_each(|m| {
m.cursor_forward(
&mut self.cursor,
&mut self.cursor_column,
&self.buffer.borrow_move().rope,
selecting,
)
});
}
pub fn multi_cursor_home(&mut self, matches: &mut [MultiCursor], selecting: bool) {
matches.iter_mut().for_each(|m| {
m.cursor_home(
&mut self.cursor,
&mut self.cursor_column,
&self.buffer.borrow_move().rope,
selecting,
)
});
}
pub fn multi_cursor_end(&mut self, matches: &mut [MultiCursor], selecting: bool) {
matches.iter_mut().for_each(|m| {
m.cursor_end(
&mut self.cursor,
&mut self.cursor_column,
&self.buffer.borrow_move().rope,
selecting,
)
});
}
pub fn multi_select_inside(&mut self, matches: &mut [MultiCursor], selected: usize) {
let buffer = self.buffer.borrow();
matches.iter_mut().for_each(|c| {
let _ = try_select_inside(&buffer.rope, &mut c.cursor, &mut c.selection, |pos| {
c.range.contains(&pos)
});
});
if let Some(c) = matches.get(selected) {
self.cursor = c.cursor;
self.cursor_column = cursor_column(&buffer.rope, self.cursor);
}
}
pub fn multi_cursor_copy(&self, matches: &mut [MultiCursor]) -> Vec<CutBuffer> {
let buffer = &self.buffer.borrow();
matches
.iter_mut()
.filter_map(|m| m.get_selection(&buffer.rope, &buffer.bookmarks))
.collect()
}
pub fn multi_cursor_cut(
&mut self,
mut alt: Vec<AltCursor<'_>>,
matches: &mut [MultiCursor],
) -> Vec<CutBuffer> {
let mut buf = borrow_multi_update!(self, alt, matches);
let (mut rope, bookmarks) = buf.rope_bookmarks_mut();
let mut alt = Secondary::new(alt, bookmarks);
let mut cut_buffers = vec![];
multicursor_update(
matches,
|m| match m.take_selection(&mut rope, &mut self.cursor, &mut alt) {
Some(cut) => {
let removed = cut.chars_len;
cut_buffers.push(cut);
Ok::<_, std::convert::Infallible>(removed)
}
None => Ok(0),
},
|r, removed| {
*r -= removed;
},
);
cut_buffers
}
pub fn multi_cursor_widen(&mut self, matches: &mut [MultiCursor]) {
matches
.iter_mut()
.for_each(|m| m.widen_selection(&mut self.cursor));
}
pub fn perform_multi_undo(&mut self, alt: Vec<AltCursor<'_>>, matches: &mut [MultiCursor]) {
if self.perform_undo_active().is_ok() {
alt.into_iter().for_each(|mut a| a.perform_undo());
matches.iter_mut().for_each(|m| m.perform_undo());
}
}
pub fn perform_multi_redo(&mut self, alt: Vec<AltCursor<'_>>, matches: &mut [MultiCursor]) {
if self.perform_redo_active().is_ok() {
alt.into_iter().for_each(|mut a| a.perform_redo());
matches.iter_mut().for_each(|m| m.perform_redo());
}
}
pub fn set_buffer_message(&mut self, message: BufferMessage) {
self.message = Some(message);
}
pub fn set_error<S: Into<Cow<'static, str>>>(&mut self, err: S) {
self.message = Some(BufferMessage::Error(err.into()))
}
pub fn set_message<S: Into<Cow<'static, str>>>(&mut self, msg: S) {
self.message = Some(BufferMessage::Notice(msg.into()))
}
pub fn alt_cursor(&mut self) -> AltCursor<'_> {
AltCursor {
cursor: &mut self.cursor,
cursor_column: self.cursor_column,
selection: &mut self.selection,
undo: &mut self.undo,
redo: &mut self.redo,
}
}
pub fn help_options(&self, multiple_buffers: bool, multiple_panes: bool) -> Help {
let buffer = &self.buffer.borrow();
let has_bookmarks = buffer.has_bookmarks();
let rope = &buffer.rope;
match self.selection {
Some(selection) => Help {
find: if rope.try_char_to_line(self.cursor).ok()
== rope.try_char_to_line(selection).ok()
{
FindMode::Selected
} else {
FindMode::InSelection
},
has_bookmarks,
cursor_pos: CursorPos::default(),
has_selection: true,
multiple_buffers,
multiple_panes,
},
None => {
let current_char = rope.get_char(self.cursor);
Help {
find: FindMode::WholeFile,
has_bookmarks,
cursor_pos: match current_char {
Some('(' | ')' | '{' | '}' | '[' | ']' | '<' | '>') => CursorPos::AtParen,
Some(c) if is_word(c) => CursorPos::InWord,
_ => match self
.cursor
.checked_sub(1)
.and_then(|prev| rope.get_char(prev))
{
Some(c) => {
if is_word_part(c) {
CursorPos::AfterWord
} else {
CursorPos::default()
}
}
None => CursorPos::default(),
},
},
has_selection: false,
multiple_buffers,
multiple_panes,
}
}
}
}
pub fn toggle_bookmark(&mut self) {
let mut buf = self.buffer.borrow_mut();
self.message = Some(BufferMessage::Notice(
(match buf.bookmarks.toggle(self.cursor) {
Toggle::Inserted => "Bookmark Added",
Toggle::Removed => "Bookmark Removed",
})
.into(),
))
}
#[must_use]
pub fn toggle_bookmarks(&mut self, positions: impl Iterator<Item = usize>) -> ToggledBookmarks {
let mut added = 0;
let mut removed = 0;
let mut buf = self.buffer.borrow_mut();
for pos in positions {
match buf.bookmarks.toggle(pos) {
Toggle::Inserted => {
added += 1;
}
Toggle::Removed => {
removed += 1;
}
}
}
ToggledBookmarks { added, removed }
}
pub fn delete_bookmark(&mut self) {
let mut buf = self.buffer.borrow_mut();
if let Ok(()) = buf.bookmarks.remove(self.cursor) {
self.message = Some(BufferMessage::Notice("Bookmark Removed".into()));
}
}
fn goto_bookmark(&mut self, forward: bool) {
let buf = self.buffer.borrow_move();
let Some(cursor) = (if forward {
buf.bookmarks.next_after(self.cursor)
} else {
buf.bookmarks.next_before(self.cursor)
}) else {
return;
};
self.cursor = cursor;
self.cursor_column = cursor_column(&buf.rope, self.cursor);
self.selection = None;
}
pub fn next_bookmark(&mut self) {
self.goto_bookmark(true);
}
pub fn previous_bookmark(&mut self) {
self.goto_bookmark(false);
}
pub fn bookmarks(&self) -> usize {
self.buffer.borrow().bookmarks.len()
}
}
impl std::fmt::Display for BufferContext {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.buffer.borrow().source().name().fmt(f)
}
}
impl<'a> MultiBuffer<'a> for BufferContext {
type Matches = Vec<MultiCursor>;
type Offsets = Vec<usize>;
type Alt = Vec<AltCursor<'a>>;
fn multi_insert_char(&mut self, alt: Self::Alt, matches: &mut Self::Matches, c: char) {
BufferContext::multi_insert_char(self, alt, matches, c)
}
fn multi_insert_string(&mut self, alt: Self::Alt, matches: &mut Self::Matches, s: &str) {
BufferContext::multi_insert_string(self, alt, matches, s)
}
fn multi_backspace(&mut self, alt: Self::Alt, matches: &mut Self::Matches) {
BufferContext::multi_backspace(self, alt, matches)
}
fn multi_delete(&mut self, alt: Self::Alt, matches: &mut Self::Matches) {
BufferContext::multi_delete(self, alt, matches)
}
fn delete_buffer(
&mut self,
matches: &mut Self::Matches,
match_idx: &mut usize,
) -> BufferDeleted {
matches.remove(*match_idx);
match matches.len().checked_sub(1) {
Some(max) => {
*match_idx = (*match_idx).min(max);
if let Some(m) = matches.get(*match_idx) {
self.set_cursor(m.cursor());
}
BufferDeleted::BuffersRemain
}
None => BufferDeleted::NoBuffers,
}
}
fn multi_select_inside(&mut self, matches: &mut Self::Matches, selected: usize) {
BufferContext::multi_select_inside(self, matches, selected)
}
fn multi_cursor_back(&mut self, matches: &mut Self::Matches, selecting: bool) {
BufferContext::multi_cursor_back(self, matches, selecting)
}
fn multi_cursor_forward(&mut self, matches: &mut Self::Matches, selecting: bool) {
BufferContext::multi_cursor_forward(self, matches, selecting)
}
fn multi_cursor_home(&mut self, matches: &mut Self::Matches, selecting: bool) {
BufferContext::multi_cursor_home(self, matches, selecting)
}
fn multi_cursor_end(&mut self, matches: &mut Self::Matches, selecting: bool) {
BufferContext::multi_cursor_end(self, matches, selecting)
}
fn paste_group_count(matches: &Self::Matches) -> Option<NonZero<usize>> {
matches
.iter()
.map(|m| m.paste_group_count())
.max()
.flatten()
}
fn multi_paste(
&mut self,
alt: Self::Alt,
matches: &mut Self::Matches,
cut: &mut EditorCutBuffer,
) {
BufferContext::multi_paste(self, alt, matches, cut);
}
fn multi_cursor_copy(&mut self, matches: &mut Self::Matches) -> Vec<CutBuffer> {
BufferContext::multi_cursor_copy(self, matches)
}
fn multi_cursor_cut(&mut self, alt: Self::Alt, matches: &mut Self::Matches) -> Vec<CutBuffer> {
BufferContext::multi_cursor_cut(self, alt, matches)
}
fn multi_insert_group(
&mut self,
alt: Self::Alt,
matches: &mut Self::Matches,
group_num: usize,
) {
BufferContext::multi_insert_group(self, alt, matches, group_num);
}
fn previous_match(&mut self, matches: &mut Self::Matches, match_idx: &mut usize) {
*match_idx = match_idx.checked_sub(1).unwrap_or(matches.len() - 1);
if let Some(r) = matches.get(*match_idx) {
self.set_cursor(r.cursor());
}
}
fn next_match(&mut self, matches: &mut Self::Matches, match_idx: &mut usize) {
*match_idx = (*match_idx + 1) % matches.len();
if let Some(r) = matches.get(*match_idx) {
self.set_cursor(r.cursor());
}
}
fn multi_cursor_widen(&mut self, matches: &mut Self::Matches) {
BufferContext::multi_cursor_widen(self, matches)
}
fn toggle_bookmarks(&mut self, matches: &mut Self::Matches) -> ToggledBookmarks {
BufferContext::toggle_bookmarks(self, matches.iter().map(|m| m.cursor()))
}
fn multi_autocomplete_matches(
&self,
matches: &mut Self::Matches,
) -> Option<(Self::Offsets, Vec<String>)> {
BufferContext::multi_autocomplete_matches(self, matches)
}
fn multi_autocomplete(
&mut self,
alt: Self::Alt,
matches: &mut Self::Matches,
offsets: &Self::Offsets,
original: &str,
replacement: &str,
) {
BufferContext::multi_autocomplete(self, alt, matches, offsets, original, replacement)
}
fn perform_undo(&mut self, alt: Self::Alt, matches: &mut Self::Matches) {
BufferContext::perform_multi_undo(self, alt, matches);
}
fn perform_redo(&mut self, alt: Self::Alt, matches: &mut Self::Matches) {
BufferContext::perform_multi_redo(self, alt, matches);
}
fn set_buffer_message(&mut self, message: BufferMessage) {
BufferContext::set_buffer_message(self, message)
}
fn set_message<S: Into<Cow<'static, str>>>(&mut self, msg: S) {
BufferContext::set_message(self, msg)
}
fn set_error<S: Into<Cow<'static, str>>>(&mut self, msg: S) {
BufferContext::set_error(self, msg)
}
}
impl<'r> Searchable<'r> for BufferContext {
type Output = Vec<MultiCursor>;
type Range = Option<&'r SelectionRange>;
fn all_matches<S: SearchTerm>(
&mut self,
range: Self::Range,
term: S,
) -> Result<(usize, Self::Output), S> {
let buf = self.buffer.borrow_move();
let rope = &buf.rope;
let matches = search_area(rope, range)
.flat_map(|(line, offset)| {
term.match_ranges(&line)
.map(|m| m + offset)
.collect::<Vec<_>>()
})
.filter_map(|SearchMatch { start, end, groups }| {
let start_char = rope.try_byte_to_char(start).ok()?;
let end_char = rope.try_byte_to_char(end).ok()?;
Some(MultiCursor {
cursor: end_char,
selection: Some(start_char),
range: start_char..end_char,
groups,
..MultiCursor::default()
})
})
.collect::<Vec<_>>();
let start = match self.selection {
Some(selection) => selection.min(self.cursor),
None => self.cursor,
};
let (idx, next_match) = matches
.iter()
.enumerate()
.find(|(_, m)| m.range.start >= start)
.or_else(|| matches.first().map(|m| (0, m)))
.ok_or(term)?;
self.cursor = next_match.range.end;
self.selection = None;
Ok((idx, matches))
}
fn all_multiline_matches<S: SearchTerm>(
&mut self,
range: Self::Range,
term: S,
) -> Result<(usize, Self::Output), S> {
let buf = self.buffer.borrow_move();
let rope = &buf.rope;
let (whole_rope, offset) = match range {
None => (
rope.chunks()
.fold(String::with_capacity(rope.len_bytes()), |mut acc, s| {
acc.push_str(s);
acc
}),
0,
),
Some(SelectionRange { start, lines }) => (
rope.lines_at(*start)
.take(lines.get())
.fold(String::new(), |mut acc, l| {
acc.push_str(&Cow::from(l));
acc
}),
rope.line_to_byte(*start),
),
};
let matches = term
.match_ranges(&whole_rope)
.map(|m| m + offset)
.filter_map(|SearchMatch { start, end, groups }| {
let start_char = rope.try_byte_to_char(start).ok()?;
let end_char = rope.try_byte_to_char(end).ok()?;
Some(MultiCursor {
cursor: end_char,
selection: Some(start_char),
range: start_char..end_char,
groups,
..MultiCursor::default()
})
})
.collect::<Vec<_>>();
let start = match self.selection {
Some(selection) => selection.min(self.cursor),
None => self.cursor,
};
let (idx, next_match) = matches
.iter()
.enumerate()
.find(|(_, m)| m.range.start >= start)
.or_else(|| matches.first().map(|m| (0, m)))
.ok_or(term)?;
self.cursor = next_match.range.end;
self.selection = None;
Ok((idx, matches))
}
fn search_autocomplete_matches(&self, prefix: String) -> Vec<String> {
autocomplete_matches(&self.buffer.borrow().rope, prefix)
}
fn set_error<S: Into<Cow<'static, str>>>(&mut self, err: S) {
BufferContext::set_error(self, err)
}
}
#[derive(Default)]
pub struct ToggledBookmarks {
added: usize,
removed: usize,
}
impl std::ops::AddAssign for ToggledBookmarks {
fn add_assign(&mut self, Self { added, removed }: Self) {
self.added += added;
self.removed += removed;
}
}
impl TryFrom<ToggledBookmarks> for BufferMessage {
type Error = ();
fn try_from(toggled: ToggledBookmarks) -> Result<Self, ()> {
match toggled {
ToggledBookmarks {
added: 0,
removed: 0,
} => Err(()),
ToggledBookmarks {
added: 1,
removed: 0,
} => Ok(BufferMessage::Notice("Bookmark Added".into())),
ToggledBookmarks {
added: n,
removed: 0,
} => Ok(BufferMessage::Notice(format!("{n} Bookmarks Added").into())),
ToggledBookmarks {
added: 0,
removed: 1,
} => Ok(BufferMessage::Notice("Bookmark Removed".into())),
ToggledBookmarks {
added: 0,
removed: n,
} => Ok(BufferMessage::Notice(
format!("{n} Bookmarks Removed").into(),
)),
ToggledBookmarks { .. } => Ok(BufferMessage::Notice("Bookmarks Toggled".into())),
}
}
}
pub struct MainCursor<'a> {
cursor: usize,
cursor_column: usize,
selection: Option<usize>,
undo: &'a mut Vec<BufferContextState>,
redo: &'a mut Vec<BufferContextState>,
}
impl MainCursor<'_> {
fn perform_update(self) {
self.undo.push(BufferContextState {
cursor: self.cursor,
cursor_column: self.cursor_column,
selection: self.selection,
});
self.redo.clear();
}
}
pub struct AltCursor<'a> {
cursor: &'a mut usize,
cursor_column: usize,
selection: &'a mut Option<usize>,
undo: &'a mut Vec<BufferContextState>,
redo: &'a mut Vec<BufferContextState>,
}
impl AltCursor<'_> {
fn perform_update(&mut self) {
self.undo.push(BufferContextState {
cursor: *self.cursor,
cursor_column: self.cursor_column,
selection: *self.selection,
});
self.redo.clear();
}
fn perform_undo(&mut self) {
if let Some(mut ctx_state) = self.undo.pop() {
std::mem::swap(self.cursor, &mut ctx_state.cursor);
std::mem::swap(&mut self.cursor_column, &mut ctx_state.cursor_column);
std::mem::swap(self.selection, &mut ctx_state.selection);
self.redo.push(ctx_state);
}
}
fn perform_redo(&mut self) {
if let Some(mut ctx_state) = self.redo.pop() {
std::mem::swap(self.cursor, &mut ctx_state.cursor);
std::mem::swap(&mut self.cursor_column, &mut ctx_state.cursor_column);
std::mem::swap(self.selection, &mut ctx_state.selection);
self.undo.push(ctx_state);
}
}
}
#[derive(Default)]
pub struct MultiCursor {
range: Range<usize>,
cursor: usize,
selection: Option<usize>,
groups: Vec<String>,
undo: Vec<MultiCursorState>,
redo: Vec<MultiCursorState>,
}
impl MultiCursor {
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn start(&self) -> usize {
self.range.start
}
pub fn end(&self) -> usize {
self.range.end
}
pub fn widen_selection(&mut self, cursor: &mut usize) {
self.selection = Some(self.range.start);
if *cursor == self.cursor {
*cursor = self.range.end;
}
self.cursor = self.range.end;
}
fn zap_selection(
&mut self,
rope: &mut ropey::Rope,
cursor: &mut usize,
secondary: &mut Secondary,
) -> Option<usize> {
let (start, end) = reorder(self.cursor, self.selection.take()?);
let removed = end - start;
rope.try_remove(secondary.remove(start..end)).ok()?;
if end <= *cursor {
*cursor -= removed;
} else if start <= *cursor {
*cursor = start;
}
secondary.update(|a| {
if start <= *a {
*a -= removed;
}
});
self.cursor = start;
self.range.end -= removed;
Some(removed)
}
#[must_use]
fn insert_char(
&mut self,
rope: &mut ropey::Rope,
cursor: &mut usize,
secondary: &mut Secondary,
c: char,
) -> (usize, usize) {
use std::cmp::Ordering;
match (&mut self.selection, c) {
(Some(selection), '(') => {
*cursor += match self.cursor.cmp(cursor) {
Ordering::Less => 2,
Ordering::Equal => 1,
Ordering::Greater => 0,
};
perform_surround(rope, &mut self.cursor, selection, secondary, ['(', ')']);
self.range.end += 2;
(0, 2)
}
(Some(selection), '[') => {
*cursor += match self.cursor.cmp(cursor) {
Ordering::Less => 2,
Ordering::Equal => 1,
Ordering::Greater => 0,
};
perform_surround(rope, &mut self.cursor, selection, secondary, ['[', ']']);
self.range.end += 2;
(0, 2)
}
(Some(selection), '{') => {
*cursor += match self.cursor.cmp(cursor) {
Ordering::Less => 2,
Ordering::Equal => 1,
Ordering::Greater => 0,
};
perform_surround(rope, &mut self.cursor, selection, secondary, ['{', '}']);
self.range.end += 2;
(0, 2)
}
(Some(selection), '<') => {
*cursor += match self.cursor.cmp(cursor) {
Ordering::Less => 2,
Ordering::Equal => 1,
Ordering::Greater => 0,
};
perform_surround(rope, &mut self.cursor, selection, secondary, ['<', '>']);
self.range.end += 2;
(0, 2)
}
(Some(selection), '\"') => {
*cursor += match self.cursor.cmp(cursor) {
Ordering::Less => 2,
Ordering::Equal => 1,
Ordering::Greater => 0,
};
perform_surround(rope, &mut self.cursor, selection, secondary, ['\"', '\"']);
self.range.end += 2;
(0, 2)
}
(Some(selection), '\'') => {
*cursor += match self.cursor.cmp(cursor) {
Ordering::Less => 2,
Ordering::Equal => 1,
Ordering::Greater => 0,
};
perform_surround(rope, &mut self.cursor, selection, secondary, ['\'', '\'']);
self.range.end += 2;
(0, 2)
}
_ => {
let zapped = self
.zap_selection(rope, cursor, secondary)
.unwrap_or_default();
let inserted = insert_char_or_pair(rope, self.cursor, secondary, c);
*cursor += match self.cursor.cmp(cursor) {
Ordering::Less => inserted,
Ordering::Equal => 1,
Ordering::Greater => 0,
};
self.cursor += 1;
self.range.end += inserted;
(zapped, inserted)
}
}
}
#[must_use]
fn insert_str(
&mut self,
rope: &mut ropey::Rope,
cursor: &mut usize,
secondary: &mut Secondary,
s: &str,
s_chars: usize,
) -> usize {
let zapped = self
.zap_selection(rope, cursor, secondary)
.unwrap_or_default();
if self.cursor <= *cursor {
*cursor += s_chars;
}
secondary.update(|a| {
if self.cursor <= *a {
*a += s_chars;
}
});
rope.insert(self.cursor, s);
self.cursor += s_chars;
self.range.end += s_chars;
zapped
}
#[must_use]
fn paste_single(
&mut self,
rope: &mut ropey::Rope,
cursor: &mut usize,
secondary: &mut Secondary,
cut: &CutBuffer,
) -> usize {
let zapped = self
.zap_selection(rope, cursor, secondary)
.unwrap_or_default();
if self.cursor <= *cursor {
*cursor += cut.chars_len;
}
secondary.update(|a| {
if self.cursor <= *a {
*a += cut.chars_len;
}
});
rope.insert(self.cursor, cut.as_str());
secondary.add_bookmarks(cut.bookmarks.iter().map(|b| self.cursor + b));
self.cursor += cut.chars_len;
self.range.end += cut.chars_len;
zapped
}
#[must_use]
fn paste_multiple(
&mut self,
rope: &mut ropey::Rope,
cursor: &mut usize,
secondary: &mut Secondary,
cut: &mut CutBuffer,
) -> (usize, usize) {
match self.take_selection(rope, cursor, secondary) {
None => (
self.paste_single(rope, cursor, secondary, cut),
cut.chars_len,
),
Some(mut old_selection) => {
let zapped = old_selection.chars_len;
let added = cut.chars_len;
assert_eq!(self.paste_single(rope, cursor, secondary, cut), 0);
std::mem::swap(&mut old_selection, cut);
(zapped, added)
}
}
}
fn indent(
&mut self,
rope: &mut ropey::Rope,
secondary: &mut Secondary,
indent: &str,
indent_chars: usize,
) {
secondary.update(|a| {
if self.range.start <= *a {
*a += indent_chars;
}
});
rope.insert(self.range.start, indent);
self.cursor += indent_chars;
self.range.end += indent_chars;
}
fn can_unindent(&self, rope: &ropey::Rope, indent: &str, indent_len: usize) -> bool {
rope.chars_at(self.range.start)
.take(indent_len)
.eq(indent.chars())
}
fn un_indent(
&mut self,
rope: &mut ropey::Rope,
secondary: &mut Secondary,
indent_chars: usize,
) -> Result<(), ()> {
rope.try_remove(secondary.remove(self.range.start..self.range.start + indent_chars))
.map_err(|_| ())?;
self.cursor = self.cursor.saturating_sub(indent_chars);
self.range.end -= indent_chars;
secondary.update(|a| {
if self.range.start <= *a {
*a = a.saturating_sub(indent_chars);
}
});
Ok(())
}
fn backspace(
&mut self,
rope: &mut ropey::Rope,
cursor: &mut usize,
secondary: &mut Secondary,
) -> Result<usize, ()> {
use std::cmp::Ordering;
if self.cursor <= self.range.start {
return Err(());
}
match self.zap_selection(rope, cursor, secondary) {
None => {
let (before, after) = backspace_or_un_pair(rope, self.cursor, secondary)?;
let total = before + after;
*cursor -= match self.cursor.cmp(cursor) {
Ordering::Less => total,
Ordering::Equal => before,
Ordering::Greater => 0,
};
self.cursor -= before;
self.range.end -= total;
Ok(total)
}
Some(removed) => Ok(removed),
}
}
fn delete(
&mut self,
rope: &mut ropey::Rope,
cursor: &mut usize,
secondary: &mut Secondary,
) -> Result<usize, ()> {
if self.cursor < self.range.end {
match &mut self.selection {
None => {
let to_delete = rope
.chars_at(self.cursor)
.skip(1)
.take_while(|c| is_grapheme_part(*c))
.count()
+ 1;
if self.cursor < *cursor {
*cursor = cursor.saturating_sub(to_delete);
}
secondary.update(|a| {
if self.cursor < *a {
*a = a.saturating_sub(to_delete);
}
});
let _ = rope.try_remove(secondary.remove(self.cursor..self.cursor + to_delete));
self.range.end -= to_delete;
Ok(to_delete)
}
Some(selection) => {
match delete_surround(rope, &mut self.cursor, selection, secondary, |pos| {
self.range.contains(&pos)
}) {
Ok(()) => {
use std::cmp::Ordering;
self.range.end -= 2;
match (self.cursor + 1).cmp(cursor) {
Ordering::Less => {
*cursor = cursor.saturating_sub(2);
}
Ordering::Equal => {
*cursor = cursor.saturating_sub(1);
}
Ordering::Greater => { }
}
Ok(2)
}
Err(()) => self.zap_selection(rope, cursor, secondary).ok_or(()),
}
}
}
} else {
Err(())
}
}
fn cursor_back(
&mut self,
cursor: &mut usize,
cursor_col: &mut usize,
rope: &ropey::Rope,
selecting: bool,
) {
let to_retreat = rope
.chars_at(self.cursor)
.reversed()
.take_while(|c| is_grapheme_part(*c))
.count()
+ 1;
if self.cursor > self.range.start {
if self.cursor == *cursor {
*cursor = cursor.saturating_sub(to_retreat);
*cursor_col = cursor_column(rope, *cursor);
}
update_selection(&mut self.selection, self.cursor, selecting);
self.cursor -= to_retreat;
} else if !selecting {
self.selection = None;
}
}
fn cursor_forward(
&mut self,
cursor: &mut usize,
cursor_col: &mut usize,
rope: &ropey::Rope,
selecting: bool,
) {
if self.cursor < self.range.end {
let to_advance = rope
.chars_at(self.cursor + 1)
.take_while(|c| is_grapheme_part(*c))
.count()
+ 1;
if self.cursor == *cursor {
*cursor += to_advance;
*cursor_col = cursor_column(rope, *cursor);
}
update_selection(&mut self.selection, self.cursor, selecting);
self.cursor += to_advance;
} else if !selecting {
self.selection = None;
}
}
fn cursor_home(
&mut self,
cursor: &mut usize,
cursor_col: &mut usize,
rope: &ropey::Rope,
selecting: bool,
) {
if self.cursor == *cursor {
*cursor = self.range.start;
*cursor_col = cursor_column(rope, *cursor);
}
update_selection(&mut self.selection, self.cursor, selecting);
self.cursor = self.range.start;
}
fn cursor_end(
&mut self,
cursor: &mut usize,
cursor_col: &mut usize,
rope: &ropey::Rope,
selecting: bool,
) {
if self.cursor == *cursor {
*cursor = self.range.end;
*cursor_col = cursor_column(rope, *cursor);
}
update_selection(&mut self.selection, self.cursor, selecting);
self.cursor = self.range.end;
}
fn autocomplete_prefix(&self, rope: &ropey::Rope) -> Option<(usize, String)> {
if let Some(c) = rope.get_char(self.cursor)
&& is_word(c)
{
return None;
}
let prefix_chars = rope
.chars_at(self.cursor)
.reversed()
.take(self.cursor - self.range.start)
.take_while(|c| is_word_part(*c))
.collect::<Vec<_>>();
if prefix_chars.is_empty() {
return None;
}
let offset = self
.cursor
.checked_sub(prefix_chars.len())?
.checked_sub(self.range.start)?;
Some((offset, prefix_chars.into_iter().rev().collect()))
}
#[allow(clippy::too_many_arguments)]
fn autocomplete(
&mut self,
rope: &mut ropey::Rope,
cursor: &mut usize,
secondary: &mut Secondary<'_, '_>,
offset: usize,
original_chars: usize,
replacement: &str,
replacement_chars: usize,
) {
self.selection = None;
let abs_start = self.range.start + offset;
rope.remove(secondary.remove(abs_start..abs_start + original_chars));
if self.range.start <= *cursor {
*cursor -= original_chars;
}
secondary.update(|a| {
if self.range.start <= *a {
*a -= original_chars;
}
});
self.range.end -= original_chars;
rope.insert(abs_start, replacement);
if self.range.start <= *cursor {
*cursor += replacement_chars;
}
secondary.update(|a| {
if self.range.start <= *a {
*a += replacement_chars;
}
});
self.cursor = abs_start + replacement_chars;
self.range.end += replacement_chars;
}
fn selection_range(&self) -> Option<Range<usize>> {
self.selection.map(|sel| {
let (start, end) = reorder(self.cursor, sel);
start..end
})
}
fn get_selection(
&mut self,
rope: &ropey::Rope,
bookmarks: &private::Bookmarks,
) -> Option<CutBuffer> {
let (start, end) = reorder(self.cursor, self.selection.take()?);
Some(CutBuffer::new(
rope.get_slice(start..end)?,
bookmarks.range(start..end).map(|b| b - start),
))
}
fn take_selection(
&mut self,
rope: &mut ropey::Rope,
cursor: &mut usize,
secondary: &mut Secondary,
) -> Option<CutBuffer> {
let (start, end) = reorder(self.cursor, self.selection.take()?);
let cut = CutBuffer::new(
rope.get_slice(start..end)?,
secondary.extract_bookmarks(start..end).map(|b| b - start),
);
let removed = end - start;
rope.try_remove(secondary.remove(start..end)).ok()?;
if end <= *cursor {
*cursor -= removed;
} else if start <= *cursor {
*cursor = start;
}
secondary.update(|a| {
if start <= *a {
*a -= removed;
}
});
self.cursor = start;
self.range.end -= removed;
Some(cut)
}
pub fn paste_group_count(&self) -> Option<NonZero<usize>> {
NonZero::new(self.groups.len())
}
pub fn insert_group(
&mut self,
rope: &mut ropey::Rope,
cursor: &mut usize,
secondary: &mut Secondary,
group_num: usize,
) -> Option<(usize, usize)> {
let s = self.groups.get(group_num).cloned()?;
let s_chars = s.chars().count();
let zapped = self.insert_str(rope, cursor, secondary, &s, s_chars);
Some((zapped, s_chars))
}
fn perform_update(&mut self) {
self.undo.push(MultiCursorState {
range: self.range.clone(),
cursor: self.cursor,
selection: self.selection,
});
self.redo.clear();
}
fn perform_undo(&mut self) {
if let Some(mut ctx_state) = self.undo.pop() {
std::mem::swap(&mut self.range, &mut ctx_state.range);
std::mem::swap(&mut self.cursor, &mut ctx_state.cursor);
std::mem::swap(&mut self.selection, &mut ctx_state.selection);
self.redo.push(ctx_state);
}
}
fn perform_redo(&mut self) {
if let Some(mut ctx_state) = self.redo.pop() {
std::mem::swap(&mut self.range, &mut ctx_state.range);
std::mem::swap(&mut self.cursor, &mut ctx_state.cursor);
std::mem::swap(&mut self.selection, &mut ctx_state.selection);
self.undo.push(ctx_state);
}
}
}
impl From<usize> for MultiCursor {
fn from(cursor: usize) -> Self {
Self {
range: cursor..cursor,
cursor,
..MultiCursor::default()
}
}
}
impl From<Range<usize>> for MultiCursor {
fn from(range: Range<usize>) -> Self {
Self {
cursor: range.end,
selection: Some(range.start),
range,
..MultiCursor::default()
}
}
}
impl From<SelectedLine> for MultiCursor {
fn from(line: SelectedLine) -> Self {
Self {
cursor: line.start,
range: line.start..line.end,
..MultiCursor::default()
}
}
}
impl std::ops::AddAssign<usize> for MultiCursor {
fn add_assign(&mut self, chars: usize) {
self.range.start += chars;
self.range.end += chars;
self.cursor += chars;
if let Some(selection) = &mut self.selection {
*selection += chars;
}
}
}
impl std::ops::SubAssign<usize> for MultiCursor {
fn sub_assign(&mut self, chars: usize) {
self.range.start -= chars;
self.range.end -= chars;
self.cursor -= chars;
if let Some(selection) = &mut self.selection {
*selection -= chars;
}
}
}
fn multicursor_update<T: Copy, E>(
mut cursors: &mut [MultiCursor],
mut on_cursor: impl FnMut(&mut MultiCursor) -> Result<T, E>,
mut on_next: impl FnMut(&mut MultiCursor, T),
) {
loop {
match cursors {
[] => break,
[m] => {
let _ = on_cursor(m);
break;
}
[m, rest @ ..] => {
if let Ok(t) = on_cursor(m) {
rest.iter_mut().for_each(|r| on_next(r, t));
}
cursors = rest;
}
}
}
}
struct MultiCursorState {
range: Range<usize>,
cursor: usize,
selection: Option<usize>,
}
fn insert_char_or_pair(
rope: &mut ropey::Rope,
cursor: usize,
alt: &mut Secondary,
c: char,
) -> usize {
match match c {
'(' => Err("()"),
'[' => Err("[]"),
'{' => Err("{}"),
c => Ok(c),
} {
Ok(c) => {
rope.insert_char(cursor, c);
alt.update(|a| {
if *a >= cursor {
*a += 1;
}
});
1
}
Err(s) => {
rope.insert(cursor, s);
alt.update(|a| {
*a += match (*a).cmp(&cursor) {
std::cmp::Ordering::Greater => 2,
std::cmp::Ordering::Equal => 1,
std::cmp::Ordering::Less => 0,
};
});
2
}
}
}
fn backspace_or_un_pair(
rope: &mut ropey::Rope,
cursor: usize,
alt: &mut Secondary,
) -> Result<(usize, usize), ()> {
fn remove_pair(
rope: &mut ropey::Rope,
alt: &mut Secondary,
prev: usize,
cursor: usize,
) -> Result<(usize, usize), ()> {
rope.try_remove(alt.remove(prev..cursor + 1))
.map_err(|_| ())?;
alt.update(|a| {
*a -= match (*a).cmp(&cursor) {
std::cmp::Ordering::Greater => 2,
std::cmp::Ordering::Equal => 1,
std::cmp::Ordering::Less => 0,
};
});
Ok((1, 1))
}
let prev = cursor.checked_sub(1).ok_or(())?;
match rope.get_char(prev).ok_or(())? {
'(' if rope.get_char(cursor) == Some(')') => remove_pair(rope, alt, prev, cursor),
'[' if rope.get_char(cursor) == Some(']') => remove_pair(rope, alt, prev, cursor),
'{' if rope.get_char(cursor) == Some('}') => remove_pair(rope, alt, prev, cursor),
c if is_grapheme_part(c) => {
let removed = (rope
.chars_at(cursor)
.reversed()
.take_while(|c| is_grapheme_part(*c))
.count()
+ 1)
.min(cursor);
rope.try_remove(alt.remove(cursor.saturating_sub(removed)..cursor))
.map_err(|_| ())?;
alt.update(|a| {
if *a >= cursor {
*a -= removed;
}
});
Ok((removed, 0))
}
_ => {
rope.try_remove(alt.remove(prev..cursor)).map_err(|_| ())?;
alt.update(|a| {
if *a >= cursor {
*a -= 1;
}
});
Ok((1, 0))
}
}
}
pub enum SelectionType {
Term(String),
Range(SelectionRange),
}
pub struct SelectionRange {
start: usize,
lines: NonZero<usize>,
}
pub trait SearchTerm: std::fmt::Display + Clone {
fn match_ranges(&self, s: &str) -> impl Iterator<Item = SearchMatch>;
}
impl<T: SearchTerm> SearchTerm for &T {
fn match_ranges(&self, s: &str) -> impl Iterator<Item = SearchMatch> {
(**self).match_ranges(s)
}
}
pub struct SearchMatch {
start: usize,
end: usize,
groups: Vec<String>,
}
impl std::ops::Add<usize> for SearchMatch {
type Output = Self;
fn add(self, rhs: usize) -> Self {
Self {
start: self.start + rhs,
end: self.end + rhs,
groups: self.groups,
}
}
}
impl SearchTerm for fancy_regex::Regex {
fn match_ranges(&self, s: &str) -> impl Iterator<Item = SearchMatch> {
self.captures_iter(s).filter_map(|c| c.ok()).map(|c| {
let first = c.get(0).unwrap();
SearchMatch {
start: first.start(),
end: first.end(),
groups: c
.iter()
.map(|m| m.map(|m| m.as_str().to_string()).unwrap_or_default())
.collect(),
}
})
}
}
impl SearchTerm for String {
fn match_ranges(&self, s: &str) -> impl Iterator<Item = SearchMatch> {
s.match_indices(self.as_str()).map(|(idx, s)| SearchMatch {
start: idx,
end: idx + s.len(),
groups: vec![],
})
}
}
#[derive(Clone)]
pub struct Normalizations(std::collections::HashSet<String>);
impl TryFrom<String> for Normalizations {
type Error = String;
#[allow(clippy::manual_try_fold)]
fn try_from(base: String) -> Result<Self, Self::Error> {
use unicode_normalization::UnicodeNormalization;
[
base.nfc().collect(),
base.nfkd().collect(),
base.nfd().collect(),
base.nfkc().collect(),
]
.into_iter()
.fold(Err(base), |acc, s| match acc {
Ok(mut map) => {
map.0.insert(s);
Ok(map)
}
Err(mut current) => {
if current == s {
Err(current)
} else {
Ok(Self([std::mem::take(&mut current), s].into()))
}
}
})
}
}
impl std::fmt::Display for Normalizations {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.0.iter().min_by_key(|s| s.len()) {
Some(s) => s.fmt(f),
None => "".fmt(f), }
}
}
impl SearchTerm for Normalizations {
fn match_ranges(&self, s: &str) -> impl Iterator<Item = SearchMatch> {
let mut ranges = self
.0
.iter()
.flat_map(|string| string.match_ranges(s))
.collect::<Vec<_>>();
ranges.sort_unstable_by_key(|r| r.start);
ranges.into_iter()
}
}
#[derive(Clone)]
pub struct CaseInsensitiveNormalizations(Vec<fancy_regex::Regex>);
impl From<Normalizations> for CaseInsensitiveNormalizations {
fn from(normalizations: Normalizations) -> Self {
Self(
normalizations
.0
.into_iter()
.filter_map(|s| {
fancy_regex::RegexBuilder::new(&s)
.case_insensitive(true)
.build()
.ok()
})
.collect(),
)
}
}
impl std::fmt::Display for CaseInsensitiveNormalizations {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.0.iter().min_by_key(|s| s.to_string().len()) {
Some(s) => s.fmt(f),
None => "".fmt(f), }
}
}
impl SearchTerm for CaseInsensitiveNormalizations {
fn match_ranges(&self, s: &str) -> impl Iterator<Item = SearchMatch> {
let mut ranges = self
.0
.iter()
.flat_map(|regex| regex.match_ranges(s))
.collect::<Vec<_>>();
ranges.sort_unstable_by_key(|r| r.start);
ranges.into_iter()
}
}
pub struct Modified;
fn line_char_range(rope: &ropey::Rope, line: usize) -> Option<(usize, usize)> {
Some((
rope.try_line_to_char(line).ok()?,
rope.try_line_to_char(line + 1).ok()?.saturating_sub(1),
))
}
struct SelectedLine {
start: usize,
end: usize,
}
impl SelectedLine {
fn is_empty(&self) -> bool {
self.start >= self.end
}
}
fn selected_lines(
rope: &ropey::Rope,
cursor: usize,
selection: Option<usize>,
) -> Box<dyn DoubleEndedIterator<Item = SelectedLine> + '_> {
match selection {
None => match rope.try_char_to_line(cursor) {
Ok(line) => Box::new(
line_char_range(rope, line)
.map(|(start, end)| SelectedLine { start, end })
.into_iter(),
),
Err(_) => Box::new(std::iter::empty()),
},
Some(selection) => {
let (start, end) = reorder(cursor, selection);
if let Ok(start_line) = rope.try_char_to_line(start)
&& let Ok(end_line) = rope.try_char_to_line(end)
{
Box::new((start_line..=end_line).filter_map(move |line| {
line_char_range(rope, line).map(|(start, end)| SelectedLine { start, end })
}))
} else {
Box::new(std::iter::empty())
}
}
}
}
fn cursor_column(rope: &ropey::Rope, cursor: usize) -> usize {
use unicode_width::UnicodeWidthChar;
rope.try_char_to_line(cursor)
.ok()
.and_then(|line| rope.try_line_to_char(line).ok())
.map(|line_start| {
rope.chars_at(line_start)
.take(cursor.saturating_sub(line_start))
.map(|c| match c {
'\t' => *SPACES_PER_TAB,
c => c.width().unwrap_or(1),
})
.sum()
})
.unwrap_or(0)
}
fn apply_cursor_column(
rope: &ropey::Rope,
mut cursor_column: usize,
mut line_start: usize,
line_end: usize,
) -> usize {
use unicode_width::UnicodeWidthChar;
let mut chars = rope.chars_at(line_start);
while cursor_column > 0 && line_start < line_end {
match chars.next() {
Some('\t') => {
cursor_column = cursor_column.saturating_sub(*SPACES_PER_TAB);
line_start += 1;
}
Some(c) => {
cursor_column = cursor_column.saturating_sub(c.width().unwrap_or(1));
line_start += 1;
}
None => break,
}
}
while let Some(c) = chars.next()
&& is_grapheme_part(c)
{
line_start += 1;
}
line_start
}
fn line_start_to_cursor(rope: &ropey::Rope, cursor: usize) -> Option<impl Iterator<Item = char>> {
let line = rope.try_char_to_line(cursor).ok()?;
let start = rope.try_line_to_char(line).ok()?;
rope.get_chars_at(start)
.map(|iter| iter.take(cursor.saturating_sub(start)))
}
fn line_chars(rope: &ropey::Rope, cursor: usize) -> Option<impl Iterator<Item = char>> {
line_char_range(rope, rope.try_char_to_line(cursor).ok()?).and_then(|(start, end)| {
rope.get_chars_at(start)
.map(|iter| iter.take(end.saturating_sub(start)))
})
}
fn update_selection(selection: &mut Option<usize>, cursor: usize, selecting: bool) {
if selecting && selection.is_none() {
*selection = Some(cursor);
} else if !selecting && selection.is_some() {
*selection = None
}
}
fn zap_selection(
rope: &mut ropey::Rope,
cursor: &mut usize,
column: &mut usize,
selection: usize,
secondary: &mut Secondary,
) {
let (selection_start, selection_end) = reorder(*cursor, selection);
if rope
.try_remove(secondary.remove(selection_start..selection_end))
.is_ok()
{
*cursor = selection_start;
*column = cursor_column(rope, *cursor);
secondary.update(|pos| {
if (selection_start..selection_end).contains(pos) {
*pos = selection_start;
} else {
*pos -= selection_end - selection_start;
}
});
}
}
fn select_next_char<const FORWARD: bool>(
rope: &ropey::Rope,
cursor: usize,
target: char,
stack: Option<char>,
) -> Option<usize> {
let mut chars = rope.chars_at(cursor);
if !FORWARD {
chars.reverse();
}
match stack {
None => chars
.position(|c| c == target)
.map(|pos| if FORWARD { cursor + pos } else { cursor - pos }),
Some(stack) => {
let mut stacked = 0;
chars
.zip(0..)
.find(|(c, _)| {
if *c == target {
if stacked > 0 {
stacked -= 1;
false
} else {
true
}
} else if *c == stack {
stacked += 1;
false
} else {
false
}
})
.map(|(_, pos)| if FORWARD { cursor + pos } else { cursor - pos })
}
}
}
pub fn next_pairing_char(rope: &ropey::Rope, offset: usize) -> Option<(char, usize)> {
let mut stacked_paren = 0;
let mut stacked_square_bracket = 0;
let mut stacked_curly_bracket = 0;
let mut stacked_angle_bracket = 0;
fn checked_dec(i: &mut usize) -> bool {
if *i > 0 {
*i -= 1;
false
} else {
true
}
}
if offset > rope.len_chars() {
return None;
}
rope.chars_at(offset)
.zip(0..)
.find(|(c, _)| match c {
'(' => {
stacked_paren += 1;
false
}
'[' => {
stacked_square_bracket += 1;
false
}
'{' => {
stacked_curly_bracket += 1;
false
}
'<' => {
stacked_angle_bracket += 1;
false
}
')' => checked_dec(&mut stacked_paren),
']' => checked_dec(&mut stacked_square_bracket),
'}' => checked_dec(&mut stacked_curly_bracket),
'>' => checked_dec(&mut stacked_angle_bracket),
'"' | '\'' => true,
_ => false,
})
.map(|(c, pos)| (c, offset + pos))
}
pub fn prev_pairing_char(rope: &ropey::Rope, offset: usize) -> Option<(char, usize)> {
let mut stacked_paren = 0;
let mut stacked_square_bracket = 0;
let mut stacked_curly_bracket = 0;
let mut stacked_angle_bracket = 0;
fn checked_dec(i: &mut usize) -> bool {
if *i > 0 {
*i -= 1;
false
} else {
true
}
}
if offset > rope.len_chars() {
return None;
}
let mut chars = rope.chars_at(offset);
chars.reverse();
chars
.zip(0..)
.find(|(c, _)| match c {
')' => {
stacked_paren += 1;
false
}
']' => {
stacked_square_bracket += 1;
false
}
'}' => {
stacked_curly_bracket += 1;
false
}
'>' => {
stacked_angle_bracket += 1;
false
}
'(' => checked_dec(&mut stacked_paren),
'[' => checked_dec(&mut stacked_square_bracket),
'{' => checked_dec(&mut stacked_curly_bracket),
'<' => checked_dec(&mut stacked_angle_bracket),
'"' | '\'' => true,
_ => false,
})
.map(|(c, pos)| (c, offset - pos))
}
pub fn prev_opening_char(rope: &ropey::Rope, offset: usize, limit: usize) -> Option<(char, usize)> {
let mut stacked_paren = 0;
let mut stacked_square_bracket = 0;
let mut stacked_curly_bracket = 0;
fn checked_dec(i: &mut usize) -> bool {
if *i > 0 {
*i -= 1;
false
} else {
true
}
}
if offset > rope.len_chars() {
return None;
}
let mut chars = rope.chars_at(offset);
chars.reverse();
chars
.zip(0..limit)
.find(|(c, _)| match c {
')' => {
stacked_paren += 1;
false
}
']' => {
stacked_square_bracket += 1;
false
}
'}' => {
stacked_curly_bracket += 1;
false
}
'(' => checked_dec(&mut stacked_paren),
'[' => checked_dec(&mut stacked_square_bracket),
'{' => checked_dec(&mut stacked_curly_bracket),
_ => false,
})
.map(|(c, pos)| (c, offset - pos))
}
pub fn next_closing_char(rope: &ropey::Rope, offset: usize, limit: usize) -> Option<(char, usize)> {
let mut stacked_paren = 0;
let mut stacked_square_bracket = 0;
let mut stacked_curly_bracket = 0;
fn checked_dec(i: &mut usize) -> bool {
if *i > 0 {
*i -= 1;
false
} else {
true
}
}
if offset > rope.len_chars() {
return None;
}
rope.chars_at(offset)
.zip(0..limit)
.find(|(c, _)| match c {
'(' => {
stacked_paren += 1;
false
}
'[' => {
stacked_square_bracket += 1;
false
}
'{' => {
stacked_curly_bracket += 1;
false
}
')' => checked_dec(&mut stacked_paren),
']' => checked_dec(&mut stacked_square_bracket),
'}' => checked_dec(&mut stacked_curly_bracket),
_ => false,
})
.map(|(c, pos)| {
(
match c {
')' => '(',
']' => '[',
'}' => '{',
_ => unreachable!(),
},
offset + pos,
)
})
}
fn perform_surround(
rope: &mut ropey::Rope,
cursor: &mut usize,
selection: &mut usize,
alt: &mut Secondary<'_, '_>,
[start, end]: [char; 2],
) {
let (start_pos, end_pos) = reorder(&mut *cursor, selection);
if rope.try_insert_char(*end_pos, end).is_ok()
&& rope.try_insert_char(*start_pos, start).is_ok()
{
alt.update(|pos| {
*pos += if *pos > *end_pos {
2
} else if *pos >= *start_pos {
1
} else {
0
}
});
*start_pos += 1;
*end_pos += 1;
}
}
fn delete_surround(
rope: &mut ropey::Rope,
cursor: &mut usize,
selection: &mut usize,
alt: &mut Secondary<'_, '_>,
mut in_bounds: impl FnMut(usize) -> bool,
) -> Result<(), ()> {
let (start, end) = reorder(&mut *cursor, selection);
if let Some(prev_pos) = start.checked_sub(1)
&& in_bounds(prev_pos)
&& in_bounds(*end)
&& let Some(prev_char) = rope.get_char(prev_pos)
&& let Some(next_char) = rope.get_char(*end)
&& matches!(
(prev_char, next_char),
('(', ')') | ('[', ']') | ('{', '}') | ('<', '>') | ('"', '"') | ('\'', '\'')
)
&& rope.try_remove(alt.remove(*end..*end + 1)).is_ok()
&& rope.try_remove(alt.remove(prev_pos..*start)).is_ok()
{
alt.update(|pos| {
*pos -= if *pos > *end {
2
} else if *pos >= *start {
1
} else {
0
}
});
*end -= 1;
*start -= 1;
Ok(())
} else {
Err(())
}
}
impl From<Buffer> for BufferContext {
fn from(buffer: Buffer) -> Self {
Self {
buffer: buffer.into(),
cursor: 0,
cursor_column: 0,
selection: None,
message: None,
undo: vec![],
redo: vec![],
}
}
}
#[derive(Clone, Default)]
pub struct BufferList {
buffers: Vec<BufferContext>,
current: usize,
}
impl BufferList {
pub fn new(paths: impl IntoIterator<Item = Source>) -> std::io::Result<Self> {
let buffers = paths
.into_iter()
.map(|p| Buffer::open(p).map(BufferContext::from))
.collect::<Result<Vec<_>, _>>()?;
if buffers.is_empty() {
Ok(Self {
buffers: vec![Buffer::tutorial().into()],
current: 0,
})
} else {
Ok(Self {
buffers,
current: 0,
})
}
}
pub fn len(&self) -> usize {
self.buffers.len()
}
pub fn is_empty(&self) -> bool {
self.buffers.is_empty()
}
pub fn multiple_buffers(&self) -> bool {
self.buffers.len() > 1
}
pub fn push(&mut self, buffer: BufferContext, select: bool) {
self.buffers.push(buffer);
if select {
self.current = self.buffers.len() - 1;
}
}
pub fn remove(&mut self, buffer: &BufferId) {
self.buffers.retain(|buf| buf.buffer.id() != *buffer);
self.current = self.current.min(self.buffers.len().saturating_sub(1));
}
pub fn current(&self) -> Option<&BufferContext> {
self.buffers.get(self.current)
}
pub fn current_mut(&mut self) -> Option<&mut BufferContext> {
self.buffers.get_mut(self.current)
}
pub fn next_buffer(&mut self) {
if !self.buffers.is_empty() {
self.current = (self.current + 1) % self.buffers.len()
}
}
pub fn previous_buffer(&mut self) {
if !self.buffers.is_empty() {
self.current = self
.current
.checked_sub(1)
.unwrap_or(self.buffers.len() - 1);
}
}
pub fn select_buffer(&mut self, index: usize) -> Result<&mut BufferContext, ()> {
if index < self.buffers.len() {
self.current = index;
self.buffers.get_mut(index).ok_or(())
} else {
Err(())
}
}
pub fn cursor_viewport_position(&self, viewport_height: usize) -> Option<(usize, usize)> {
let buf = self.current()?;
buf.cursor_position()
.map(|(_, col)| (viewport_height / 2, col))
}
pub fn set_cursor_focus(&mut self, area: Rect, position: Position) {
if let Some(buf) = self.current_mut() {
buf.set_cursor_focus(area, position);
}
}
pub fn update_buf(&mut self, f: impl FnOnce(&mut BufferContext)) {
if let Some(buf) = self.current_mut() {
f(buf);
}
}
pub fn on_buf<T>(&mut self, f: impl FnOnce(&mut BufferContext) -> T) -> Option<T> {
self.current_mut().map(f)
}
pub fn select_by_source(&mut self, source: &Source) -> Result<(), ()> {
match self
.buffers
.iter()
.position(|buf| buf.buffer.borrow().source() == source)
{
Some(idx) => {
self.current = idx;
Ok(())
}
None => Err(()),
}
}
pub fn current_index(&self) -> usize {
self.current
}
pub fn buffers(&self) -> impl Iterator<Item = &BufferContext> {
self.buffers.iter()
}
pub fn buffers_mut(&mut self) -> impl Iterator<Item = &mut BufferContext> {
self.buffers.iter_mut()
}
pub fn set_index(&mut self, index: usize) {
if index < self.buffers.len() {
self.current = index;
}
}
pub fn get_mut(&mut self, idx: usize) -> Option<&mut BufferContext> {
self.buffers.get_mut(idx)
}
pub fn swap_buffers(&mut self, a: usize, b: usize) {
self.buffers.swap(a, b);
if self.current == a {
self.current = b;
} else if self.current == b {
self.current = a;
}
}
pub fn tabs(&self) -> Option<(usize, Vec<String>)> {
(self.buffers.len() > 1).then(|| {
(
self.current,
self.buffers
.iter()
.enumerate()
.map(|(idx, b)| {
if self.current == idx {
format!("[{}]", b.buffer.borrow().source.short_name())
} else {
format!(" {} ", b.buffer.borrow().source.short_name())
}
})
.collect(),
)
})
}
pub fn has_tabs(&self) -> bool {
self.buffers.len() > 1
}
pub fn save_all(&mut self) -> Result<std::io::Result<usize>, Modified> {
let mut count = 0;
for (idx, buf) in self
.buffers
.iter_mut()
.enumerate()
.filter(|(_, buf)| buf.modified())
{
match buf.verified_save() {
Ok(Ok(())) => {
count += 1;
}
Ok(Err(err)) => {
self.current = idx;
return Ok(Err(err));
}
Err(Modified) => {
self.current = idx;
return Err(Modified);
}
}
}
Ok(Ok(count))
}
pub fn reload_all(
&mut self,
alts: &mut [&mut Self],
) -> Result<std::io::Result<usize>, Modified> {
let mut count = 0;
for (idx, buf) in self.buffers.iter_mut().enumerate() {
match buf.verified_reload(
alts.iter_mut()
.filter_map(|a| a.get_mut(idx).map(|a| a.alt_cursor()))
.collect(),
) {
Ok(Ok(())) => {
count += 1;
}
Ok(Err(err)) => {
self.current = idx;
return Ok(Err(err));
}
Err(Modified) => {
self.current = idx;
return Err(Modified);
}
}
}
Ok(Ok(count))
}
pub fn multi_autocomplete_matches(
&self,
cursors: &BTreeMap<usize, Vec<MultiCursor>>,
) -> Option<(BTreeMap<usize, Vec<usize>>, Vec<String>)> {
let mut offsets: BTreeMap<usize, Vec<usize>> = BTreeMap::default();
let mut prefix = None;
for (buf_idx, buf_cursors) in cursors {
let buf = self.buffers.get(*buf_idx)?;
let rope = &buf.buffer.borrow().rope;
let offsets = offsets.entry(*buf_idx).or_default();
for cursor in buf_cursors {
let (offset, p) = cursor.autocomplete_prefix(rope)?;
offsets.push(offset);
match &mut prefix {
None => {
prefix = Some(p);
}
Some(prefix) => {
if prefix != &p {
return None;
}
}
}
}
}
let prefix = prefix?;
let matches = cursors.keys().fold(Trie::default(), |acc, buf_idx| {
match self.buffers.get(*buf_idx) {
Some(buf) => accumulate_matches(acc, &buf.buffer.borrow().rope, &prefix),
None => acc,
}
});
Some((offsets, finalize_matches(matches, prefix)))
}
pub fn scratch_buffers(&self) -> Vec<PathBuf> {
let mut scratch = self
.buffers
.iter()
.filter_map(|b| match &b.buffer.borrow().source {
Source::Scratch { path, .. } => Some(path.clone()),
_ => None,
})
.collect::<Vec<_>>();
scratch.sort_unstable();
scratch
}
}
impl<'r> Searchable<'r> for BufferList {
type Output = BTreeMap<usize, Vec<MultiCursor>>;
type Range = ();
fn all_matches<S: SearchTerm>(
&mut self,
_range: Self::Range,
term: S,
) -> Result<(usize, Self::Output), S> {
let (mut current_indexes, matches): (
BTreeMap<usize, usize>,
BTreeMap<usize, Vec<MultiCursor>>,
) = self
.buffers_mut()
.enumerate()
.filter_map(|(buf_idx, b)| {
let (match_idx, matches) = b.all_matches(None, term.clone()).ok()?;
Some(((buf_idx, match_idx), (buf_idx, matches)))
})
.unzip();
let Some((buffer_idx, match_idx)) = current_indexes
.extract_if(self.current.., |_, _| true)
.next()
.or_else(|| current_indexes.pop_first())
else {
return Err(term);
};
self.current = buffer_idx;
Ok((match_idx, matches))
}
fn all_multiline_matches<S: SearchTerm>(
&mut self,
_range: Self::Range,
term: S,
) -> Result<(usize, Self::Output), S> {
let (mut current_indexes, matches): (
BTreeMap<usize, usize>,
BTreeMap<usize, Vec<MultiCursor>>,
) = self
.buffers_mut()
.enumerate()
.filter_map(|(buf_idx, b)| {
let (match_idx, matches) = b.all_multiline_matches(None, term.clone()).ok()?;
Some(((buf_idx, match_idx), (buf_idx, matches)))
})
.unzip();
let Some((buffer_idx, match_idx)) = current_indexes
.extract_if(self.current.., |_, _| true)
.next()
.or_else(|| current_indexes.pop_first())
else {
return Err(term);
};
self.current = buffer_idx;
Ok((match_idx, matches))
}
fn search_autocomplete_matches(&self, prefix: String) -> Vec<String> {
let matches = self.buffers.iter().fold(Trie::default(), |acc, buf| {
accumulate_matches(acc, &buf.buffer.borrow().rope, &prefix)
});
finalize_matches(matches, prefix)
}
fn set_error<S: Into<Cow<'static, str>>>(&mut self, err: S) {
if let Some(buffer) = self.current_mut() {
buffer.set_error(err);
}
}
}
pub struct BufferWidget<'e> {
pub focused: bool,
pub mode: Option<&'e mut EditorMode>,
pub show_help: Option<Help>,
pub show_sub_help: bool,
pub buffer_idx: usize,
pub pane_idx: Option<char>,
}
impl BufferWidget<'_> {
pub const RIGHT_MARGIN: u16 = 5;
}
impl BufferWidget<'_> {
pub fn viewport_height(area: Rect) -> usize {
use ratatui::{
layout::{
Constraint::{Length, Min},
Layout,
},
widgets::Block,
};
let block = Block::bordered();
let [text_area, _] = Layout::horizontal([Min(0), Length(1)]).areas(block.inner(area));
text_area.height.into()
}
}
impl StatefulWidget for BufferWidget<'_> {
type State = BufferContext;
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer, state: &mut BufferContext) {
use crate::editor::{SearchType, SingleBufferRange};
use crate::help::{
CONFIRM_CLOSE, MARK_SET, MULTICURSOR_MARK_SET, PASTE_GROUP, REPLACE_MATCHES,
SELECT_BUFFER, SELECT_INSIDE, SELECT_LINE, SELECT_LINE_BOOKMARKED, SPLIT_PANE,
VERIFY_RELOAD, VERIFY_SAVE, render_help,
};
use crate::prompt::TextField;
use crate::scrollbar::{Scrollbar, ScrollbarState};
use crate::syntax::{Highlighter, Syntax};
use private::SpanDeque;
use ratatui::{
layout::{
Constraint::{Length, Min},
Layout,
},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Clear, Paragraph, Widget},
};
use std::borrow::Cow;
use std::collections::{BTreeMap, VecDeque};
use std::ops::RangeInclusive;
const EDITING: Style = Style::new().add_modifier(Modifier::REVERSED);
const MATCHING: Style = Style::new().fg(Color::Black).bg(Color::LightYellow);
const MISMATCH: Style = Style::new().fg(Color::Black).bg(Color::Red);
const BOOKMARK: Style = Style::new().fg(Color::Black).bg(Color::Cyan);
const HIGHLIGHTED: Style = Style::new().bg(Color::LightYellow).fg(Color::Black);
const HIGHLIGHT_MATCH: Style = underline_color(Color::Blue)
.bg(Color::LightYellow)
.fg(Color::Black);
const CURSOR_COLOR: Style = Style::new().fg(Color::White).bg(Color::Blue);
fn sub_match_ranges(matches: &[MultiCursor]) -> VecDeque<Range<usize>> {
matches.iter().map(|m| m.range.start..m.range.end).collect()
}
struct EditorLine<'s> {
line: Cow<'s, str>,
range: RangeInclusive<usize>, number: usize, }
impl<'s> EditorLine<'s> {
fn iter(rope: &'s ropey::Rope, start_line: usize) -> impl Iterator<Item = Self> {
let mut lines = rope.lines_at(start_line);
let mut line_numbers = start_line..;
let mut line_start_numbers = start_line..rope.len_lines();
let mut line_starts = std::iter::from_fn(move || {
line_start_numbers
.next()
.and_then(|l| rope.try_line_to_char(l).ok())
})
.peekable();
std::iter::from_fn(move || {
Some(EditorLine {
line: Cow::from(lines.next()?),
range: line_starts.next()?
..=line_starts
.peek()
.map(|e| e.saturating_sub(1))
.unwrap_or_else(|| rope.len_chars() + 1),
number: line_numbers.next()?,
})
})
}
fn colorize<H: Highlighter>(
self,
highlighter: &mut H,
current_line: Option<usize>,
) -> ColorizedLine<'s> {
ColorizedLine {
spans: colorize(highlighter, self.line, current_line == Some(self.number)),
range: self.range,
}
}
}
struct ColorizedLine<'s> {
spans: VecDeque<Span<'s>>,
range: RangeInclusive<usize>,
}
impl<'s> ColorizedLine<'s> {
fn widen(mut self) -> Self {
self.spans.push_back(Span::raw(" "));
self
}
fn widen_if(self, f: impl FnOnce(&Self) -> bool) -> Self {
if f(&self) { self.widen() } else { self }
}
fn highlight_matches(
mut self,
matches: &mut VecDeque<Range<usize>>,
apply: impl Fn(Span<'s>) -> Span<'s> + Copy,
) -> Self {
fn highlight_matches<'s>(
spans: &mut VecDeque<Span<'s>>,
line_range: RangeInclusive<usize>,
matches: &mut VecDeque<Range<usize>>,
apply: impl Fn(Span<'s>) -> Span<'s> + Copy,
) {
struct IntRange {
start: usize,
end: usize,
}
impl From<Range<usize>> for IntRange {
#[inline]
fn from(r: Range<usize>) -> Self {
Self {
start: r.start,
end: r.end,
}
}
}
impl From<IntRange> for Range<usize> {
#[inline]
fn from(IntRange { start, end }: IntRange) -> Self {
start..end
}
}
impl IntRange {
#[inline]
fn is_empty(&self) -> bool {
self.start == self.end
}
#[inline]
fn remaining(&self) -> usize {
self.end.saturating_sub(self.start)
}
#[inline]
fn take(&mut self, requested: usize) -> usize {
let to_extract = requested.min(self.remaining());
self.start += to_extract;
to_extract
}
#[inline]
fn take_both(&mut self, other: &mut Self, requested: usize) -> usize {
let to_extract = requested.min(self.remaining().min(other.remaining()));
self.start += to_extract;
other.start += to_extract;
to_extract
}
}
let (line_start, line_end) = line_range.into_inner();
let mut spans = SpanDeque::new(spans);
let mut line_range = IntRange {
start: line_start,
end: line_end,
};
while !line_range.is_empty() {
let Some(match_range) = matches.pop_front() else {
return;
};
let mut match_range = IntRange::from(match_range);
if match_range.end < line_range.start {
continue;
}
if match_range.start < line_range.start {
match_range.start = line_range.start;
}
spans.extract(
line_range.take(match_range.start - line_range.start),
|span| span,
);
spans.extract(
match_range.take_both(&mut line_range, match_range.remaining()),
apply,
);
if !match_range.is_empty() {
matches.push_front(match_range.into());
}
}
}
highlight_matches(&mut self.spans, self.range.clone(), matches, apply);
self
}
fn highlight_selection(
mut self,
selection: (usize, usize),
apply: impl Fn(Span<'s>) -> Span<'s> + Copy,
) -> Self {
fn highlight_selection<'s>(
spans: &mut VecDeque<Span<'s>>,
line_range: RangeInclusive<usize>,
(selection_start, selection_end): (usize, usize),
highlight: impl Fn(Span<'s>) -> Span<'s>,
) {
let (line_start, line_end) = line_range.into_inner();
if selection_end > line_start && selection_start < line_end {
let mut spans = SpanDeque::new(spans);
spans.extract(selection_start.saturating_sub(line_start), |span| span);
spans.extract(selection_end - selection_start.max(line_start), highlight);
}
}
highlight_selection(&mut self.spans, self.range.clone(), selection, apply);
self
}
fn highlight_marks(mut self, marks: &mut BTreeMap<usize, Style>) -> Self {
fn highlight_marks<'s>(
spans: &mut VecDeque<Span<'s>>,
line_range: RangeInclusive<usize>,
marks: &mut BTreeMap<usize, Style>,
) {
let (line_start, line_end) = line_range.into_inner();
let mut spans = SpanDeque::new(spans);
let mut offset = line_start;
for (position, style) in marks.extract_if(offset..=line_end, |_, _| true) {
spans.extract(position - offset, |s| s);
spans.extract(1, |s| s.patch_style(style));
offset = position + 1;
}
}
highlight_marks(&mut self.spans, self.range.clone(), marks);
self
}
}
impl<'s> From<ColorizedLine<'s>> for Line<'s> {
fn from(line: ColorizedLine<'s>) -> Self {
Line::from(Vec::from(line.spans))
}
}
fn colorize<'s, H: Highlighter>(
highlighter: &mut H,
text: Cow<'s, str>,
current_line: bool,
) -> VecDeque<Span<'s>> {
fn trim_string_matches(mut s: String, to_trim: char) -> String {
loop {
match s.pop() {
Some(c) if c == to_trim => { }
Some(c) => {
s.push(c);
break s;
}
None => {
break s;
}
}
}
}
trait FromRange<'s>: Sized + Into<Cow<'s, str>> + AsRef<str> {
fn extract_range(&self, range: std::ops::Range<usize>) -> Self;
fn extract_range_from(&self, range: std::ops::RangeFrom<usize>) -> Self;
}
impl<'s> FromRange<'s> for &'s str {
fn extract_range(&self, range: std::ops::Range<usize>) -> Self {
&self[range]
}
fn extract_range_from(&self, range: std::ops::RangeFrom<usize>) -> Self {
&self[range]
}
}
impl FromRange<'static> for String {
fn extract_range(&self, range: std::ops::Range<usize>) -> Self {
self[range].to_string()
}
fn extract_range_from(&self, range: std::ops::RangeFrom<usize>) -> Self {
self[range].to_string()
}
}
fn colorize<'r, R: FromRange<'r>, H: Highlighter>(
highlighter: &mut H,
text: R,
) -> VecDeque<Span<'r>> {
let mut elements = VecDeque::default();
let mut idx = 0;
for (highlight, range) in highlighter.highlight(text.as_ref()) {
if idx < range.start {
elements.push_back(Span::raw(text.extract_range(idx..range.start)));
}
elements.push_back(Span::styled(
text.extract_range(range.clone()),
Style::from(highlight),
));
idx = range.end;
}
let last = text.extract_range_from(idx..);
if !last.as_ref().is_empty() {
elements.push_back(Span::raw(last));
}
match highlighter.underline() {
None => elements,
Some(underline) => add_underlines(underline(text.as_ref()), elements),
}
}
fn add_underlines<'r>(
underlines: impl Iterator<Item = std::ops::Range<usize>>,
mut input: VecDeque<Span<'r>>,
) -> VecDeque<Span<'r>> {
let mut underlines = underlines.peekable();
if underlines.peek().is_none() {
return input;
}
let mut spans = SpanDeque::new(&mut input);
let mut idx = 0;
for underline in underlines {
spans.extract_bytes(underline.start - idx, |span| span);
spans.extract_bytes(underline.end - underline.start, |span| {
span.patch_style(underline_color(Color::DarkGray))
});
idx = underline.end;
}
drop(spans);
input
}
fn highlight_trailing_whitespace(
mut colorized: VecDeque<Span<'_>>,
) -> VecDeque<Span<'_>> {
fn trim_end(s: &str) -> Result<(&str, &str), &str> {
let trimmed = s.trim_ascii_end();
if trimmed.len() == s.len() {
Err(s)
} else {
Ok((trimmed, &s[trimmed.len()..]))
}
}
if let Some(last) = colorized.back()
&& let Ok((non_ws, ws)) = trim_end(&last.content)
&& !ws.is_empty()
{
let non_ws = Span {
content: Cow::Owned(non_ws.to_string()),
style: last.style,
};
let ws = Span {
content: Cow::Owned(ws.to_string()),
style: Style::default()
.fg(Color::Red)
.add_modifier(Modifier::REVERSED),
};
colorized.pop_back();
if !non_ws.content.is_empty() {
colorized.push_back(non_ws);
}
colorized.push_back(ws);
colorized
} else {
colorized
}
}
if current_line {
match text {
Cow::Borrowed(s) => colorize(highlighter, s.trim_end_matches('\n')),
Cow::Owned(s) => colorize(highlighter, trim_string_matches(s, '\n')),
}
} else {
highlight_trailing_whitespace(match text {
Cow::Borrowed(s) => colorize(highlighter, s.trim_end_matches('\n')),
Cow::Owned(s) => colorize(highlighter, trim_string_matches(s, '\n')),
})
}
}
fn border_title(title: String, active: bool) -> Line<'static> {
if active {
Line::from(vec![
Span::raw("\u{252b}"),
Span::styled(title, Style::default().bold()),
Span::raw("\u{2523}"),
])
} else {
Line::from(vec![
Span::raw("\u{2524}"),
Span::raw(title),
Span::raw("\u{251c}"),
])
}
}
enum FindSyntax<'s, S> {
Plain(&'s S),
Regex,
}
fn render_find_prompt<'t, 's, S: Syntax>(
syntax: FindSyntax<'t, S>,
text_area: Rect,
buf: &mut ratatui::buffer::Buffer,
prompt: &TextField,
highlight: impl FnOnce(VecDeque<Span<'s>>) -> VecDeque<Span<'s>>,
block: impl FnOnce(Block) -> Block,
) {
let [_, dialog_area, _] =
Layout::vertical([Min(0), Length(3), Min(0)]).areas(text_area);
Clear.render(dialog_area, buf);
Paragraph::new(crate::truncate::line_start(
widen_tabs(
Vec::from(match syntax {
FindSyntax::Plain(syntax) => highlight(colorize(
&mut syntax.initialize_find(),
prompt.chars().collect::<String>().into(),
true,
)),
FindSyntax::Regex => highlight(colorize(
&mut crate::syntax::Regex.initialize_find(),
prompt.chars().collect::<String>().into(),
true,
)),
})
.into(),
),
prompt
.cursor_column()
.saturating_sub(dialog_area.width.saturating_sub(2).into()),
))
.block(block(Block::bordered().border_type(BorderType::Rounded)))
.render(dialog_area, buf);
}
fn find_mode_help(prompt: &TextField, type_: SearchType) -> Vec<crate::help::Keybinding> {
use crate::help::{ctrl, keybind, none};
let mut help = if prompt.is_empty() {
vec![none(
&["Tab"],
match type_ {
SearchType::CaseSensitive => "Case-Insensitive Find",
SearchType::CaseInsensitive => "Regex Find",
SearchType::Regex => "Case-Sensitive Find",
},
)]
} else if prompt.can_autocomplete() {
vec![none(&["Tab"], "Autocomplete Word")]
} else {
vec![]
};
help.extend([
ctrl(&["V"], "Paste From Cut Buffer"),
keybind::<crate::key::GotoLine>("Goto Line"),
keybind::<crate::key::Find>(match prompt.is_empty() {
true => "Redo Last Find",
false => "Begin New Find",
}),
none(&["Enter"], "Browse All Matches"),
none(&["Esc"], "Cancel"),
]);
help
}
fn line_count<'b>(
rope: &ropey::Rope,
block: Block<'b>,
cursor: usize,
selection: Option<usize>,
focused: bool,
) -> Block<'b> {
match selection {
Some(selection) => {
let (start, end) = reorder(cursor, selection);
if let Ok(start_line) = rope.try_char_to_line(start)
&& let Ok(end_line) = rope.try_char_to_line(end)
&& let Some(lines) = end_line.checked_sub(start_line)
{
block.title_bottom(
border_title(
match lines {
0 => "1 Line".to_string(),
n => format!("{} Lines", n + 1),
},
focused,
)
.centered(),
)
} else {
block
}
}
None => block,
}
}
fn apply_margins(
mut lines: Vec<Line<'_>>,
top_margin: usize,
bottom_margin: usize,
) -> Vec<Line<'_>> {
lines.splice(
0..0,
std::iter::repeat_n(Line::styled("~", Style::new().blue()), top_margin),
);
lines.extend(std::iter::repeat_n(
Line::styled("~", Style::new().blue()),
bottom_margin,
));
lines
}
if let Some(EditorMode::Open { chooser }) = self.mode {
use crate::files::FileChooser;
FileChooser::default().render(area, buf, chooser);
return;
}
let buffer = state.buffer.borrow();
let rope = &buffer.rope;
let syntax = &buffer.syntax;
let focused = self.focused && self.mode.is_some();
let show_sub_help: fn(
ratatui::layout::Rect,
&mut ratatui::buffer::Buffer,
&[crate::help::Keybinding],
) = if self.show_sub_help {
|text_area, buf, keybindings| {
render_help(text_area, buf, keybindings, |b| {
b.title_bottom(
Line::from(vec![
Span::styled("F1", Style::default().add_modifier(Modifier::REVERSED)),
Span::raw(" to toggle"),
])
.centered(),
)
})
}
} else {
|_, _, _| { }
};
let block = Block::bordered()
.border_type(if focused {
BorderType::Thick
} else {
BorderType::Plain
})
.title_top(border_title(
if buffer.modified() {
format!("{} *", buffer.source.name())
} else {
buffer.source.name().to_string()
},
focused,
));
let block = if focused {
block
} else {
block.border_style(Style::default().dim())
};
let block = match buffer.source {
Source::Local(_) | Source::Tutorial | Source::Test => block,
Source::Scratch { .. } => {
block.title_bottom(border_title("Scratch".to_string(), focused).right_aligned())
}
#[cfg(feature = "ssh")]
Source::Ssh { .. } => {
block.title_bottom(border_title("SSH".to_string(), focused).right_aligned())
}
};
let block = match buffer.endings.name() {
Some(name) => block
.title_bottom(border_title(syntax.to_string(), focused).right_aligned())
.title_bottom(border_title(name.to_string(), focused)),
None => block.title_bottom(border_title(syntax.to_string(), focused).right_aligned()),
};
let block = match buffer.bookmarks.len() {
0 => block,
bookmarks => block.title_top(if focused {
Line::from(vec![
Span::raw("\u{252b}"),
Span::styled(bookmarks.to_string(), BOOKMARK),
Span::raw("\u{2523}"),
])
.right_aligned()
} else {
Line::from(vec![
Span::raw("\u{2524}"),
Span::styled(bookmarks.to_string(), BOOKMARK),
Span::raw("\u{251c}"),
])
.right_aligned()
}),
};
let block = block.title_top(
border_title(
match self.mode {
Some(EditorMode::SelectLine { prompt }) => prompt.to_string(),
_ => match buffer.rope.try_char_to_line(state.cursor) {
Ok(line) => match buffer.rope.try_line_to_char(line) {
Ok(line_start) => {
format!(
"{}:{}",
Thousands(line + 1),
(state.cursor - line_start) + 1
)
}
Err(_) => format!("{}", Thousands(line + 1)),
},
Err(_) => "???".to_string(),
},
},
focused,
)
.right_aligned(),
);
let block = match &self.mode {
Some(EditorMode::SingleBuffer {
cursors:
MultiCursors {
match_idx,
matches,
mode:
MultiCursorMode::Editing
| MultiCursorMode::MarkSet
| MultiCursorMode::Autocomplete { .. },
..
},
..
}) => block.title_bottom(
border_title(
format!("Match {} / {}", *match_idx + 1, matches.len()),
focused,
)
.centered(),
),
Some(EditorMode::AllBuffers {
cursors:
MultiCursors {
match_idx,
matches,
mode:
MultiCursorMode::Editing
| MultiCursorMode::MarkSet
| MultiCursorMode::Autocomplete { .. },
..
},
}) => block.title_bottom(
border_title(
format!(
"Match {} / {}",
matches
.range(0..self.buffer_idx)
.map(|(_, m)| m.len())
.sum::<usize>()
+ *match_idx
+ 1,
matches.values().map(|m| m.len()).sum::<usize>()
),
focused,
)
.centered(),
),
Some(EditorMode::SelectLine { .. }) => {
let mut bookmarks = buffer.bookmarks.range(..=state.cursor).rev().peekable();
if let Some(b) = bookmarks.peek()
&& *b == state.cursor
{
block.title_bottom(
border_title(
format!(
"Bookmark {} / {}",
bookmarks.count(),
buffer.bookmarks.len()
),
focused,
)
.centered(),
)
} else {
block
}
}
Some(EditorMode::SplitPane) => block.border_style(Style::default().blue()),
Some(EditorMode::MarkSet) => {
line_count(rope, block, state.cursor, state.selection, focused)
}
Some(EditorMode::Open { .. }) => block,
_ => line_count(rope, block, state.cursor, state.selection, focused),
};
let [text_area, scrollbar_area] =
Layout::horizontal([Min(0), Length(1)]).areas(block.inner(area));
block.render(area, buf);
let current_line = rope.try_char_to_line(state.cursor).ok();
let viewport_height: usize = text_area.height.into();
let (viewport_line, top_margin): (usize, usize) = current_line
.map(|line| match line.checked_sub(viewport_height / 2) {
Some(start) => (start, 0),
None => (0, viewport_height / 2 - line),
})
.unwrap_or_default();
let bottom_margin = (viewport_line + viewport_height).saturating_sub(rope.len_lines());
let viewport_start = rope.try_line_to_char(viewport_line).unwrap_or(0);
let mut highlighter = syntax.initialize(rope, viewport_line, area.height);
let viewport_size = rope
.try_line_to_char(current_line.unwrap_or(0) + viewport_height)
.unwrap_or(rope.len_chars())
.saturating_sub(viewport_start);
let mut marks: BTreeMap<usize, Style> =
match prev_opening_char(rope, state.cursor, viewport_size) {
Some((opener, start)) => match next_closing_char(rope, state.cursor, viewport_size)
{
Some((closer, end)) => {
if opener == closer {
[(start.saturating_sub(1), MATCHING), (end, MATCHING)].into()
} else {
[(start.saturating_sub(1), MISMATCH), (end, MISMATCH)].into()
}
}
None => [(start.saturating_sub(1), MATCHING)].into(),
},
None => BTreeMap::default(),
};
marks.extend(
buffer
.bookmarks
.iter()
.filter(|p| *p >= viewport_start)
.map(|bookmark| (bookmark, BOOKMARK)),
);
if let Some(EditorMode::SelectLine { .. }) = self.mode
&& let Some(mark) = marks.get_mut(&state.cursor)
{
*mark = HIGHLIGHTED;
}
Paragraph::new(apply_margins(
crate::truncate::lines_start(
match self.mode {
Some(EditorMode::SingleBuffer {
cursors:
MultiCursors {
matches,
match_idx,
highlight: true,
mode:
MultiCursorMode::Editing
| MultiCursorMode::MarkSet
| MultiCursorMode::PasteGroup { .. },
..
},
..
}) => {
let selection_start = matches[*match_idx].range.start;
let selection_end = matches[*match_idx].range.end;
let mut matches = sub_match_ranges(matches);
EditorLine::iter(rope, viewport_line)
.map(|line| {
line.colorize(&mut highlighter, current_line)
.highlight_marks(&mut marks)
.highlight_matches(&mut matches, |span| span.style(HIGHLIGHTED))
.highlight_selection((selection_start, selection_end), |span| {
span.style(HIGHLIGHT_MATCH)
})
.into()
})
.map(|line| widen_tabs(line))
.take(area.height.into())
.collect()
}
Some(EditorMode::SingleBuffer {
cursors:
MultiCursors {
matches,
highlight: false,
mode:
MultiCursorMode::Editing
| MultiCursorMode::MarkSet
| MultiCursorMode::PasteGroup { .. },
..
},
..
}) => {
let (cursors, (mut ranges, selections)): (Vec<_>, (_, VecFiltered<_>)) =
matches
.iter()
.map(|m| (m.cursor, (m.range.clone(), m.selection_range())))
.unzip();
marks.extend(
cursors
.into_iter()
.filter(|c| *c != state.cursor)
.map(|c| (c, CURSOR_COLOR)),
);
let mut selections = selections.into();
EditorLine::iter(rope, viewport_line)
.map(|line| {
line.colorize(&mut highlighter, current_line)
.widen()
.highlight_marks(&mut marks)
.highlight_matches(&mut ranges, |span| {
span.patch_style(underline_color(Color::Blue))
})
.highlight_matches(&mut selections, |span| span.style(EDITING))
.into()
})
.map(|line| widen_tabs(line))
.take(area.height.into())
.collect()
}
Some(EditorMode::AllBuffers {
cursors:
MultiCursors {
matches,
match_idx,
highlight: true,
mode:
MultiCursorMode::Editing
| MultiCursorMode::MarkSet
| MultiCursorMode::PasteGroup { .. },
},
}) if let Some(matches) = matches.get(&self.buffer_idx) => {
let selection_start = matches[*match_idx].range.start;
let selection_end = matches[*match_idx].range.end;
let mut matches = sub_match_ranges(matches);
EditorLine::iter(rope, viewport_line)
.map(|line| {
line.colorize(&mut highlighter, current_line)
.highlight_marks(&mut marks)
.highlight_matches(&mut matches, |span| span.style(HIGHLIGHTED))
.highlight_selection((selection_start, selection_end), |span| {
span.style(HIGHLIGHT_MATCH)
})
.into()
})
.map(|line| widen_tabs(line))
.take(area.height.into())
.collect()
}
Some(EditorMode::AllBuffers {
cursors:
MultiCursors {
matches,
match_idx,
highlight: false,
mode:
MultiCursorMode::Editing
| MultiCursorMode::MarkSet
| MultiCursorMode::PasteGroup { .. },
},
}) if let Some(matches) = matches.get(&self.buffer_idx) => {
let (cursors, (mut ranges, selections)): (Vec<_>, (_, VecFiltered<_>)) =
matches
.iter()
.map(|m| (m.cursor, (m.range.clone(), m.selection_range())))
.unzip();
marks.extend(
cursors
.into_iter()
.filter(|c| *c != state.cursor)
.map(|c| (c, CURSOR_COLOR)),
);
let mut selections = selections.into();
EditorLine::iter(rope, viewport_line)
.map(|line| {
line.colorize(&mut highlighter, current_line)
.widen()
.highlight_marks(&mut marks)
.highlight_matches(&mut ranges, |span| {
span.patch_style(underline_color(Color::Blue))
})
.highlight_matches(&mut selections, |span| span.style(EDITING))
.into()
})
.map(|line| widen_tabs(line))
.take(area.height.into())
.collect()
}
Some(EditorMode::SingleBuffer {
cursors:
MultiCursors {
matches,
mode:
MultiCursorMode::Autocomplete {
offsets,
completions,
index,
},
..
},
..
}) => {
let (cursors, mut ranges): (Vec<_>, _) =
matches.iter().map(|m| (m.cursor, m.range.clone())).unzip();
let completion_chars = completions[*index].chars().count();
let mut replacements = matches
.iter()
.zip(offsets)
.map(|(m, o)| m.range.start + *o..m.range.start + *o + completion_chars)
.collect();
marks.extend(
cursors
.into_iter()
.filter(|c| *c != state.cursor)
.map(|c| (c, CURSOR_COLOR)),
);
EditorLine::iter(rope, viewport_line)
.map(|line| {
line.colorize(&mut highlighter, current_line)
.widen()
.highlight_marks(&mut marks)
.highlight_matches(&mut ranges, |span| {
span.patch_style(underline_color(Color::Blue))
})
.highlight_matches(&mut replacements, |span| {
span.patch_style(underline_color(Color::Red))
})
.into()
})
.map(|line| widen_tabs(line))
.take(area.height.into())
.collect()
}
Some(EditorMode::AllBuffers {
cursors:
MultiCursors {
matches,
mode:
MultiCursorMode::Autocomplete {
offsets,
completions,
index,
},
..
},
}) if let Some(matches) = matches.get(&self.buffer_idx)
&& let Some(offsets) = offsets.get(&self.buffer_idx) =>
{
let (cursors, mut ranges): (Vec<_>, _) =
matches.iter().map(|m| (m.cursor, m.range.clone())).unzip();
let completion_chars = completions[*index].chars().count();
let mut replacements = matches
.iter()
.zip(offsets)
.map(|(m, o)| m.range.start + *o..m.range.start + *o + completion_chars)
.collect();
marks.extend(
cursors
.into_iter()
.filter(|c| *c != state.cursor)
.map(|c| (c, CURSOR_COLOR)),
);
EditorLine::iter(rope, viewport_line)
.map(|line| {
line.colorize(&mut highlighter, current_line)
.widen()
.highlight_marks(&mut marks)
.highlight_matches(&mut ranges, |span| {
span.patch_style(underline_color(Color::Blue))
})
.highlight_matches(&mut replacements, |span| {
span.patch_style(underline_color(Color::Red))
})
.into()
})
.map(|line| widen_tabs(line))
.take(area.height.into())
.collect()
}
Some(EditorMode::Autocomplete {
offset,
completions,
index,
}) => {
let completion_start = *offset;
let completion_end = *offset + completions[*index].chars().count();
EditorLine::iter(rope, viewport_line)
.map(|line| {
line.colorize(&mut highlighter, current_line)
.highlight_selection(
(completion_start, completion_end),
|span| span.patch_style(underline_color(Color::Red)),
)
.widen()
.highlight_marks(&mut marks)
.into()
})
.map(|line| widen_tabs(line))
.take(area.height.into())
.collect()
}
_ => {
match state.selection {
None => EditorLine::iter(rope, viewport_line)
.map(|line| {
line.colorize(&mut highlighter, current_line)
.widen()
.highlight_marks(&mut marks)
.into()
})
.map(|line| widen_tabs(line))
.take(area.height.into())
.collect(),
Some(selection) => {
let (selection_start, selection_end) =
reorder(state.cursor, selection);
EditorLine::iter(rope, viewport_line)
.map(|line| {
line.colorize(&mut highlighter, current_line)
.widen_if(|line| *line.range.end() >= selection_end)
.highlight_marks(&mut marks)
.highlight_selection(
(selection_start, selection_end),
|span| span.style(EDITING),
)
.into()
})
.map(|line| widen_tabs(line))
.take(area.height.into())
.collect()
}
}
}
},
state
.cursor_position()
.map(|(_, col)| {
col.saturating_sub(
text_area.width.saturating_sub(Self::RIGHT_MARGIN).into(),
)
})
.unwrap_or(0),
),
top_margin,
bottom_margin,
))
.render(text_area, buf);
Scrollbar.render(
scrollbar_area,
buf,
&mut ScrollbarState::new(buffer.total_lines() + viewport_height.saturating_sub(1))
.viewport_content_length(viewport_height)
.position(current_line.unwrap_or(0)),
);
match &self.mode {
None | Some(EditorMode::Editing) | Some(EditorMode::Autocomplete { .. }) => {
if let Some(Help {
find,
has_bookmarks,
cursor_pos,
has_selection,
multiple_buffers,
multiple_panes,
}) = self.show_help
{
use crate::help::{
EDITING_0, EDITING_2, EDITING_3, F10, SWITCH_PANE, ctrl, keybind, none,
};
use crate::key::{
GotoLine, GotoPair, SelectInside, UpdateLines, WidenSelection,
};
let mut help = Vec::with_capacity(16);
help.extend(EDITING_0);
help.push(keybind::<GotoLine>(if has_bookmarks {
"Goto Line / Bookmark"
} else {
"Goto Line"
}));
help.push(find.into());
help.extend(
has_selection.then_some(keybind::<UpdateLines>("Update Selected Lines")),
);
help.extend(
matches!(cursor_pos, CursorPos::AtParen)
.then_some(keybind::<GotoPair>("Goto Matching Pair")),
);
help.push(keybind::<SelectInside>("Select Inside Pair"));
help.push(keybind::<WidenSelection>(
if matches!(cursor_pos, CursorPos::InWord) {
"Select Word"
} else {
"Widen Selection"
},
));
help.push(F10);
help.extend(EDITING_2);
help.extend(has_selection.then_some(ctrl(&["Home"], "Start of Selection")));
help.extend(has_selection.then_some(ctrl(&["End"], "End of Selection")));
help.push(none(
&["Tab"],
if matches!(cursor_pos, CursorPos::AfterWord) {
"Autocomplete Word"
} else {
"Indent Text"
},
));
help.extend(EDITING_3);
help.extend(multiple_panes.then_some(SWITCH_PANE));
help.extend(
multiple_buffers.then_some(ctrl(&["]", "PgUp", "PgDn"], "Switch Buffer")),
);
crate::help::render_main_help(text_area, buf, &help, |b| {
b.title_top("Keybindings").title_bottom(
Line::from(vec![
Span::styled(
"F1",
Style::default().add_modifier(Modifier::REVERSED),
),
Span::raw(" to toggle"),
])
.centered(),
)
});
}
}
Some(EditorMode::MarkSet) => {
show_sub_help(text_area, buf, MARK_SET);
}
Some(EditorMode::ConfirmClose { .. }) => {
show_sub_help(text_area, buf, CONFIRM_CLOSE);
render_message(
text_area,
buf,
BufferMessage::Error("Unsaved changes. Really quit?".into()),
);
}
Some(EditorMode::VerifySave) => {
show_sub_help(text_area, buf, VERIFY_SAVE);
render_message(
text_area,
buf,
BufferMessage::Error("Buffer changed on disk. Really save?".into()),
);
}
Some(EditorMode::SplitPane) => {
show_sub_help(text_area, buf, SPLIT_PANE);
}
Some(EditorMode::VerifyReload) => {
show_sub_help(text_area, buf, VERIFY_RELOAD);
render_message(
text_area,
buf,
BufferMessage::Error("Buffer not yet saved. Really reload?".into()),
);
}
Some(EditorMode::SelectInside) => {
show_sub_help(text_area, buf, SELECT_INSIDE);
}
Some(EditorMode::SelectLine { .. }) => {
show_sub_help(
text_area,
buf,
if buffer.has_bookmarks() {
SELECT_LINE_BOOKMARKED
} else {
SELECT_LINE
},
);
}
Some(
EditorMode::Search {
search:
Search {
prompt,
type_,
mode: SearchMode::Editing,
},
..
}
| EditorMode::SearchAll {
search:
Search {
prompt,
type_,
mode: SearchMode::Editing,
},
},
) => {
show_sub_help(text_area, buf, &find_mode_help(prompt, *type_));
render_find_prompt(
match type_ {
SearchType::CaseSensitive | SearchType::CaseInsensitive => {
FindSyntax::Plain(syntax)
}
SearchType::Regex => FindSyntax::Regex,
},
text_area,
buf,
prompt,
|s| s,
|b| match state
.message
.take_if(|m| matches!(m, BufferMessage::Error(_)))
{
Some(BufferMessage::Error(err)) => b
.title_top(
Line::from(
if matches!(&self.mode, Some(EditorMode::SearchAll { .. })) {
"Find All"
} else {
"Find"
},
)
.left_aligned(),
)
.title_top(Line::from(type_.to_string()).right_aligned())
.title_bottom(Line::from(err.to_string()).centered())
.border_style(Style::default().fg(Color::Red)),
_ => b
.title_top(
Line::from(
if matches!(&self.mode, Some(EditorMode::SearchAll { .. })) {
"Find All"
} else {
"Find"
},
)
.left_aligned(),
)
.title_top(Line::from(type_.to_string()).right_aligned()),
},
);
}
Some(
EditorMode::Search {
search:
Search {
prompt,
type_,
mode:
SearchMode::Autocomplete {
offset,
completions,
index,
},
},
..
}
| EditorMode::SearchAll {
search:
Search {
prompt,
type_,
mode:
SearchMode::Autocomplete {
offset,
completions,
index,
},
},
},
) => {
show_sub_help(text_area, buf, &find_mode_help(prompt, *type_));
render_find_prompt(
match type_ {
SearchType::CaseSensitive | SearchType::CaseInsensitive => {
FindSyntax::Plain(syntax)
}
SearchType::Regex => FindSyntax::Regex,
},
text_area,
buf,
prompt,
|mut spans| {
let mut highlighted = SpanDeque::new(&mut spans);
highlighted.extract(*offset, |span| span);
highlighted.extract(completions[*index].chars().count(), |span| {
span.patch_style(underline_color(Color::Red))
});
drop(highlighted);
spans
},
|b| match state
.message
.take_if(|m| matches!(m, BufferMessage::Error(_)))
{
Some(BufferMessage::Error(err)) => b
.title_top(
Line::from(
if matches!(&self.mode, Some(EditorMode::SearchAll { .. })) {
"Find All"
} else {
"Find"
},
)
.left_aligned(),
)
.title_top(Line::from(type_.to_string()).right_aligned())
.title_bottom(Line::from(err.to_string()).centered())
.border_style(Style::default().fg(Color::Red)),
_ => b
.title_top(
Line::from(
if matches!(&self.mode, Some(EditorMode::SearchAll { .. })) {
"Find All"
} else {
"Find"
},
)
.left_aligned(),
)
.title_top(Line::from(type_.to_string()).right_aligned()),
},
);
}
Some(
EditorMode::SingleBuffer {
cursors:
MultiCursors {
mode: MultiCursorMode::Editing | MultiCursorMode::Autocomplete { .. },
..
},
range: SingleBufferRange::WholeFile | SingleBufferRange::Lines(_),
}
| EditorMode::AllBuffers {
cursors:
MultiCursors {
mode: MultiCursorMode::Editing | MultiCursorMode::Autocomplete { .. },
..
},
},
) => {
show_sub_help(text_area, buf, REPLACE_MATCHES);
}
Some(EditorMode::SingleBuffer {
cursors:
MultiCursors {
mode: MultiCursorMode::Editing | MultiCursorMode::Autocomplete { .. },
..
},
range: SingleBufferRange::UpdateLines,
}) => {
use crate::help::keybind;
use crate::key;
let mut help = Vec::from(REPLACE_MATCHES);
help.insert(3, keybind::<key::UpdateLines>("To Selection"));
show_sub_help(text_area, buf, &help);
}
Some(
EditorMode::SingleBuffer {
cursors:
MultiCursors {
mode: MultiCursorMode::MarkSet,
..
},
..
}
| EditorMode::AllBuffers {
cursors:
MultiCursors {
mode: MultiCursorMode::MarkSet,
..
},
},
) => {
show_sub_help(text_area, buf, MULTICURSOR_MARK_SET);
}
Some(
EditorMode::SingleBuffer {
cursors:
MultiCursors {
mode: MultiCursorMode::PasteGroup { total, .. },
..
},
..
}
| EditorMode::AllBuffers {
cursors:
MultiCursors {
mode: MultiCursorMode::PasteGroup { total, .. },
..
},
},
) => {
show_sub_help(
text_area,
buf,
PASTE_GROUP
.iter()
.copied()
.take(*total + 1)
.collect::<Vec<_>>()
.as_slice(),
);
}
Some(EditorMode::Open { .. }) => { }
Some(EditorMode::SelectBuffer { buffer_list, index }) => {
use ratatui::{
layout::Constraint,
widgets::{Cell, Row},
};
fn shortcut_letters() -> impl Iterator<Item = char> {
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']
.into_iter()
.chain('A'..='Z')
.chain(std::iter::repeat(' '))
}
let selected_buf = state.id();
let mut max_name = 0; let mut max_bookmarks = 0; let rows = shortcut_letters()
.zip(
buffer_list
.iter()
.map(|bid| {
(
bid.to_string(),
bid.bookmarks,
bid.modified,
bid.buffer == selected_buf,
)
})
.inspect(|(s, bookmarks, _, _)| {
use unicode_width::UnicodeWidthStr;
max_name = max_name.max(s.width());
max_bookmarks = max_bookmarks.max(*bookmarks)
}),
)
.map(|(c, (s, bookmarks, modified, selected))| {
Row::new([
Cell::new(c.to_string()).style(Style::new().reversed()),
Cell::new(if modified { "*" } else { " " }),
Cell::new(s).style(if selected {
Style::new().underlined()
} else {
Style::new()
}),
if bookmarks == 0 {
Cell::new("")
} else {
Cell::new(bookmarks.to_string()).style(BOOKMARK)
},
])
})
.collect::<Vec<_>>();
let table_rows = rows.len();
let column_widths = [
1,
1,
max_name as u16,
max_bookmarks.checked_ilog10().map(|i| i + 1).unwrap_or(0) as u16,
];
let table_width = column_widths.iter().sum::<u16>() + 3;
let table = ratatui::widgets::Table::new(rows, column_widths.map(Constraint::Max));
let mut state = ratatui::widgets::TableState::default().with_selected(Some(*index));
render_list(
text_area,
buf,
table,
&mut state,
table_rows as u16,
table_width,
);
show_sub_help(text_area, buf, SELECT_BUFFER);
}
}
if let Some(index) = self.pane_idx {
render_pane_index(text_area, buf, index);
}
if let Some(message) = state.message.take() {
render_message(text_area, buf, message);
}
}
}
pub fn widen_tabs<'l>(mut input: ratatui::prelude::Line<'l>) -> ratatui::prelude::Line<'l> {
fn tabs_to_spaces(s: &mut Cow<'_, str>) {
if s.as_ref().contains('\t') {
*s = Cow::Owned(s.as_ref().replace('\t', &TAB_SUBSTITUTION));
}
}
input
.spans
.iter_mut()
.for_each(|s| tabs_to_spaces(&mut s.content));
input
}
pub fn render_message(area: Rect, buf: &mut ratatui::buffer::Buffer, message: BufferMessage) {
use ratatui::{
layout::{
Constraint::{Length, Min},
Layout,
},
style::{Color, Style},
widgets::{Block, BorderType, Clear, Paragraph, Widget},
};
use unicode_width::UnicodeWidthStr;
let width = message.as_str().width().try_into().unwrap_or(u16::MAX);
let [_, dialog_area, _] = Layout::horizontal([Min(0), Length(width + 2), Min(0)]).areas(area);
let [_, dialog_area, _] = Layout::vertical([Min(0), Length(3), Min(0)]).areas(dialog_area);
Clear.render(dialog_area, buf);
Paragraph::new(message.as_str())
.style(match message {
BufferMessage::Notice(_) => Style::default(),
BufferMessage::Error(_) => Style::default().fg(Color::Red),
})
.block(Block::bordered().border_type(BorderType::Rounded))
.render(dialog_area, buf);
}
fn render_pane_index(area: Rect, buf: &mut ratatui::buffer::Buffer, index: char) {
use ratatui::{
layout::{
Constraint::{Length, Min},
Layout,
},
style::Style,
text::Text,
widgets::{Block, BorderType, Clear, Paragraph, Widget},
};
let [_, dialog_area, _] = Layout::horizontal([Min(0), Length(5), Min(0)]).areas(area);
let [_, dialog_area, _] = Layout::vertical([Min(0), Length(3), Min(0)]).areas(dialog_area);
Clear.render(dialog_area, buf);
Paragraph::new(Text::styled(
format!(" {index} "),
Style::default().reversed(),
))
.block(
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::default().blue()),
)
.render(dialog_area, buf);
}
pub fn render_list(
area: Rect,
buf: &mut ratatui::buffer::Buffer,
table: ratatui::widgets::Table,
state: &mut ratatui::widgets::TableState,
rows: u16,
width: u16,
) {
use ratatui::{
layout::{
Constraint::{Length, Min},
Layout,
},
style::Style,
text::Line,
widgets::{Block, BorderType, Clear, Widget},
};
let [_, dialog_area, _] = Layout::horizontal([Min(0), Length(width + 2), Min(0)]).areas(area);
let [_, dialog_area, _] =
Layout::vertical([Min(0), Length(rows + 2), Min(0)]).areas(dialog_area);
Clear.render(dialog_area, buf);
StatefulWidget::render(
table.row_highlight_style(Style::new().reversed()).block(
Block::bordered()
.border_type(BorderType::Rounded)
.title_top(Line::from("Buffer").centered()),
),
dialog_area,
buf,
state,
);
}
pub enum EditorCutBuffer {
Single(CutBuffer),
Multiple(Vec<CutBuffer>), }
impl EditorCutBuffer {
pub fn paste_and_rotate(&mut self) -> &CutBuffer {
match self {
Self::Single(b) => b,
Self::Multiple(v) => {
v.rotate_left(1);
v.last().unwrap()
}
}
}
pub fn primary_mut(&mut self) -> Option<&mut CutBuffer> {
match self {
Self::Single(b) => Some(b),
Self::Multiple(v) => v.first_mut(),
}
}
}
#[derive(Default)]
pub struct CutBuffer {
data: String,
chars_len: usize,
bookmarks: Vec<usize>, }
impl CutBuffer {
pub fn new<B>(rope: ropey::RopeSlice<'_>, bookmarks: B) -> Self
where
B: IntoIterator<Item = usize>,
{
Self {
chars_len: rope.len_chars(),
data: rope.chunks().collect(),
bookmarks: bookmarks.into_iter().collect(),
}
}
pub fn as_str(&self) -> &str {
self.data.as_str()
}
pub fn multi_line(&self) -> bool {
self.data.contains('\n')
}
}
impl From<String> for CutBuffer {
fn from(data: String) -> Self {
Self {
chars_len: data.chars().count(),
data,
bookmarks: vec![],
}
}
}
fn search_area<'r>(
rope: &'r ropey::Rope,
range: Option<&SelectionRange>,
) -> impl Iterator<Item = (Cow<'r, str>, usize)> {
fn no_nl(s: Cow<'_, str>) -> Option<Cow<'_, str>> {
(!s.is_empty()).then(|| match s {
Cow::Borrowed(s) => Cow::Borrowed(s.trim_end_matches('\n')),
Cow::Owned(mut s) => {
while s.ends_with('\n') {
let _ = s.pop();
}
Cow::Owned(s)
}
})
}
match range {
None => Box::new(rope.lines().enumerate().filter_map(|(line_num, line)| {
Some((no_nl(line.into())?, rope.try_line_to_byte(line_num).ok()?))
})) as Box<dyn Iterator<Item = (Cow<'_, str>, usize)>>,
Some(SelectionRange { start, lines }) => Box::new(
(*start..)
.zip(rope.lines_at(*start))
.take(lines.get())
.filter_map(|(line_num, line)| {
Some((no_nl(line.into())?, rope.try_line_to_byte(line_num).ok()?))
}),
),
}
}
struct BufferState {
rope: ropey::Rope,
bookmarks: private::Bookmarks,
}
#[derive(Clone)]
struct BufferContextState {
cursor: usize,
cursor_column: usize,
selection: Option<usize>,
}
#[derive(Clone)]
pub enum BufferMessage {
Notice(Cow<'static, str>),
Error(Cow<'static, str>),
}
impl BufferMessage {
fn as_str(&self) -> &str {
match self {
Self::Notice(s) | Self::Error(s) => s.as_ref(),
}
}
}
fn patch_rope(
source: &mut ropey::Rope,
target: String,
cursor: &mut usize,
selection: &mut Option<usize>,
mut alt: Secondary<'_, '_>,
) {
use imara_diff::{Algorithm::Histogram, Diff, Hunk, InternedInput};
use ropey::Rope;
use std::ops::Range;
#[must_use]
fn remove_lines(
rope: &mut Rope,
alt: &mut Secondary<'_, '_>,
lines: Range<u32>,
) -> Range<usize> {
let removed =
rope.line_to_char(lines.start as usize)..rope.line_to_char(lines.end as usize);
rope.remove(alt.remove(removed.clone()));
removed
}
fn get_lines(rope: &Rope, lines: Range<u32>) -> (String, usize) {
if lines.end > lines.start {
rope.lines_at(lines.start as usize)
.take((lines.end - lines.start) as usize)
.fold((String::default(), 0), |(mut s, chars), line| {
s.extend(line.chunks());
(s, chars + line.len_chars())
})
} else {
(String::default(), 0)
}
}
fn decrement_pos(pos: &mut usize, removed: &Range<usize>) {
if *pos > removed.end {
*pos -= removed.end - removed.start;
} else if *pos > removed.start {
*pos = removed.start;
}
}
fn increment_pos(pos: &mut usize, inserted_pos: usize, inserted_chars: usize) {
if *pos >= inserted_pos {
*pos += inserted_chars;
}
}
let source_str =
source
.chunks()
.fold(String::with_capacity(source.len_bytes()), |mut acc, s| {
acc.push_str(s);
acc
});
let hunks = Diff::compute(Histogram, &InternedInput::new(source_str.as_str(), &target))
.hunks()
.collect::<Vec<_>>();
let target = Rope::from(target);
for Hunk { before, after } in hunks.into_iter().rev() {
let removed = remove_lines(source, &mut alt, before.clone());
decrement_pos(cursor, &removed);
if let Some(selection) = selection.as_mut() {
decrement_pos(selection, &removed);
}
alt.update(|a| decrement_pos(a, &removed));
let (to_insert, inserted_chars) = get_lines(&target, after);
if !to_insert.is_empty() {
let inserted_pos = source.line_to_char(before.start as usize);
increment_pos(cursor, inserted_pos, inserted_chars);
if let Some(selection) = selection.as_mut() {
increment_pos(selection, inserted_pos, inserted_chars);
}
alt.update(|a| increment_pos(a, inserted_pos, inserted_chars));
source.insert(inserted_pos, &to_insert);
}
}
}
pub enum BufferDeleted {
BuffersRemain,
NoBuffers,
}
pub trait MultiBuffer<'a> {
type Matches;
type Offsets;
type Alt;
fn multi_insert_char(&mut self, alt: Self::Alt, matches: &mut Self::Matches, c: char);
fn multi_insert_string(&mut self, alt: Self::Alt, matches: &mut Self::Matches, s: &str);
fn multi_backspace(&mut self, alt: Self::Alt, matches: &mut Self::Matches);
fn multi_delete(&mut self, alt: Self::Alt, matches: &mut Self::Matches);
fn delete_buffer(
&mut self,
matches: &mut Self::Matches,
match_idx: &mut usize,
) -> BufferDeleted;
fn multi_select_inside(&mut self, matches: &mut Self::Matches, selected: usize);
fn multi_cursor_back(&mut self, matches: &mut Self::Matches, selecting: bool);
fn multi_cursor_forward(&mut self, matches: &mut Self::Matches, selecting: bool);
fn multi_cursor_home(&mut self, matches: &mut Self::Matches, selecting: bool);
fn multi_cursor_end(&mut self, matches: &mut Self::Matches, selecting: bool);
fn paste_group_count(matches: &Self::Matches) -> Option<NonZero<usize>>;
fn multi_paste(
&mut self,
alt: Self::Alt,
matches: &mut Self::Matches,
cut: &mut EditorCutBuffer,
);
fn multi_cursor_copy(&mut self, matches: &mut Self::Matches) -> Vec<CutBuffer>;
fn multi_cursor_cut(&mut self, alt: Self::Alt, matches: &mut Self::Matches) -> Vec<CutBuffer>;
fn multi_insert_group(&mut self, alt: Self::Alt, matches: &mut Self::Matches, group_num: usize);
fn previous_match(&mut self, matches: &mut Self::Matches, match_idx: &mut usize);
fn next_match(&mut self, matches: &mut Self::Matches, match_idx: &mut usize);
fn multi_cursor_widen(&mut self, matches: &mut Self::Matches);
#[must_use]
fn toggle_bookmarks(&mut self, matches: &mut Self::Matches) -> ToggledBookmarks;
fn multi_autocomplete_matches(
&self,
matches: &mut Self::Matches,
) -> Option<(Self::Offsets, Vec<String>)>;
fn multi_autocomplete(
&mut self,
alt: Self::Alt,
matches: &mut Self::Matches,
offsets: &Self::Offsets,
original: &str,
replacement: &str,
);
fn perform_undo(&mut self, alt: Self::Alt, matches: &mut Self::Matches);
fn perform_redo(&mut self, alt: Self::Alt, matches: &mut Self::Matches);
fn set_buffer_message(&mut self, message: BufferMessage);
fn set_message<S: Into<Cow<'static, str>>>(&mut self, msg: S);
fn set_error<S: Into<Cow<'static, str>>>(&mut self, msg: S);
}
pub trait Searchable<'r> {
type Output;
type Range;
fn all_matches<S: SearchTerm>(
&mut self,
range: Self::Range,
term: S,
) -> Result<(usize, Self::Output), S>;
fn all_multiline_matches<S: SearchTerm>(
&mut self,
range: Self::Range,
term: S,
) -> Result<(usize, Self::Output), S>;
fn search_autocomplete_matches(&self, prefix: String) -> Vec<String>;
fn set_error<S: Into<Cow<'static, str>>>(&mut self, err: S);
}
fn try_select_inside(
rope: &ropey::Rope,
cursor: &mut usize,
selection: &mut Option<usize>,
mut in_bounds: impl FnMut(usize) -> bool,
) -> Result<(), ()> {
let (start, end) = match selection {
Some(selection) => reorder(*cursor, *selection),
None => (*cursor, *cursor),
};
let start = start.checked_sub(1).ok_or(())?;
match match (rope.get_char(start), rope.get_char(end)) {
(Some('('), Some(')'))
| (Some('['), Some(']'))
| (Some('{'), Some('}'))
| (Some('<'), Some('>'))
| (Some('"'), Some('"'))
| (Some('\''), Some('\'')) => Some((start, end + 1)),
(_, Some(')')) => {
prev_pairing_char(rope, start).and_then(|(c, start)| (c == '(').then_some((start, end)))
}
(Some('('), _) => {
next_pairing_char(rope, end).and_then(|(c, end)| (c == ')').then_some((start + 1, end)))
}
(_, Some(']')) => {
prev_pairing_char(rope, start).and_then(|(c, start)| (c == '[').then_some((start, end)))
}
(Some('['), _) => {
next_pairing_char(rope, end).and_then(|(c, end)| (c == ']').then_some((start + 1, end)))
}
(_, Some('}')) => {
prev_pairing_char(rope, start).and_then(|(c, start)| (c == '{').then_some((start, end)))
}
(Some('{'), _) => {
next_pairing_char(rope, end).and_then(|(c, end)| (c == '}').then_some((start + 1, end)))
}
(_, Some('>')) => {
prev_pairing_char(rope, start).and_then(|(c, start)| (c == '<').then_some((start, end)))
}
(Some('<'), _) => {
next_pairing_char(rope, end).and_then(|(c, end)| (c == '>').then_some((start + 1, end)))
}
(_, Some('"')) => {
prev_pairing_char(rope, start).and_then(|(c, start)| (c == '"').then_some((start, end)))
}
(Some('"'), _) => {
next_pairing_char(rope, end).and_then(|(c, end)| (c == '"').then_some((start + 1, end)))
}
(_, Some('\'')) => prev_pairing_char(rope, start)
.and_then(|(c, start)| (c == '\'').then_some((start, end))),
(Some('\''), _) => next_pairing_char(rope, end)
.and_then(|(c, end)| (c == '\'').then_some((start + 1, end))),
_ => match (
prev_pairing_char(rope, start),
next_pairing_char(rope, end + 1),
) {
(Some(('(', start)), Some((')', end)))
| (Some(('[', start)), Some((']', end)))
| (Some(('{', start)), Some(('}', end)))
| (Some(('<', start)), Some(('>', end)))
| (Some(('"', start)), Some(('"', end)))
| (Some(('\'', start)), Some(('\'', end))) => Some((start, end)),
_ => None,
},
} {
Some((start, end)) => {
if in_bounds(start) && in_bounds(end) {
*cursor = end;
*selection = Some(start);
Ok(())
} else {
Err(())
}
}
None => Err(()),
}
}
fn accumulate_matches(
mut acc: Trie<String, u64>,
rope: &ropey::Rope,
prefix: &str,
) -> Trie<String, u64> {
for line in rope.lines() {
let line = Cow::from(line);
for word in line.split(|c| !is_word_part(c)).filter(|s| !s.is_empty()) {
if word.starts_with(prefix) && word != prefix {
acc.map_with_default(word.to_string(), |c| *c += 1, 1);
}
}
}
acc
}
fn finalize_matches(counts: Trie<String, u64>, prefix: String) -> Vec<String> {
use radix_trie::TrieCommon;
let mut counts = counts
.iter()
.map(|(s, c)| (s.to_string(), c))
.collect::<Vec<_>>();
counts.sort_unstable_by(|(s1, c1), (s2, c2)| c1.cmp(c2).then(s1.cmp(s2).reverse()));
std::iter::once(prefix)
.chain(counts.into_iter().map(|(s, _)| s).rev())
.collect()
}
fn autocomplete_matches(rope: &ropey::Rope, prefix: String) -> Vec<String> {
let matches = accumulate_matches(Trie::default(), rope, &prefix);
finalize_matches(matches, prefix)
}
struct Thousands(usize);
impl std::fmt::Display for Thousands {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
fn write_separated(u: usize, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match u {
u @ 0..1000 => u.fmt(f),
u => {
write_separated(u / 1000, f)?;
write!(f, "_{:03}", u % 1000)
}
}
}
match self.0 {
u @ 0..10000 => u.fmt(f),
u => write_separated(u, f),
}
}
}
enum Toggle {
Inserted, Removed, }
#[derive(Default)]
struct VecFiltered<T>(Vec<T>);
impl<T> Extend<Option<T>> for VecFiltered<T> {
fn extend<I: IntoIterator<Item = Option<T>>>(&mut self, iter: I) {
self.0.extend(iter.into_iter().flatten());
}
}
impl<T> From<VecFiltered<T>> for std::collections::VecDeque<T> {
fn from(v: VecFiltered<T>) -> Self {
v.0.into()
}
}
#[cfg(feature = "underline-color")]
const fn underline_color(color: ratatui::style::Color) -> ratatui::style::Style {
ratatui::style::Style::new()
.underlined()
.underline_color(color)
}
#[cfg(not(feature = "underline-color"))]
const fn underline_color(_color: ratatui::style::Color) -> ratatui::style::Style {
ratatui::style::Style::new().underlined()
}
#[inline]
pub fn is_word(c: char) -> bool {
c == '_' || c.is_alphanumeric()
}
#[inline]
fn is_word_part(c: char) -> bool {
is_word(c) || is_grapheme_part(c)
}
#[inline]
pub fn is_grapheme_part(c: char) -> bool {
use unicode_width::UnicodeWidthChar;
c.width() == Some(0)
}
fn reorder<T: Ord>(x: T, y: T) -> (T, T) {
if x <= y { (x, y) } else { (y, x) }
}