use std::cmp::Ordering; use std::io::{self, stdout, BufWriter, Stdout}; use std::ops::{Deref, DerefMut, RangeInclusive}; use regex::Regex; use crossterm::{ cursor::MoveTo, style::{Print, PrintStyledContent, Stylize}, terminal::ScrollUp, QueueableCommand, }; use modalkit::editing::{ action::{EditInfo, EditResult, Editable, EditorAction, Jumpable, UIResult}, application::ApplicationInfo, buffer::{CursorGroupId, EditBuffer}, context::EditContext, cursor::Cursor, history::{HistoryList, ScrollbackState}, rope::{CharOff, EditRope}, store::Store, }; use modalkit::prelude::*; pub struct EditorContext { pub stdout: BufWriter, pub top: u16, } impl Default for EditorContext { fn default() -> Self { let stdout = BufWriter::new(stdout()); let top = 0; EditorContext { stdout, top } } } pub struct Editor where I: ApplicationInfo, { buffer: EditBuffer, scrollback: ScrollbackState, viewctx: ViewportContext, gid: CursorGroupId, } impl Editor where I: ApplicationInfo, { pub fn new(id: I::ContentId) -> Self { let mut buffer = EditBuffer::new(id); let mut viewctx = ViewportContext::default(); viewctx.set_wrap(true); let gid = buffer.create_group(); Editor { buffer, scrollback: ScrollbackState::Pending, viewctx, gid, } } pub fn resize(&mut self, width: u16, height: u16) { self.viewctx.dimensions = (width.into(), height.into()); } pub fn is_blank(&self) -> bool { self.buffer.is_blank() } fn _highlight_ranges( &self, line: usize, start: usize, end: usize, ) -> Vec> { let hinfo = self.buffer.selection_intervals(self.gid); let mut ranges = vec![]; for selection in hinfo.query_point(line) { let (sb, se, shape) = &selection.value; let maxcol = end.saturating_sub(1); let range = start..end; match shape { TargetShape::LineWise => { ranges.push(start..=maxcol); break; }, TargetShape::CharWise => { let x1 = if line == sb.y { sb.x.max(start) } else { start }; let x2 = if line == se.y { se.x.min(maxcol) } else { maxcol }; if range.contains(&x1) && range.contains(&x2) { ranges.push(x1..=x2); } }, TargetShape::BlockWise => { let lx = sb.x.min(se.x); let rx = sb.x.max(se.x); let x1 = lx.max(start); let x2 = rx.min(maxcol); if range.contains(&x1) && range.contains(&x2) { ranges.push(x1..=x2); } }, } } ranges.sort_by(|a, b| { let res = a.start().cmp(b.start()); if res != Ordering::Equal { return res; } return a.end().cmp(b.end()); }); return ranges; } fn _redraw_wrap( &mut self, prompt: &Option, off: u16, context: &mut EditorContext, ) -> Result { let width = self.viewctx.dimensions.0; let height = self.viewctx.dimensions.1; let cursor = self.buffer.get_leader(self.gid); let cby = self.viewctx.corner.y; let cbx = self.viewctx.corner.x; let mut line = cby; let mut wrapped = Vec::new(); let mut sawcursor = false; for s in self.buffer.lines_at(line, cbx) { if wrapped.len() >= height && sawcursor { break; } let mut off = 0; let slen = s.len(); while off < slen && (wrapped.len() < height || !sawcursor) { let start = off; let end = (start + width).min(slen); let swrapped = s.slice(CharOff::from(start)..CharOff::from(end)).to_string(); let cursor_line = line == cursor.y && (start..=end).contains(&cursor.x); wrapped.push((line, start, end, swrapped, cursor_line)); if cursor_line { sawcursor = true; } off = end; } if slen == 0 { wrapped.push((line, 0, 0, s.to_string(), line == cursor.y)); } line += 1; } if wrapped.len() > height { let n = wrapped.len() - height; let _ = wrapped.drain(..n); let (line, start, _, _, _) = wrapped.first().unwrap(); self.viewctx.corner.set_y(*line); self.viewctx.corner.set_x(*start); } let mut x = 0; let mut y = context.top + off; let avail = height.saturating_sub(y as usize); let lines = wrapped.len(); if avail < lines { let amt = lines.saturating_sub(avail) as u16; context.stdout.queue(ScrollUp(amt))?; context.top = context.top.saturating_sub(amt); y = y.saturating_sub(amt); } let bot = self.viewctx.dimensions.1 as u16; let mut term_cursor = (0, 0); context.stdout.queue(MoveTo(0, y))?; if let Some(ref prompt) = prompt { context.stdout.queue(Print(prompt))?; x = prompt.len() as u16; } for (line, start, end, s, cursor_line) in wrapped.into_iter() { if y >= bot { break; } if cursor_line { let coff = (cursor.x - start) as u16; term_cursor = (x + coff, y); } let ranges = self._highlight_ranges(line, start, end); // XXX: need to highlight followers, too. // let finfo = self.buffer._follower_intervals(self.gid); let mut prev = 0; context.stdout.queue(MoveTo(x, y))?; for range in ranges { let rs = prev.max(*range.start()); let re = *range.end(); context.stdout.queue(Print(&s[prev..rs]))?; let neg = s[(rs - start)..=(re - start)].negative(); prev = re.saturating_add(1); context.stdout.queue(PrintStyledContent(neg))?; } context.stdout.queue(Print(&s[prev..]))?; y += 1; } context.stdout.queue(MoveTo(term_cursor.0, term_cursor.1))?; Ok(lines as u16) } pub fn get_trim(&self) -> EditRope { self.buffer.get().trim_end_matches(super::is_newline) } fn _redraw_nowrap( &mut self, _: &Option, _: u16, _: &mut EditorContext, ) -> Result { Ok(0) } pub fn set_text>(&mut self, text: T) { self.buffer.set_text(text) } pub fn redraw( &mut self, prompt: &Option, off: u16, context: &mut EditorContext, ) -> Result { if self.viewctx.wrap { self._redraw_wrap(prompt, off, context) } else { self._redraw_nowrap(prompt, off, context) } } pub fn reset(&mut self) -> EditRope { self.scrollback = ScrollbackState::Pending; self.buffer.reset() } pub fn find( &mut self, history: &mut HistoryList, needle: &Regex, dir: MoveDir1D, inc: bool, ) -> Option { if self.scrollback == ScrollbackState::Pending { let rope = self.get_trim(); if rope.is_empty() { self.scrollback = ScrollbackState::Empty; } else { self.scrollback = ScrollbackState::Typed; history.append(rope); } } history.find(needle, dir, inc).cloned() } pub fn recall( &mut self, history: &mut HistoryList, dir: MoveDir1D, prefixed: bool, count: usize, ) -> Option { history.recall(self.buffer.get(), &mut self.scrollback, dir, prefixed, count) } pub fn line_leftover(&mut self, dir: MoveDir1D, count: usize) -> usize { let leader = self.buffer.get_leader(self.gid); let text = self.buffer.get(); match dir { MoveDir1D::Next => { let avail = text.get_lines().saturating_sub(1).saturating_sub(leader.y); return count.saturating_sub(avail); }, MoveDir1D::Previous => { return count.saturating_sub(leader.y); }, } } } impl Deref for Editor where I: ApplicationInfo, { type Target = EditBuffer; fn deref(&self) -> &Self::Target { &self.buffer } } impl DerefMut for Editor where I: ApplicationInfo, { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.buffer } } impl Editable, I> for Editor where I: ApplicationInfo, { fn editor_command( &mut self, act: &EditorAction, ctx: &EditContext, store: &mut Store, ) -> EditResult { let ctx = (self.gid, &self.viewctx, ctx); self.buffer.editor_command(act, &ctx, store) } } impl Jumpable for Editor where I: ApplicationInfo, { fn jump( &mut self, list: PositionList, dir: MoveDir1D, count: usize, ctx: &EditContext, ) -> UIResult { let ctx = (self.gid, &self.viewctx, ctx); self.buffer.jump(list, dir, count, &ctx) } } #[cfg(test)] mod tests { use super::super::{ReadLineId, ReadLineInfo}; use super::*; fn mked() -> Editor { Editor::new(ReadLineId::Line) } fn mkedstr(s: &str) -> Editor { let mut ed = mked(); ed.set_text(s); ed } fn strs(hist: &HistoryList) -> Vec { hist.iter().map(ToString::to_string).collect() } #[test] fn test_line_leftover() { let mut ed = mked(); ed.set_text("a\nb\nc\n"); assert_eq!(ed.buffer.get_leader(ed.gid), Cursor::new(0, 0)); assert_eq!(ed.line_leftover(MoveDir1D::Previous, 1), 1); assert_eq!(ed.line_leftover(MoveDir1D::Previous, 2), 2); assert_eq!(ed.line_leftover(MoveDir1D::Previous, 3), 3); assert_eq!(ed.line_leftover(MoveDir1D::Next, 1), 0); assert_eq!(ed.line_leftover(MoveDir1D::Next, 2), 0); assert_eq!(ed.line_leftover(MoveDir1D::Next, 3), 1); ed.buffer.set_leader(ed.gid, Cursor::new(1, 0)); assert_eq!(ed.line_leftover(MoveDir1D::Previous, 1), 0); assert_eq!(ed.line_leftover(MoveDir1D::Previous, 2), 1); assert_eq!(ed.line_leftover(MoveDir1D::Previous, 3), 2); assert_eq!(ed.line_leftover(MoveDir1D::Previous, 1), 0); assert_eq!(ed.line_leftover(MoveDir1D::Previous, 2), 1); assert_eq!(ed.line_leftover(MoveDir1D::Previous, 3), 2); } #[test] fn test_find_empty() { let mut ed = mkedstr("\n"); let mut history = HistoryList::new(EditRope::from("hello world"), 100); let needle = Regex::new("he").unwrap(); history.push("foo".into()); history.push("help me".into()); history.push("bar".into()); history.push("writhe".into()); history.push("baz".into()); let v = vec!["hello world", "foo", "help me", "bar", "writhe", "baz"]; assert_eq!(ed.scrollback, ScrollbackState::Pending); assert_eq!(strs(&history), v); let res = ed.find(&mut history, &needle, MoveDir1D::Previous, false).unwrap(); assert_eq!(res, EditRope::from("writhe")); assert_eq!(ed.scrollback, ScrollbackState::Empty); assert_eq!(strs(&history), v); let res = ed.find(&mut history, &needle, MoveDir1D::Previous, false).unwrap(); assert_eq!(res, EditRope::from("help me")); assert_eq!(ed.scrollback, ScrollbackState::Empty); assert_eq!(strs(&history), v); let res = ed.find(&mut history, &needle, MoveDir1D::Next, false).unwrap(); assert_eq!(res, EditRope::from("writhe")); assert_eq!(ed.scrollback, ScrollbackState::Empty); assert_eq!(strs(&history), v); } #[test] fn test_find_typed() { let mut ed = mkedstr("quux\n"); let mut history = HistoryList::new(EditRope::from("hello world"), 100); let needle = Regex::new("he").unwrap(); history.push("foo".into()); history.push("help me".into()); history.push("bar".into()); history.push("writhe".into()); history.push("baz".into()); let v = vec!["hello world", "foo", "help me", "bar", "writhe", "baz"]; assert_eq!(ed.scrollback, ScrollbackState::Pending); assert_eq!(strs(&history), v); let v = vec![ "hello world", "foo", "help me", "bar", "writhe", "baz", "quux", ]; let res = ed.find(&mut history, &needle, MoveDir1D::Previous, false).unwrap(); assert_eq!(res, EditRope::from("writhe")); assert_eq!(ed.scrollback, ScrollbackState::Typed); assert_eq!(strs(&history), v); let res = ed.find(&mut history, &needle, MoveDir1D::Previous, false).unwrap(); assert_eq!(res, EditRope::from("help me")); assert_eq!(ed.scrollback, ScrollbackState::Typed); assert_eq!(strs(&history), v); let res = ed.find(&mut history, &needle, MoveDir1D::Next, false).unwrap(); assert_eq!(res, EditRope::from("writhe")); assert_eq!(ed.scrollback, ScrollbackState::Typed); assert_eq!(strs(&history), v); } #[test] fn test_recall_empty() { let mut ed = mkedstr("\n"); let mut history = HistoryList::new(EditRope::from("hello world"), 100); history.push("foo".into()); history.push("help me".into()); history.push("bar".into()); history.push("writhe".into()); history.push("baz".into()); let v = vec!["hello world", "foo", "help me", "bar", "writhe", "baz"]; assert_eq!(ed.scrollback, ScrollbackState::Pending); assert_eq!(strs(&history), v); let res = ed.recall(&mut history, MoveDir1D::Previous, false, 3).unwrap(); assert_eq!(res, EditRope::from("bar")); assert_eq!(ed.scrollback, ScrollbackState::Empty); assert_eq!(strs(&history), v); let res = ed.recall(&mut history, MoveDir1D::Next, false, 1).unwrap(); assert_eq!(res, EditRope::from("writhe")); assert_eq!(ed.scrollback, ScrollbackState::Empty); assert_eq!(strs(&history), v); let res = ed.recall(&mut history, MoveDir1D::Next, false, 2).unwrap(); assert_eq!(res, EditRope::from("")); assert_eq!(ed.scrollback, ScrollbackState::Pending); assert_eq!(strs(&history), v); } #[test] fn test_recall_typed() { let mut ed = mkedstr("quux\n"); let mut history = HistoryList::new(EditRope::from("hello world"), 100); history.push("foo".into()); history.push("help me".into()); history.push("bar".into()); history.push("writhe".into()); history.push("baz".into()); let v = vec!["hello world", "foo", "help me", "bar", "writhe", "baz"]; assert_eq!(ed.scrollback, ScrollbackState::Pending); assert_eq!(strs(&history), v); let v = vec![ "hello world", "foo", "help me", "bar", "writhe", "baz", "quux", ]; let res = ed.recall(&mut history, MoveDir1D::Previous, false, 3).unwrap(); assert_eq!(res, EditRope::from("bar")); assert_eq!(ed.scrollback, ScrollbackState::Typed); assert_eq!(strs(&history), v); let res = ed.recall(&mut history, MoveDir1D::Next, false, 1).unwrap(); assert_eq!(res, EditRope::from("writhe")); assert_eq!(ed.scrollback, ScrollbackState::Typed); assert_eq!(strs(&history), v); let res = ed.recall(&mut history, MoveDir1D::Next, false, 2).unwrap(); assert_eq!(res, EditRope::from("quux")); assert_eq!(ed.scrollback, ScrollbackState::Typed); assert_eq!(strs(&history), v); } }