diff options
| -rw-r--r-- | src/cursor.rs | 10 | ||||
| -rw-r--r-- | src/lib.rs | 208 | ||||
| -rw-r--r-- | src/line/buf.rs (renamed from src/linebuf.rs) | 12 | ||||
| -rw-r--r-- | src/line/mod.rs | 219 | ||||
| -rw-r--r-- | src/parse/mod.rs | 84 | ||||
| -rw-r--r-- | src/parse/span.rs | 86 | ||||
| -rw-r--r-- | src/run/builtin.rs | 61 | ||||
| -rw-r--r-- | src/syntax_highlighting.rs | 76 |
8 files changed, 556 insertions, 200 deletions
diff --git a/src/cursor.rs b/src/cursor.rs index fbaacbb..1ec23d5 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -8,9 +8,9 @@ pub enum Direction { Right, } -pub fn move_cursor(direction: Direction, n: usize) { +pub fn fmove_cursor(direction: Direction, n: usize, stdout: &mut dyn Write) -> std::io::Result<()> { if n == 0 { - return; + return Ok(()); } let code = match direction { @@ -20,7 +20,11 @@ pub fn move_cursor(direction: Direction, n: usize) { Direction::Left => 'D', }; - print!("\x1b[{n}{code}"); + write!(stdout, "\x1b[{n}{code}") +} + +pub fn move_cursor(direction: Direction, n: usize) { + fmove_cursor(direction, n, &mut std::io::stdout()).unwrap() } pub fn save() { @@ -25,7 +25,7 @@ pub mod date; pub mod defer; pub mod export_fun; pub mod history; -pub mod linebuf; +pub mod line; pub mod panic; pub mod parse; pub mod raw; @@ -33,15 +33,15 @@ pub mod reload; pub mod run; pub mod rw; pub mod serialization; +pub mod syntax_highlighting; pub mod wait; -use linebuf::LineBuf; use raw::*; use crate::completion::{PathCache, completion}; use crate::ctrlc::CtrlC; -use crate::cursor::{Direction, move_cursor}; use crate::history::HistoryEntry; +use crate::line::Line; use crate::parse::{Block, ExpString, Parse, PostExpansion}; macro_rules! print { @@ -93,7 +93,7 @@ impl PushAll for BString { pub struct Session { raw: Option<ScopedRawMode>, - line: LineBuf, + line: Line, history: Vec<HistoryEntry>, prev_path: BString, builtins: HashMap<BString, &'static dyn run::BuiltinClone>, @@ -115,13 +115,15 @@ pub struct Session { /// n before end of history.len() /// 0 == not checking history history_visit: usize, + + highlighter: syntax_highlighting::Highlighter, } impl Session { pub fn new_noninteractive() -> Self { Self { raw: None, - line: LineBuf::new(), + line: Line::new(), history: Vec::new(), prev_path: b".".into(), builtins: HashMap::new(), @@ -136,6 +138,7 @@ impl Session { debug_keystrokes: false, loud: false, history_visit: 0, + highlighter: syntax_highlighting::Highlighter::new(), } } } @@ -212,17 +215,8 @@ impl Session { expanded } - fn clear_prompt(&mut self) { - cursor::move_cursor(Direction::Right, self.line.distance_from_right_end()); - for _ in 0..self.line.len() { - print!("\x08 \x08"); - } - io::stdout().lock().flush().unwrap(); - self.line.clear(); - } - fn prompt_clear(&mut self) { - self.clear_prompt(); + self.line.clear_prompt().unwrap(); self.history_visit = 0; } @@ -234,7 +228,7 @@ impl Session { } fn display_historic_entry(&mut self) { - self.clear_prompt(); + self.line.clear_prompt().unwrap(); let new = if self.history_visit == 0 { Vec::new() } else { @@ -244,7 +238,7 @@ impl Session { }; io::stdout().write_all(&new).unwrap(); io::stdout().flush().unwrap(); - self.line.set_content(new); + self.line.set_content(new).unwrap(); } fn history_up(&mut self) { @@ -261,150 +255,26 @@ impl Session { } } - fn type_bytes(&mut self, bs: &[u8]) { - for b in bs.iter() { - self.line.add(*b); - } - io::stdout().lock().write_all(&bs).unwrap(); - self.line.display_post(b""); - } - - fn type_byte(&mut self, b: u8) { - self.type_bytes(&[b]); - } - - fn del_left(&mut self) { - if self.line.del_left().is_some() { - cursor::move_cursor(Direction::Left, 1); - self.line.display_post(b" "); - } - } - - fn del_right(&mut self) { - self.line.del_right(); - self.line.display_post(b" "); - } - fn del_left_or_previous(&mut self) { - if self.line.is_empty() && !self.line.is_dirty() && !self.history.is_empty() { + if self.line.is_empty() && !self.line.had_contents() && !self.history.is_empty() { // take previous command for editing let cmd = self.history[self.history.len() - 1].cmd.clone(); - self.type_bytes(&cmd); + self.line.type_bytes(&cmd).unwrap(); } else { - self.del_left(); + self.line.del_left().unwrap(); } } fn prompt_pipe_previous(&mut self) { - if self.line.is_empty() && let Some(prev) = self.history.last() { + if self.line.is_empty() + && let Some(prev) = self.history.last() + { let mut cmd = prev.cmd.clone(); cmd.push_all(b" | "); - self.type_bytes(&cmd); + self.line.type_bytes(&cmd).unwrap(); } else { - self.type_byte(b'|'); - } - } - - fn move_to_begin(&mut self) { - cursor::move_cursor(Direction::Left, self.line.all_left()); - io::stdout().flush().unwrap(); - } - - fn move_to_end(&mut self) { - cursor::move_cursor(Direction::Right, self.line.all_right()); - io::stdout().flush().unwrap(); - } - - fn del_left_word(&mut self) { - let mut del = 0; - while let Some(x) = self.line.get_left() - && x == b' ' - { - self.line.del_left(); - del += 1; - } - while let Some(x) = self.line.get_left() - && x != b' ' - { - self.line.del_left(); - del += 1; - } - cursor::move_cursor(Direction::Left, del); - self.line.display_post(&vec![b' '; del]); - } - - fn del_right_word(&mut self) { - let mut del = 0; - while let Some(x) = self.line.get_right() - && x == b' ' - { - self.line.del_right(); - del += 1; - } - while let Some(x) = self.line.get_right() - && x != b' ' - { - self.line.del_right(); - del += 1; - } - self.line.display_post(&vec![b' '; del]); - } - - fn cursor_left(&mut self) { - if self.line.left() { - move_cursor(Direction::Left, 1); - io::stdout().lock().flush().unwrap(); - } - } - - fn cursor_right(&mut self) { - if self.line.right() { - move_cursor(Direction::Right, 1); - io::stdout().lock().flush().unwrap(); - } - } - - // move to next - fn cursor_left_word(&mut self) { - let mut i = 0; - - // find word - while let Some(b' ') = self.line.get_left() { - self.line.left(); - i += 1; - } - - // skip it - while let Some(x) = self.line.get_left() - && !x.is_ascii_whitespace() - { - self.line.left(); - i += 1; - } - - cursor::move_cursor(Direction::Left, i); - io::stdout().flush().unwrap(); - } - - fn cursor_right_word(&mut self) { - let mut i = 0; - - // find word - while let Some(b' ') = self.line.get_right() { - self.line.right(); - i += 1; + self.line.type_byte(b'|').unwrap(); } - - // skip it - while let Some(x) = self.line.get_right() - && !x.is_ascii_whitespace() - { - self.line.right(); - i += 1; - } - - cursor::move_cursor(Direction::Right, i); - io::stdout().flush().unwrap(); } fn complete(session: Arc<Mutex<Session>>) { @@ -414,7 +284,7 @@ impl Session { let mut se = session.lock().unwrap(); - se.type_bytes(&comp.shared_prefix); + se.line.type_bytes(&comp.shared_prefix).unwrap(); if comp.suggestions.len() > 1 { print!("\r\n"); @@ -435,7 +305,7 @@ impl Session { let parsed = match parse::do_parse(&line) { Ok(p) => p, Err(_) => { - se.line.add(b'\n'); + se.line.type_byte(b'\n').unwrap(); print!("\r\n> "); return; } @@ -467,6 +337,27 @@ impl Session { r.disable(); } } + + /// do cleanup actions after a bunch of mutations. + fn cohere(&mut self) -> io::Result<()> { + if self.line.is_dirty() { + self.line.mark_clean(); + + // overwrite line in terminal with syntax-highlighted line. + if self.highlighter.enabled { + let buf = self.line.into_bytes(); + let mut parser = parse::Cursor::new(&buf, parse::ParseMode::Completion); + let _res = parse::Ast::parse(&mut parser); + let mut stdout = io::stdout().lock(); + cursor::fmove_cursor(cursor::Direction::Left, self.line.left_len(), &mut stdout)?; + self.highlighter + .pretty_print(&buf, parser.highlights, &mut stdout)?; + cursor::fmove_cursor(cursor::Direction::Left, self.line.right_len(), &mut stdout)?; + } + } + + Ok(()) + } } fn exec_rc_file(se: Arc<Mutex<Session>>) { @@ -492,21 +383,14 @@ pub fn event_loop() { let se = Session { raw: Some(raw), - line: LineBuf::new(), - history: Vec::new(), + line: Line::new(), builtins: run::builtin_map(), prev_path: vec![b'.'], history_visit: 0, socket_running: None, - vars: run::Vars::default(), - funs: HashMap::new(), - aliases: run::Aliases::new(), path_cache: Default::default(), ctrlc: Default::default(), - debug_keystrokes: false, - loud: false, - ti_keybinds: HashMap::new(), - ascii_keybinds: HashMap::new(), + ..Session::new_noninteractive() }; let session = Arc::new(Mutex::new(se)); @@ -540,7 +424,7 @@ pub fn event_loop() { } match key { - ansi::KbInput::Key([x]) => se.type_byte(x), + ansi::KbInput::Key([x]) => se.line.type_byte(x).unwrap(), ansi::KbInput::Escape(escape) => { for terminfo_key in escape.keys.iter() { if let Some(cmd) = se.ti_keybinds.get(terminfo_key.as_bytes()) { @@ -554,6 +438,8 @@ pub fn event_loop() { } ansi::KbInput::InvalidEscape(_) => continue, } + + se.cohere().unwrap(); } session.lock().unwrap().raw_disable(); diff --git a/src/linebuf.rs b/src/line/buf.rs index 548ec75..420546a 100644 --- a/src/linebuf.rs +++ b/src/line/buf.rs @@ -67,11 +67,11 @@ impl LineBuf { n } - pub fn get_left(&self) -> Option<u8>{ + pub fn get_left(&self) -> Option<u8> { self.pre.last().cloned() } - pub fn get_right(&self) -> Option<u8>{ + pub fn get_right(&self) -> Option<u8> { self.post.last().cloned() } @@ -139,4 +139,12 @@ impl LineBuf { move_cursor(Direction::Left, self.post.len() + post.len()); std::io::stdout().flush().unwrap(); } + + pub fn left_len(&self) -> usize { + self.pre.len() + } + + pub fn right_len(&self) -> usize { + self.post.len() + } } diff --git a/src/line/mod.rs b/src/line/mod.rs new file mode 100644 index 0000000..72f2fe1 --- /dev/null +++ b/src/line/mod.rs @@ -0,0 +1,219 @@ +mod buf; + +use std::io::{self, Write}; + +use crate::cursor::{self, Direction}; +pub use buf::LineBuf; + +pub struct Line { + buf: LineBuf, + dirty: bool, +} + +impl std::ops::Deref for Line { + type Target = LineBuf; + + fn deref(&self) -> &Self::Target { + &self.buf + } +} + +impl Line { + pub fn new() -> Self { + Self { + buf: LineBuf::new(), + dirty: false, + } + } + + pub fn mark_clean(&mut self) { + self.dirty = false; + } + + pub fn is_dirty(&self) -> bool { + self.dirty + } + + pub fn had_contents(&self) -> bool { + self.buf.is_dirty() + } + + pub fn is_empty(&self) -> bool { + self.buf.is_empty() + } + + pub fn cursor_left(&mut self) -> io::Result<()> { + if self.buf.left() { + let mut stdout = io::stdout().lock(); + cursor::fmove_cursor(Direction::Left, 1, &mut stdout)?; + stdout.flush()?; + } + Ok(()) + } + + pub fn cursor_right(&mut self) -> io::Result<()> { + if self.buf.right() { + let mut stdout = io::stdout().lock(); + cursor::fmove_cursor(Direction::Right, 1, &mut stdout)?; + stdout.flush()?; + } + Ok(()) + } + + pub fn move_to_begin(&mut self) -> io::Result<()> { + let mut stdout = io::stdout().lock(); + cursor::fmove_cursor(Direction::Left, self.buf.all_left(), &mut stdout)?; + stdout.flush() + } + + pub fn move_to_end(&mut self) -> io::Result<()> { + let mut stdout = io::stdout().lock(); + cursor::fmove_cursor(Direction::Right, self.buf.all_right(), &mut stdout)?; + stdout.flush() + } + + // move to next + pub fn cursor_left_word(&mut self) -> io::Result<()> { + let mut i = 0; + + // find word + while let Some(b' ') = self.buf.get_left() { + self.buf.left(); + i += 1; + } + + // skip it + while let Some(x) = self.buf.get_left() + && !x.is_ascii_whitespace() + { + self.buf.left(); + i += 1; + } + + let mut stdout = io::stdout().lock(); + cursor::fmove_cursor(Direction::Left, i, &mut stdout)?; + stdout.flush() + } + + pub fn cursor_right_word(&mut self) -> io::Result<()> { + let mut i = 0; + + // find word + while let Some(b' ') = self.buf.get_right() { + self.buf.right(); + i += 1; + } + + // skip it + while let Some(x) = self.buf.get_right() + && !x.is_ascii_whitespace() + { + self.buf.right(); + i += 1; + } + + let mut stdout = io::stdout().lock(); + cursor::fmove_cursor(Direction::Right, i, &mut stdout)?; + stdout.flush() + } +} + +macro_rules! mutating_impls { + ($(pub fn $fun:ident(&mut $self:ident $($arg:tt)*) -> $ty:ty $body:block )*) => { + impl Line {$( + pub fn $fun(&mut $self $($arg)*) -> $ty { + $self.dirty = true; + $body + } + )*} + }; +} + +mutating_impls! { + pub fn clear_prompt(&mut self) -> io::Result<()> { + let mut stdout = io::stdout().lock(); + cursor::fmove_cursor( + Direction::Right, + self.buf.distance_from_right_end(), + &mut stdout, + )?; + for _ in 0..self.buf.len() { + stdout.write_all(b"\x08 \x08")?; + } + self.buf.clear(); + stdout.flush() + } + + pub fn type_bytes(&mut self, bs: &[u8]) -> io::Result<()> { + for b in bs.iter() { + self.buf.add(*b); + } + io::stdout().lock().write_all(&bs)?; + self.buf.display_post(b""); + Ok(()) + } + + pub fn type_byte(&mut self, b: u8) -> io::Result<()> { + self.type_bytes(&[b]) + } + + pub fn del_left(&mut self) -> io::Result<()> { + if self.buf.del_left().is_some() { + cursor::fmove_cursor(Direction::Left, 1, &mut io::stdout())?; + self.buf.display_post(b" "); + } + Ok(()) + } + + pub fn del_right(&mut self) -> io::Result<()> { + self.buf.del_right(); + self.buf.display_post(b" "); + Ok(()) + } + + pub fn del_left_word(&mut self) -> io::Result<()> { + let mut del = 0; + while let Some(x) = self.buf.get_left() + && x == b' ' + { + self.buf.del_left(); + del += 1; + } + while let Some(x) = self.buf.get_left() + && x != b' ' + { + self.buf.del_left(); + del += 1; + } + cursor::fmove_cursor(Direction::Left, del, &mut io::stdout())?; + self.buf.display_post(&vec![b' '; del]); + Ok(()) + } + + pub fn del_right_word(&mut self) -> io::Result<()> { + let mut del = 0; + while let Some(x) = self.buf.get_right() + && x == b' ' + { + self.buf.del_right(); + del += 1; + } + while let Some(x) = self.buf.get_right() + && x != b' ' + { + self.buf.del_right(); + del += 1; + } + self.buf.display_post(&vec![b' '; del]); + Ok(()) + } + + pub fn set_content(&mut self, content: Vec<u8>) -> io::Result<()> { + self.buf.set_content(content); + Ok(()) + } + + pub fn dump(&mut self) -> Vec<u8> { + self.buf.dump() + } +} diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 102b334..97a6e4a 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -3,6 +3,8 @@ use crate::{BString, PushAll, bstr}; #[cfg(test)] mod test; +mod span; + pub trait Stage: PartialEq { type Str: std::fmt::Debug + Clone + PartialEq; } @@ -908,6 +910,7 @@ impl Parse for ExpString { let mut last_delim = StringDelimiter::None; 'outer: loop { + let begin = b.loc(); let Some(delim) = StringDelimiter::try_begin(b) else { break; }; @@ -1034,6 +1037,14 @@ impl Parse for ExpString { add_char(p, x); } } + + if !delim.is_none() { + let end = b.loc_u32(); + b.highlights.push(Highlight { + span: begin.to(end), + kind: HighlightKind::String, + }); + } } if already_parsed { @@ -1285,6 +1296,18 @@ pub enum ParseMode { Completion, } +#[derive(Copy, Clone)] +pub enum HighlightKind { + Keyword(Keyword), + String, + None, +} + +pub struct Highlight { + pub span: span::Span, + pub kind: HighlightKind, +} + pub struct Cursor<'a> { buf: &'a [u8], mode: ParseMode, @@ -1293,6 +1316,13 @@ pub struct Cursor<'a> { spaced: bool, pub backtrace: bool, + + pub highlights: Vec<Highlight>, + + file: span::FileId, + + buf_start: u64, + buf_len: u32, } #[derive(Default)] @@ -1305,11 +1335,20 @@ struct SpaceStats { impl<'a> Cursor<'a> { pub fn new(buf: &'a [u8], mode: ParseMode) -> Self { + assert!( + buf.len() < u32::MAX as usize, + "cannot support larger parse buffers for now - what are you even doing." + ); + Self { buf, mode, spaced: false, backtrace: false, + highlights: Vec::new(), + file: span::FileId::new(), + buf_start: buf.as_ptr() as u64, + buf_len: buf.len() as u32, } } @@ -1377,6 +1416,18 @@ impl<'a> Cursor<'a> { } } + fn loc_u32(&self) -> u32 { + let now_loc = self.buf.as_ptr() as u64; + assert!(now_loc >= self.buf_start, "not the original buffer"); + let relative_loc = (now_loc - self.buf_start) as u32; + assert!(relative_loc <= self.buf_len, "not the original buffer"); + relative_loc + } + + fn loc(&self) -> span::SpanFrom { + self.file.from(self.loc_u32()) + } + fn spaces_stats(&mut self) -> SpaceStats { let mut stats = SpaceStats::default(); while self.has() && b" \t\n\r".contains(&self.buf[0]) { @@ -1418,7 +1469,8 @@ impl<'a> Cursor<'a> { } self.spaces(); - if self.buf.starts_with(bytes) { + let span = self.loc().with_len(bytes.len() as u32); + let result = if self.buf.starts_with(bytes) { if kw.requires_space() { if self.buf.len() > bytes.len() && self.buf[bytes.len()].is_ascii_whitespace() { self.buf = &self.buf[bytes.len() + 1..]; @@ -1439,11 +1491,20 @@ impl<'a> Cursor<'a> { } } else { Err(ParseError::ExpectedKeyword(kw)) + }; + + if result.is_ok() { + self.highlights.push(Highlight { + span, + kind: HighlightKind::Keyword(kw), + }) } + + result } } -#[derive(Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum Keyword { If, While, @@ -1546,8 +1607,8 @@ impl Parse for While { } } -impl Parse for Ast<PreExpansion> { - fn parse(b: &mut Cursor<'_>) -> Result<Self> { +impl Ast<PreExpansion> { + fn parse_inner(b: &mut Cursor<'_>) -> Result<Self> { b.spaces(); let orig_len = b.buf.len(); @@ -1586,6 +1647,21 @@ impl Parse for Ast<PreExpansion> { } } +impl Parse for Ast<PreExpansion> { + fn parse(b: &mut Cursor<'_>) -> Result<Self> { + let begin = b.loc(); + let result = Ast::parse_inner(b); + let span = begin.to(b.loc_u32()); + if result.is_ok() { + b.highlights.push(Highlight { + span, + kind: HighlightKind::None, + }); + } + result + } +} + impl Parse for Command<PreExpansion> { fn parse(b: &mut Cursor<'_>) -> Result<Self> { let path: ExpString = b.parse()?; diff --git a/src/parse/span.rs b/src/parse/span.rs new file mode 100644 index 0000000..340a078 --- /dev/null +++ b/src/parse/span.rs @@ -0,0 +1,86 @@ +use std::sync::atomic::AtomicU32; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FileId { + id: u32, +} + +impl FileId { + pub fn new() -> Self { + static GEN: AtomicU32 = AtomicU32::new(0); + Self { + id: GEN.fetch_add(1, std::sync::atomic::Ordering::SeqCst), + } + } + + pub fn from(self, start: u32) -> SpanFrom { + SpanFrom::new(self, start) + } + + pub fn span(self, start: u32, end: u32) -> Span { + Span::new(self, start, end) + } +} + +#[derive(Copy, Clone)] +pub struct SpanFrom { + pub file: FileId, + pub start: u32, +} + +impl SpanFrom { + pub fn new(file: FileId, start: u32) -> Self { + Self { file, start } + } + + pub fn to(self, end: u32) -> Span { + Span::new(self.file, self.start, end) + } + + pub fn with_len(self, len: u32) -> Span { + self.to(self.start + len) + } +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Span { + pub file: FileId, + pub start: u32, + pub end: u32, +} + +/// manual implementation of PartialOrd to ensure shorter Spans are first +impl PartialOrd for Span { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + match self.file.partial_cmp(&other.file) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.start.partial_cmp(&other.start) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + other.end.partial_cmp(&self.end) + } +} + +/// manual implementation of Ord to ensure shorter Spans are first +impl Ord for Span { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.file.cmp(&other.file) { + core::cmp::Ordering::Equal => {} + ord => return ord, + } + match self.start.cmp(&other.start) { + core::cmp::Ordering::Equal => {} + ord => return ord, + } + other.end.cmp(&self.end) + } +} + +impl Span { + pub fn new(file: FileId, start: u32, end: u32) -> Self { + Self { file, start, end } + } +} diff --git a/src/run/builtin.rs b/src/run/builtin.rs index 638cfcd..c946472 100644 --- a/src/run/builtin.rs +++ b/src/run/builtin.rs @@ -802,37 +802,38 @@ impl Builtin for ct { return Err(Error::Exit(-1)); }; - let mut se = session.lock().unwrap(); - - match &arg[..] { - b"cursor_begin" => se.move_to_begin(), - b"cursor_end" => se.move_to_end(), - b"cursor_right" => se.cursor_right(), - b"cursor_left" => se.cursor_left(), - b"cursor_right_word" => se.cursor_right_word(), - b"cursor_left_word" => se.cursor_left_word(), - b"prompt_clear" => se.prompt_clear(), - b"screen_clear" => { - drop(se); - Session::screen_clear(session); - } - b"history_previous" => se.history_up(), - b"history_next" => se.history_down(), - b"prompt_del_left" => se.del_left(), - b"prompt_del_right" => se.del_right(), - b"prompt_del_left_word" => se.del_left_word(), - b"prompt_del_right_word" => se.del_right_word(), - b"prompt_del_left_or_previous" => se.del_left_or_previous(), - b"prompt_pipe_previous" => se.prompt_pipe_previous(), - b"complete" => { - drop(se); - Session::complete(session) - } - b"try_submit_command" => { - drop(se); - Session::try_submit_command(session); + { + let mut se = session.lock().unwrap(); + match &arg[..] { + b"cursor_begin" => se.line.move_to_begin().unwrap(), + b"cursor_end" => se.line.move_to_end().unwrap(), + b"cursor_right" => se.line.cursor_right().unwrap(), + b"cursor_left" => se.line.cursor_left().unwrap(), + b"cursor_right_word" => se.line.cursor_right_word().unwrap(), + b"cursor_left_word" => se.line.cursor_left_word().unwrap(), + b"prompt_clear" => se.prompt_clear(), + b"screen_clear" => { + drop(se); + Session::screen_clear(session.clone()); + } + b"history_previous" => se.history_up(), + b"history_next" => se.history_down(), + b"prompt_del_left" => se.line.del_left().unwrap(), + b"prompt_del_right" => se.line.del_right().unwrap(), + b"prompt_del_left_word" => se.line.del_left_word().unwrap(), + b"prompt_del_right_word" => se.line.del_right_word().unwrap(), + b"prompt_del_left_or_previous" => se.del_left_or_previous(), + b"prompt_pipe_previous" => se.prompt_pipe_previous(), + b"complete" => { + drop(se); + Session::complete(session.clone()) + } + b"try_submit_command" => { + drop(se); + Session::try_submit_command(session.clone()); + } + _ => return Err(Error::Exit(-2)), } - _ => return Err(Error::Exit(-2)), } Ok(()) diff --git a/src/syntax_highlighting.rs b/src/syntax_highlighting.rs new file mode 100644 index 0000000..8a369da --- /dev/null +++ b/src/syntax_highlighting.rs @@ -0,0 +1,76 @@ +use crate::parse::{Highlight, HighlightKind, Keyword}; + +pub struct Highlighter { + pub enabled: bool, +} + +impl Highlighter { + pub fn new() -> Self { + Self { enabled: true } + } + + pub fn color(&self, h: HighlightKind) -> &[u8] { + // TODO: configurable + const GREEN: &[u8] = b"\x1b[32m"; + const BLUE: &[u8] = b"\x1b[36m"; + const MAGENTA: &[u8] = b"\x1b[95m"; + const COLOR_RESET: &[u8] = b"\x1b[0m"; + match h { + HighlightKind::Keyword( + Keyword::If | Keyword::Elif | Keyword::Else | Keyword::While, + ) => GREEN, + HighlightKind::Keyword(Keyword::OpenBrace | Keyword::CloseBrace) => BLUE, + HighlightKind::String => MAGENTA, + HighlightKind::None => COLOR_RESET, + } + } + + pub fn pretty_print( + &self, + bytes: &[u8], + colors: Vec<Highlight>, + stdout: &mut dyn std::io::Write, + ) -> std::io::Result<()> { + let mut coloring: Vec<_> = colors + .into_iter() + .flat_map(|hi| { + [ + (hi.span.start as usize, false, hi.kind), + (hi.span.end as usize, true, hi.kind), + ] + }) + .collect(); + coloring.sort_by_key(|x| (x.0, x.1)); + let mut coloring = &coloring[..]; + let mut color_stack = Vec::new(); + + let mut current_color = self.color(HighlightKind::None); + + for (i, x) in bytes.iter().cloned().enumerate() { + while let Some((k, is_end, kind)) = coloring.first().cloned() + && k == i + { + coloring = &coloring[1..]; + if is_end { + color_stack.pop(); + } else { + color_stack.push(kind); + } + + let new_color = + self.color(color_stack.last().cloned().unwrap_or(HighlightKind::None)); + + if current_color != new_color { + stdout.write_all(new_color)?; + current_color = new_color; + } + } + + stdout.write_all(&[x])?; + } + + stdout.write_all(self.color(HighlightKind::None))?; + + Ok(()) + } +} |
