summaryrefslogtreecommitdiff
path: root/src/builtins/dit.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/builtins/dit.rs')
-rw-r--r--src/builtins/dit.rs788
1 files changed, 788 insertions, 0 deletions
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<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)),
+ }
+ }
+}