summaryrefslogtreecommitdiff
path: root/src/builtins/dit.rs
blob: d6b1feb5d0b9c8bcf2fce6f519270b381283a97c (plain)
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(&regex)
}

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, &regex);
			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, &regex, &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)),
		}
	}
}