From eeb267c46340d5d47f41cc2440f0b281f9ae9261 Mon Sep 17 00:00:00 2001 From: Jonas Maier Date: Fri, 22 May 2026 21:26:41 +0200 Subject: basic syntax highlighting --- src/cursor.rs | 10 ++- src/lib.rs | 208 ++++++++++-------------------------------- src/line/buf.rs | 150 +++++++++++++++++++++++++++++++ src/line/mod.rs | 219 +++++++++++++++++++++++++++++++++++++++++++++ src/linebuf.rs | 142 ----------------------------- src/parse/mod.rs | 84 ++++++++++++++++- src/parse/span.rs | 86 ++++++++++++++++++ src/run/builtin.rs | 61 ++++++------- src/syntax_highlighting.rs | 76 ++++++++++++++++ 9 files changed, 696 insertions(+), 340 deletions(-) create mode 100644 src/line/buf.rs create mode 100644 src/line/mod.rs delete mode 100644 src/linebuf.rs create mode 100644 src/parse/span.rs create mode 100644 src/syntax_highlighting.rs (limited to 'src') 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() { diff --git a/src/lib.rs b/src/lib.rs index 3931f5d..2a9a1ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, - line: LineBuf, + line: Line, history: Vec, prev_path: BString, builtins: HashMap, @@ -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>) { @@ -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>) { @@ -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/line/buf.rs b/src/line/buf.rs new file mode 100644 index 0000000..420546a --- /dev/null +++ b/src/line/buf.rs @@ -0,0 +1,150 @@ +use crate::cursor::*; +use std::io::Write; + +pub struct LineBuf { + pre: Vec, + post: Vec, + dirty: bool, +} + +impl Default for LineBuf { + fn default() -> Self { + Self::new() + } +} + +#[allow(unused)] +impl LineBuf { + pub fn new() -> Self { + Self { + pre: Vec::new(), + post: Vec::new(), + dirty: false, + } + } + + pub fn del_left(&mut self) -> Option { + self.dirty = true; + self.pre.pop() + } + + pub fn del_right(&mut self) -> Option { + self.dirty = true; + self.post.pop() + } + + pub fn len(&self) -> usize { + self.pre.len() + self.post.len() + } + + pub fn left(&mut self) -> bool { + if let Some(byte) = self.del_left() { + self.post.push(byte); + true + } else { + false + } + } + + pub fn right(&mut self) -> bool { + if let Some(byte) = self.del_right() { + self.pre.push(byte); + true + } else { + false + } + } + + pub fn all_left(&mut self) -> usize { + let n = self.pre.len(); + while self.left() {} + n + } + + pub fn all_right(&mut self) -> usize { + let n = self.post.len(); + while self.right() {} + n + } + + pub fn get_left(&self) -> Option { + self.pre.last().cloned() + } + + pub fn get_right(&self) -> Option { + self.post.last().cloned() + } + + pub fn add(&mut self, chr: u8) { + self.dirty = true; + self.pre.push(chr); + } + + pub fn is_empty(&self) -> bool { + self.pre.is_empty() && self.post.is_empty() + } + + pub fn is_dirty(&self) -> bool { + self.dirty + } + + /// sets content all to the left + pub fn set_content(&mut self, buf: Vec) { + self.pre = buf; + self.post = Vec::new(); + } + + pub fn pre(&self) -> &[u8] { + &self.pre + } + + pub fn into_bytes(&self) -> Vec { + let mut buf = Vec::with_capacity(self.pre.len() + self.post.len()); + buf.extend_from_slice(&self.pre); + for b in self.post.iter().rev() { + buf.push(*b); + } + buf + } + + pub fn distance_from_right_end(&self) -> usize { + self.post.len() + } + + /// returns the whole contents of the buffer, and empties it in the process + pub fn dump(&mut self) -> Vec { + while self.right() {} + let mut buf = Vec::new(); + core::mem::swap(&mut self.pre, &mut buf); + self.dirty = false; + buf + } + + pub fn clear(&mut self) { + self.pre.clear(); + self.post.clear(); + self.dirty = false; + } + + pub fn display_pre(&self) { + std::io::stdout().write_all(&self.pre).unwrap(); + } + + /// TODO: kinda ugly that this is here + pub fn display_post(&self, post: &[u8]) { + for &x in self.post.iter().rev() { + std::io::stdout().write_all(&[x]).unwrap(); + } + std::io::stdout().write_all(post).unwrap(); + 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) -> io::Result<()> { + self.buf.set_content(content); + Ok(()) + } + + pub fn dump(&mut self) -> Vec { + self.buf.dump() + } +} diff --git a/src/linebuf.rs b/src/linebuf.rs deleted file mode 100644 index 548ec75..0000000 --- a/src/linebuf.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::cursor::*; -use std::io::Write; - -pub struct LineBuf { - pre: Vec, - post: Vec, - dirty: bool, -} - -impl Default for LineBuf { - fn default() -> Self { - Self::new() - } -} - -#[allow(unused)] -impl LineBuf { - pub fn new() -> Self { - Self { - pre: Vec::new(), - post: Vec::new(), - dirty: false, - } - } - - pub fn del_left(&mut self) -> Option { - self.dirty = true; - self.pre.pop() - } - - pub fn del_right(&mut self) -> Option { - self.dirty = true; - self.post.pop() - } - - pub fn len(&self) -> usize { - self.pre.len() + self.post.len() - } - - pub fn left(&mut self) -> bool { - if let Some(byte) = self.del_left() { - self.post.push(byte); - true - } else { - false - } - } - - pub fn right(&mut self) -> bool { - if let Some(byte) = self.del_right() { - self.pre.push(byte); - true - } else { - false - } - } - - pub fn all_left(&mut self) -> usize { - let n = self.pre.len(); - while self.left() {} - n - } - - pub fn all_right(&mut self) -> usize { - let n = self.post.len(); - while self.right() {} - n - } - - pub fn get_left(&self) -> Option{ - self.pre.last().cloned() - } - - pub fn get_right(&self) -> Option{ - self.post.last().cloned() - } - - pub fn add(&mut self, chr: u8) { - self.dirty = true; - self.pre.push(chr); - } - - pub fn is_empty(&self) -> bool { - self.pre.is_empty() && self.post.is_empty() - } - - pub fn is_dirty(&self) -> bool { - self.dirty - } - - /// sets content all to the left - pub fn set_content(&mut self, buf: Vec) { - self.pre = buf; - self.post = Vec::new(); - } - - pub fn pre(&self) -> &[u8] { - &self.pre - } - - pub fn into_bytes(&self) -> Vec { - let mut buf = Vec::with_capacity(self.pre.len() + self.post.len()); - buf.extend_from_slice(&self.pre); - for b in self.post.iter().rev() { - buf.push(*b); - } - buf - } - - pub fn distance_from_right_end(&self) -> usize { - self.post.len() - } - - /// returns the whole contents of the buffer, and empties it in the process - pub fn dump(&mut self) -> Vec { - while self.right() {} - let mut buf = Vec::new(); - core::mem::swap(&mut self.pre, &mut buf); - self.dirty = false; - buf - } - - pub fn clear(&mut self) { - self.pre.clear(); - self.post.clear(); - self.dirty = false; - } - - pub fn display_pre(&self) { - std::io::stdout().write_all(&self.pre).unwrap(); - } - - /// TODO: kinda ugly that this is here - pub fn display_post(&self, post: &[u8]) { - for &x in self.post.iter().rev() { - std::io::stdout().write_all(&[x]).unwrap(); - } - std::io::stdout().write_all(post).unwrap(); - move_cursor(Direction::Left, self.post.len() + post.len()); - std::io::stdout().flush().unwrap(); - } -} 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, + + 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 { - fn parse(b: &mut Cursor<'_>) -> Result { +impl Ast { + fn parse_inner(b: &mut Cursor<'_>) -> Result { b.spaces(); let orig_len = b.buf.len(); @@ -1586,6 +1647,21 @@ impl Parse for Ast { } } +impl Parse for Ast { + fn parse(b: &mut Cursor<'_>) -> Result { + 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 { fn parse(b: &mut Cursor<'_>) -> Result { 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 { + 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, + 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(()) + } +} -- cgit v1.2.3