From 610e575043bfc75feafcce5bddaf7e1a436e5d02 Mon Sep 17 00:00:00 2001 From: Mica White Date: Sun, 7 Dec 2025 14:23:22 -0500 Subject: First commit --- src/builtins/dit.rs | 788 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 788 insertions(+) create mode 100644 src/builtins/dit.rs (limited to 'src/builtins/dit.rs') diff --git a/src/builtins/dit.rs b/src/builtins/dit.rs new file mode 100644 index 0000000..d6b1feb --- /dev/null +++ b/src/builtins/dit.rs @@ -0,0 +1,788 @@ +use std::{ + collections::{HashMap, VecDeque}, + io::{BufRead, BufReader}, + num::NonZeroU8, + ops::Range, + rc::Rc, + sync::{LazyLock, RwLock, mpsc::Receiver}, + vec::Drain, +}; + +use deteregex::Regex; +use happylock::ThreadKey; +use uuid::Uuid; + +use crate::pipe::{Message, MessageField, VirtualFile}; + +static OPEN_BUFFERS: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +#[derive(Default)] +struct ProgramState { + open_files: HashMap, + most_recent_file: Option, +} + +struct FileBuffer { + current_line_number: usize, + file: Option>, + lines: Vec, +} + +#[derive(Debug, Default, Clone)] +struct LineBuffer { + labels: Vec, + content: String, +} + +impl ProgramState { + fn next_buffer_id(&mut self) -> usize { + let mut id = self.open_files.len(); + while self.open_files.contains_key(&id) { + id = id.wrapping_add(self.open_files.len()); + } + + self.most_recent_file = Some(id); + id + } + + fn buffer_mut(&mut self, buffer: Option) -> Result<&mut FileBuffer, Rc> { + let buffer_idx = buffer.or(self.most_recent_file).ok_or("no open file")?; + let buffer = self + .open_files + .get_mut(&buffer_idx) + .ok_or("invalid buffer id")?; + + self.most_recent_file = Some(buffer_idx); + Ok(buffer) + } + + fn close_buffer(&mut self, buffer: Option) -> Result<(), Rc> { + let buffer = buffer.or(self.most_recent_file).ok_or("no open file")?; + self.open_files.remove(&buffer).ok_or("invalid buffer id")?; + Ok(()) + } +} + +impl FileBuffer { + fn new() -> Self { + Self { + lines: vec![LineBuffer::new()], + current_line_number: 0, + file: None, + } + } + + fn open(mut file: Box) -> std::io::Result { + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + let lines = buf.split('\n'); + + Ok(Self { + lines: lines + .map(|line| -> Result { + Ok(LineBuffer { + labels: Vec::new(), + content: line.to_string(), + }) + }) + .collect::>()?, + file: Some(file), + current_line_number: 0, + }) + } + + fn write(&mut self) -> Result<(), Rc> { + let Some(file) = self.file.as_deref_mut() else { + return Err("A file must be opened first".into()); + }; + + file.write_all( + self.lines + .iter() + .fold( + String::with_capacity( + self.lines.iter().map(|line| line.content.len() + 1).sum(), + ), + |mut output, line| { + output.push('\n'); + output.push_str(&line.content); + output + }, + ) + .as_bytes(), + ) + .map_err(|e| e.to_string().into()) + } + + fn get_line_index(&self, index: &LineIndex) -> Result> { + match index { + LineIndex::Current => Ok(self.current_line_number), + LineIndex::Last => (!self.lines.is_empty()) + .then_some(self.lines.len() - 1) + .ok_or("file is empty".into()), + LineIndex::Number(i) => usize::try_from(*i).map_err(|e| e.to_string().into()), + LineIndex::CurrentPlus(amount) => { + TryInto::::try_into(self.current_line_number as i32 + amount) + .map_err(|e| e.to_string().into()) + } + LineIndex::Bookmark(label) => self + .lines + .iter() + .enumerate() + .find(|(_i, line)| line.labels.contains(label)) + .map(|(i, _line)| i) + .ok_or(format!("bookmark with label \"{label}\" does not exist").into()), + LineIndex::FirstRegex(regex) => self + .lines + .iter() + .enumerate() + .skip(self.current_line_number + 1) + .chain( + self.lines + .iter() + .enumerate() + .take(self.current_line_number + 1), + ) + .find(|(_i, line)| regex.is_match(&line.content)) + .map(|(i, _line)| i) + .ok_or("no match found".to_string().into()), + LineIndex::LastRegex(regex) => self + .lines + .iter() + .enumerate() + .take(self.current_line_number) + .chain(self.lines.iter().enumerate().skip(self.current_line_number)) + .rev() + .find(|(_i, line)| regex.is_match(&line.content)) + .map(|(i, _line)| i) + .ok_or("no match found".into()), + } + } + + fn get_line_range(&self, range: &LineRange) -> Result, Rc> { + Ok(self.get_line_index(&range.start)?..self.get_line_index(&range.end)?) + } + + fn set_file(&mut self, file: Box) { + self.file = Some(file); + } + + fn current_line_number(&self) -> usize { + self.current_line_number + } + + fn set_current_line(&mut self, index: usize) { + self.current_line_number = index; + } + + fn add_at(&mut self, index: usize, content: impl AsRef) -> usize { + let new_lines = LineBuffer::from_str(content.as_ref()).collect::>(); + let new_lines_len = new_lines.len(); + self.lines.splice(index..index, new_lines); + + new_lines_len + } + + fn find(&self, range: Range, regex: &Regex) -> impl Iterator { + self.lines[range] + .iter() + .enumerate() + .filter_map(|(i, line)| regex.is_match(&line.content).then_some(i)) + } + + fn replace_all(&mut self, range: Range, pattern: &Regex, replacement: &str) { + for line in &mut self.lines[range] { + line.replace_all(pattern, replacement); + } + } + + fn append(&mut self, index: usize, content: impl AsRef) { + let lines_added = self.add_at(index + 1, content); + self.current_line_number = usize::max(index + lines_added, self.lines.len()); + } + + fn change(&mut self, range: Range, replacement: impl AsRef) { + self.delete(range.clone()); + self.insert(range.start, replacement); + } + + fn delete(&mut self, range: Range) -> Drain<'_, LineBuffer> { + self.current_line_number = usize::max(range.start, self.lines.len() - range.len()); + self.lines.drain(range.clone()) + } + + fn insert(&mut self, index: usize, content: impl AsRef) { + let lines_added = self.add_at(index, content); + self.current_line_number = index + lines_added - 1; + } + + fn join(&mut self, range: Range) { + let deleted_lines = self.delete(range.clone()); + let line = deleted_lines.fold( + LineBuffer { + labels: Vec::new(), + content: String::new(), + }, + |mut acc, line| { + acc.labels.extend(line.labels); + acc.content.push_str(&line.content); + acc + }, + ); + + self.lines.insert(range.start, line); + + if range.len() > 1 { + self.current_line_number = range.start; + } + } + + fn bookmark(&mut self, index: usize, label: String) { + self.lines[index].add_label(label); + } + + fn copy(&mut self, range: Range, index: usize) { + let new_lines = self.lines[range.clone()] + .iter() + .cloned() + .map(|mut line| { + line.labels.clear(); + line + }) + .collect::>(); + self.lines.splice(index..index, new_lines); + self.current_line_number = index + range.len(); + } + + fn move_lines(&mut self, range: Range, index: usize) { + let lines = self.delete(range.clone()).collect::>(); + self.lines.splice(index..index, lines); + self.current_line_number = index + range.len(); + } + + fn read_lines(&self, range: Range) -> impl Iterator { + self.lines[range].iter().map(|line| line.content.as_str()) + } +} + +impl LineBuffer { + fn new() -> Self { + Self { + labels: Vec::new(), + content: String::new(), + } + } + + fn from_str(content: &str) -> impl Iterator { + content.split("\n").map(|line| Self { + labels: Vec::new(), + content: line.into(), + }) + } + + fn add_label(&mut self, label: String) { + self.labels.push(label) + } + + fn replace_all(&mut self, pattern: &Regex, replacement: &str) { + self.content = pattern.replace_all(&self.content, replacement).into_owned() + } +} + +enum LineIndex { + Current, + Last, + Number(u32), + CurrentPlus(i32), + Bookmark(String), + FirstRegex(Regex), + LastRegex(Regex), +} + +struct LineRange { + start: LineIndex, + end: LineIndex, +} + +enum Command { + Append { + position: LineIndex, + content: String, + buffer: Option, + }, + Change { + range: LineRange, + content: String, + buffer: Option, + }, + Delete { + range: LineRange, + buffer: Option, + }, + Open { + file: Box, + }, + New {}, + SetFile { + file: Box, + buffer: Option, + }, + Find { + range: LineRange, + regex: Regex, + buffer: Option, + }, + Insert { + position: LineIndex, + content: String, + buffer: Option, + }, + Join { + range: LineRange, + buffer: Option, + }, + Bookmark { + position: LineIndex, + name: String, + buffer: Option, + }, + Move { + range: LineRange, + position: LineIndex, + buffer: Option, + }, + Read { + range: LineRange, + buffer: Option, + }, + Substitute { + range: LineRange, + regex: Regex, + content: String, + buffer: Option, + }, + Copy { + range: LineRange, + position: LineIndex, + buffer: Option, + }, + Undo { + buffer: Option, + }, + Write { + buffer: Option, + }, + Close { + buffer: Option, + }, + CurrentLineNumber { + buffer: Option, + }, + Jump { + position: LineIndex, + buffer: Option, + }, +} + +fn parse_line_index(string: &str) -> Result> { + if string == "." { + Ok(LineIndex::Current) + } else if string == "$" { + Ok(LineIndex::Last) + } else if string.starts_with("+") || string.starts_with("-") { + Ok(LineIndex::CurrentPlus( + string.parse::().map_err(|e| e.to_string())?, + )) + } else if let Ok(number) = string.parse::() { + Ok(LineIndex::Number(number)) + } else if let Some(bookmark) = string.strip_prefix("'") { + Ok(LineIndex::Bookmark(bookmark.to_string())) + } else if let Some(regex) = string.strip_prefix("/") { + Ok(LineIndex::FirstRegex(Regex::new(regex)?)) + } else if let Some(regex) = string.strip_prefix("?") { + Ok(LineIndex::LastRegex(Regex::new(regex)?)) + } else { + Err("invalid line".into()) + } +} + +fn expect_field( + message: &mut impl Iterator, + error: &str, +) -> Result> { + match message.next() { + Some(field) => Ok(field), + None => Err(error.into()), + } +} + +fn field_to_string(field: &MessageField) -> Result<&str, Rc> { + let Some(field) = field.string() else { + return Err("command must be a string".into()); + }; + match field { + Ok(field) => Ok(field), + Err(error) => Err(error.to_string().into()), + } +} + +fn expect_position(message: &mut impl Iterator) -> Result> { + parse_line_index(field_to_string(&expect_field( + message, + "expected a line specifier", + )?)?) +} + +fn expect_range(message: &mut impl Iterator) -> Result> { + Ok(LineRange { + start: expect_position(message)?, + end: expect_position(message)?, + }) +} + +fn expect_content(message: &mut impl Iterator) -> Result> { + Ok(field_to_string(&expect_field(message, "expected a string argument")?)?.to_string()) +} + +fn expect_file( + message: &mut impl Iterator, +) -> Result, Rc> { + let file = expect_field(message, "expected a file")?; + match file { + MessageField::File(file) => Ok(file), + _ => Err("expected a file".into()), + } +} + +fn expect_regex(message: &mut impl Iterator) -> Result> { + let regex = expect_content(message)?; + Regex::new(®ex) +} + +fn expect_buffer( + message: &mut impl Iterator, +) -> Result, Rc> { + message + .next() + .map(|field| { + field_to_string(&field)? + .parse::() + .map_err(|e| e.to_string().into()) + }) + .transpose() +} + +fn parse_command(mut message: impl Iterator) -> Result> { + let command = expect_field(&mut message, "no command provided")?; + let command = field_to_string(&command)?; + + if command == "append" { + let position = expect_position(&mut message)?; + let content = expect_content(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Append { + position, + content, + buffer, + }) + } else if command == "change" { + let range = expect_range(&mut message)?; + let content = expect_content(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Change { + range, + content, + buffer, + }) + } else if command == "delete" { + let range = expect_range(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Delete { range, buffer }) + } else if command == "open" { + let file = expect_file(&mut message)?; + Ok(Command::Open { file }) + } else if command == "new" { + Ok(Command::New {}) + } else if command == "setfile" { + let file = expect_file(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::SetFile { file, buffer }) + } else if command == "find" { + let range = expect_range(&mut message)?; + let regex = expect_regex(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Find { + range, + regex, + buffer, + }) + } else if command == "insert" { + let position = expect_position(&mut message)?; + let content = expect_content(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Insert { + position, + content, + buffer, + }) + } else if command == "join" { + let range = expect_range(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Join { range, buffer }) + } else if command == "bookmark" { + let position = expect_position(&mut message)?; + let name = expect_content(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Bookmark { + position, + name, + buffer, + }) + } else if command == "move" { + let range = expect_range(&mut message)?; + let position = expect_position(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Move { + range, + position, + buffer, + }) + } else if command == "read" { + let range = expect_range(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Read { range, buffer }) + } else if command == "substitute" { + let range = expect_range(&mut message)?; + let regex = expect_regex(&mut message)?; + let content = expect_content(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Substitute { + range, + regex, + content, + buffer, + }) + } else if command == "copy" { + let range = expect_range(&mut message)?; + let position = expect_position(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Copy { + range, + position, + buffer, + }) + } else if command == "undo" { + let buffer = expect_buffer(&mut message)?; + Ok(Command::Undo { buffer }) + } else if command == "write" { + let buffer = expect_buffer(&mut message)?; + Ok(Command::Write { buffer }) + } else if command == "close" { + let buffer = expect_buffer(&mut message)?; + Ok(Command::Close { buffer }) + } else if command == "linenumber" { + let buffer = expect_buffer(&mut message)?; + Ok(Command::CurrentLineNumber { buffer }) + } else if command == "jump" { + let position = expect_position(&mut message)?; + let buffer = expect_buffer(&mut message)?; + Ok(Command::Jump { position, buffer }) + } else { + Err(format!("{command} is not a valid command").into()) + } +} + +enum CommandResponse { + Content(Box<[String]>), + Number(usize), + LineNumbers(Box<[usize]>), + Empty, +} + +fn run_command( + command: Command, + program_state: &mut ProgramState, +) -> Result> { + match command { + Command::Append { + position, + content, + buffer, + } => { + let buffer = program_state.buffer_mut(buffer)?; + let position = buffer.get_line_index(&position)?; + buffer.append(position, content); + Ok(CommandResponse::Empty) + } + Command::Change { + range, + content, + buffer, + } => { + let buffer = program_state.buffer_mut(buffer)?; + let range = buffer.get_line_range(&range)?; + buffer.change(range, content); + Ok(CommandResponse::Empty) + } + Command::Delete { range, buffer } => { + let buffer = program_state.buffer_mut(buffer)?; + let range = buffer.get_line_range(&range)?; + buffer.delete(range); + Ok(CommandResponse::Empty) + } + Command::Open { file } => { + let id = program_state.next_buffer_id(); + let buffer = FileBuffer::open(file).map_err(|e| e.to_string())?; + program_state.open_files.insert(id, buffer); + Ok(CommandResponse::Number(id)) + } + Command::New {} => { + let id = program_state.next_buffer_id(); + let buffer = FileBuffer::new(); + program_state.open_files.insert(id, buffer); + Ok(CommandResponse::Number(id)) + } + Command::SetFile { file, buffer } => { + let buffer = program_state.buffer_mut(buffer)?; + buffer.set_file(file); + Ok(CommandResponse::Empty) + } + Command::Find { + range, + regex, + buffer, + } => { + let buffer = program_state.buffer_mut(buffer)?; + let range = buffer.get_line_range(&range)?; + let matches = buffer.find(range, ®ex); + Ok(CommandResponse::LineNumbers(matches.collect())) + } + Command::Insert { + position, + content, + buffer, + } => { + let buffer = program_state.buffer_mut(buffer)?; + let position = buffer.get_line_index(&position)?; + buffer.insert(position, content); + Ok(CommandResponse::Empty) + } + Command::Join { range, buffer } => { + let buffer = program_state.buffer_mut(buffer)?; + let range = buffer.get_line_range(&range)?; + buffer.join(range); + Ok(CommandResponse::Empty) + } + Command::Bookmark { + position, + name, + buffer, + } => { + let buffer = program_state.buffer_mut(buffer)?; + let position = buffer.get_line_index(&position)?; + buffer.bookmark(position, name); + Ok(CommandResponse::Empty) + } + Command::Move { + range, + position, + buffer, + } => { + let buffer = program_state.buffer_mut(buffer)?; + let range = buffer.get_line_range(&range)?; + let position = buffer.get_line_index(&position)?; + + if range.contains(&position) { + return Err("the range of moved lines contains the new position".into()); + } + + buffer.move_lines(range, position); + Ok(CommandResponse::Empty) + } + Command::Read { range, buffer } => { + let buffer = program_state.buffer_mut(buffer)?; + let range = buffer.get_line_range(&range)?; + Ok(CommandResponse::Content( + buffer.read_lines(range).map(ToOwned::to_owned).collect(), + )) + } + Command::Substitute { + range, + regex, + content, + buffer, + } => { + let buffer = program_state.buffer_mut(buffer)?; + let range = buffer.get_line_range(&range)?; + buffer.replace_all(range, ®ex, &content); + Ok(CommandResponse::Empty) + } + Command::Copy { + range, + position, + buffer, + } => { + let buffer = program_state.buffer_mut(buffer)?; + let range = buffer.get_line_range(&range)?; + let position = buffer.get_line_index(&position)?; + buffer.copy(range, position); + Ok(CommandResponse::Empty) + } + Command::Undo { .. } => Err("Undo is not yet supported".into()), + Command::Write { buffer } => { + let buffer = program_state.buffer_mut(buffer)?; + buffer.write().map_err(|e| e.to_string())?; + Ok(CommandResponse::Empty) + } + Command::Close { buffer } => { + program_state.close_buffer(buffer)?; + Ok(CommandResponse::Empty) + } + Command::CurrentLineNumber { buffer } => { + let buffer = program_state.buffer_mut(buffer)?; + Ok(CommandResponse::Number(buffer.current_line_number())) + } + Command::Jump { position, buffer } => { + let buffer = program_state.buffer_mut(buffer)?; + let position = buffer.get_line_index(&position)?; + buffer.set_current_line(position); + Ok(CommandResponse::Empty) + } + } +} + +pub fn dit(mut key: ThreadKey, channel: Receiver) { + for message in channel { + let sending_program = message.sending_program; + let message_fields = message.fields.into_iter(); + let return_space = message.return_space; + + let mut buffers = OPEN_BUFFERS.write().unwrap(); + let program_state = buffers.entry(sending_program).or_default(); + let command = parse_command(message_fields); + let response = command.and_then(|command| run_command(command, program_state)); + match response { + Ok(CommandResponse::Empty) => return_space.respond_ok(MessageField::Empty), + Ok(CommandResponse::Content(string)) => { + return_space.respond_ok(MessageField::from(string.join("\n").as_str())) + } + Ok(CommandResponse::Number(number)) => { + return_space.respond_ok(MessageField::from(number.to_string().as_str())); + } + Ok(CommandResponse::LineNumbers(numbers)) => { + return_space.respond_ok(MessageField::from( + numbers + .iter() + .fold(String::new(), |mut output, num| { + output.push_str(&num.to_string()); + output.push(','); + output + }) + .as_str(), + )) + } + Err(error) => return_space.respond_err(NonZeroU8::MIN, MessageField::from(&*error)), + } + } +} -- cgit v1.2.3