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<RwLock<HashMap<Uuid, ProgramState>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
#[derive(Default)]
struct ProgramState {
open_files: HashMap<usize, FileBuffer>,
most_recent_file: Option<usize>,
}
struct FileBuffer {
current_line_number: usize,
file: Option<Box<dyn VirtualFile>>,
lines: Vec<LineBuffer>,
}
#[derive(Debug, Default, Clone)]
struct LineBuffer {
labels: Vec<String>,
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<usize>) -> Result<&mut FileBuffer, Rc<str>> {
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<usize>) -> Result<(), Rc<str>> {
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<dyn VirtualFile>) -> std::io::Result<Self> {
let mut buf = String::new();
file.read_to_string(&mut buf)?;
let lines = buf.split('\n');
Ok(Self {
lines: lines
.map(|line| -> Result<LineBuffer, std::io::Error> {
Ok(LineBuffer {
labels: Vec::new(),
content: line.to_string(),
})
})
.collect::<Result<_, _>>()?,
file: Some(file),
current_line_number: 0,
})
}
fn write(&mut self) -> Result<(), Rc<str>> {
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<usize, Rc<str>> {
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::<usize>::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<Range<usize>, Rc<str>> {
Ok(self.get_line_index(&range.start)?..self.get_line_index(&range.end)?)
}
fn set_file(&mut self, file: Box<dyn VirtualFile>) {
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<str>) -> usize {
let new_lines = LineBuffer::from_str(content.as_ref()).collect::<Vec<_>>();
let new_lines_len = new_lines.len();
self.lines.splice(index..index, new_lines);
new_lines_len
}
fn find(&self, range: Range<usize>, regex: &Regex) -> impl Iterator<Item = usize> {
self.lines[range]
.iter()
.enumerate()
.filter_map(|(i, line)| regex.is_match(&line.content).then_some(i))
}
fn replace_all(&mut self, range: Range<usize>, 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<str>) {
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<usize>, replacement: impl AsRef<str>) {
self.delete(range.clone());
self.insert(range.start, replacement);
}
fn delete(&mut self, range: Range<usize>) -> 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<str>) {
let lines_added = self.add_at(index, content);
self.current_line_number = index + lines_added - 1;
}
fn join(&mut self, range: Range<usize>) {
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<usize>, index: usize) {
let new_lines = self.lines[range.clone()]
.iter()
.cloned()
.map(|mut line| {
line.labels.clear();
line
})
.collect::<Vec<_>>();
self.lines.splice(index..index, new_lines);
self.current_line_number = index + range.len();
}
fn move_lines(&mut self, range: Range<usize>, index: usize) {
let lines = self.delete(range.clone()).collect::<Vec<_>>();
self.lines.splice(index..index, lines);
self.current_line_number = index + range.len();
}
fn read_lines(&self, range: Range<usize>) -> impl Iterator<Item = &str> {
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<Item = Self> {
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<usize>,
},
Change {
range: LineRange,
content: String,
buffer: Option<usize>,
},
Delete {
range: LineRange,
buffer: Option<usize>,
},
Open {
file: Box<dyn VirtualFile>,
},
New {},
SetFile {
file: Box<dyn VirtualFile>,
buffer: Option<usize>,
},
Find {
range: LineRange,
regex: Regex,
buffer: Option<usize>,
},
Insert {
position: LineIndex,
content: String,
buffer: Option<usize>,
},
Join {
range: LineRange,
buffer: Option<usize>,
},
Bookmark {
position: LineIndex,
name: String,
buffer: Option<usize>,
},
Move {
range: LineRange,
position: LineIndex,
buffer: Option<usize>,
},
Read {
range: LineRange,
buffer: Option<usize>,
},
Substitute {
range: LineRange,
regex: Regex,
content: String,
buffer: Option<usize>,
},
Copy {
range: LineRange,
position: LineIndex,
buffer: Option<usize>,
},
Undo {
buffer: Option<usize>,
},
Write {
buffer: Option<usize>,
},
Close {
buffer: Option<usize>,
},
CurrentLineNumber {
buffer: Option<usize>,
},
Jump {
position: LineIndex,
buffer: Option<usize>,
},
}
fn parse_line_index(string: &str) -> Result<LineIndex, Rc<str>> {
if string == "." {
Ok(LineIndex::Current)
} else if string == "$" {
Ok(LineIndex::Last)
} else if string.starts_with("+") || string.starts_with("-") {
Ok(LineIndex::CurrentPlus(
string.parse::<i32>().map_err(|e| e.to_string())?,
))
} else if let Ok(number) = string.parse::<u32>() {
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<Item = MessageField>,
error: &str,
) -> Result<MessageField, Rc<str>> {
match message.next() {
Some(field) => Ok(field),
None => Err(error.into()),
}
}
fn field_to_string(field: &MessageField) -> Result<&str, Rc<str>> {
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<Item = MessageField>) -> Result<LineIndex, Rc<str>> {
parse_line_index(field_to_string(&expect_field(
message,
"expected a line specifier",
)?)?)
}
fn expect_range(message: &mut impl Iterator<Item = MessageField>) -> Result<LineRange, Rc<str>> {
Ok(LineRange {
start: expect_position(message)?,
end: expect_position(message)?,
})
}
fn expect_content(message: &mut impl Iterator<Item = MessageField>) -> Result<String, Rc<str>> {
Ok(field_to_string(&expect_field(message, "expected a string argument")?)?.to_string())
}
fn expect_file(
message: &mut impl Iterator<Item = MessageField>,
) -> Result<Box<dyn VirtualFile>, Rc<str>> {
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<Item = MessageField>) -> Result<Regex, Rc<str>> {
let regex = expect_content(message)?;
Regex::new(®ex)
}
fn expect_buffer(
message: &mut impl Iterator<Item = MessageField>,
) -> Result<Option<usize>, Rc<str>> {
message
.next()
.map(|field| {
field_to_string(&field)?
.parse::<usize>()
.map_err(|e| e.to_string().into())
})
.transpose()
}
fn parse_command(mut message: impl Iterator<Item = MessageField>) -> Result<Command, Rc<str>> {
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<CommandResponse, Rc<str>> {
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<Message>) {
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)),
}
}
}
|