chore 0.1.0

plain-text command-line task management utility
Documentation
use crate::field::End;
use crate::field::Entry;
use crate::field::Priority;
use crate::field::{Key, Pair, Value};
use crate::taskiter::TaskIter;
use crate::token::Token;
use std::ops::Range;

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Task<'a>(&'a str);

#[derive(Clone, Debug)]
pub struct TaskBuf(String);

impl<'a> Task<'a> {
    pub fn new(str: &'a str) -> Self {
        Task(str)
    }

    pub fn iter(&self) -> TaskIter {
        TaskIter::new(&self.0)
    }

    pub fn into_iter(self) -> TaskIter<'a> {
        TaskIter::new(self.0)
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn into_str(self) -> &'a str {
        self.0
    }

    pub fn is_completed(&self) -> bool {
        self.find_marker().is_some()
    }

    pub fn get_end(&self) -> Option<End> {
        match self.find_end() {
            Some((end, _)) => Some(end),
            _ => None,
        }
    }

    pub fn get_priority(&self) -> Option<Priority> {
        match self.find_priority() {
            Some((pri, _)) => Some(pri),
            _ => None,
        }
    }

    pub fn get_entry(&self) -> Option<Entry> {
        match self.find_entry() {
            Some((entry, _)) => Some(entry),
            _ => None,
        }
    }

    pub fn has_token(&self, token: &Token) -> bool {
        self.iter().any(|(t, _)| &t == token)
    }

    pub fn get_value(&self, key: &Key) -> Option<Value> {
        match self.find_pair(key) {
            Some((pair, _)) => Some(pair.value),
            _ => None,
        }
    }

    fn find_marker(&self) -> Option<Range<usize>> {
        self.iter().take(1).find_map(|(token, range)| match token {
            Token::Marker(_) => Some(range),
            _ => None,
        })
    }

    fn find_end(&self) -> Option<(End, Range<usize>)> {
        self.iter().take(3).find_map(|(token, range)| match token {
            Token::End(end) => Some((end, range)),
            _ => None,
        })
    }

    fn find_priority(&self) -> Option<(Priority, Range<usize>)> {
        self.iter().take(5).find_map(|(token, range)| match token {
            Token::Priority(pri) => Some((pri, range)),
            _ => None,
        })
    }

    fn find_entry(&self) -> Option<(Entry, Range<usize>)> {
        self.iter().take(7).find_map(|(token, range)| match token {
            Token::Entry(entry) => Some((entry, range)),
            _ => None,
        })
    }

    fn find_pair(&self, key: &Key) -> Option<(Pair, Range<usize>)> {
        for (token, range) in self.iter() {
            if let Token::Pair(pair) = token {
                if pair.key == *key {
                    return Some((pair, range));
                }
            }
        }
        None
    }

    fn find_body_start(&self) -> usize {
        self.iter()
            .find_map(|(token, range)| match token {
                Token::Marker(_)
                | Token::End(_)
                | Token::Priority(_)
                | Token::Entry(_)
                | Token::Space(_) => None,
                _ => Some(range.start),
            })
            .unwrap_or_else(|| self.0.len())
    }
}

impl TaskBuf {
    pub fn new(str: String) -> Self {
        TaskBuf(str)
    }

    pub fn as_task(&self) -> Task {
        Task::new(&self.0)
    }

    pub fn into_string(self) -> String {
        self.0
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn set_to(&mut self, task: &Task) {
        self.0.replace_range(.., task.as_str())
    }

    pub fn append_text(&mut self, text: &str) {
        if self.0.ends_with(|c: char| c.is_ascii_whitespace()) {
            if text.chars().all(|c: char| c.is_ascii_whitespace()) {
                return;
            }
        } else if !self.0.is_empty() && text.starts_with(|c: char| !c.is_ascii_whitespace()) {
            self.0.push(' ')
        }
        self.0.push_str(text);
    }

    pub fn clear_body(&mut self) {
        self.0.replace_range(self.as_task().find_body_start().., "");
    }

    pub fn remove_token(&mut self, token: &Token) {
        if let Some((_, range)) = self.as_task().iter().find(|(t, _)| t == token) {
            self.remove_range_with_whitespace(range);
        }
    }

    pub fn remove_pair(&mut self, key: &Key) {
        if let Some((_, range)) = self.as_task().find_pair(key) {
            self.remove_range_with_whitespace(range)
        }
    }

    pub fn set_value(&mut self, key: &Key, value: &Value) {
        match self.as_task().find_pair(key) {
            Some((_, range)) => self
                .0
                .replace_range(range.start + key.len() + 1..range.end, value.as_str()),
            None => {
                if self.0.ends_with(|c: char| !c.is_ascii_whitespace()) {
                    self.0.push(' ');
                }
                self.0.push_str(key.as_str());
                self.0.push(':');
                self.0.push_str(value.as_str())
            }
        }
    }

    pub fn set_pending(&mut self) {
        if let Some(range) = self.as_task().find_marker() {
            self.remove_range_with_whitespace(range)
        }
    }

    pub fn set_completed(&mut self) {
        match self {
            _ if self.as_task().is_completed() => {}
            _ if self.0.is_empty() => self.0.insert(0, 'x'),
            _ if self.0.starts_with(|c: char| c.is_ascii_whitespace()) => self.0.insert(0, 'x'),
            _ => self.0.insert_str(0, "x "),
        }
    }

    pub fn set_end(&mut self, end: Option<End>) {
        let existing_range = match self.as_task().find_end() {
            Some((_, range)) => Some(range),
            _ => None,
        };

        match (end, existing_range) {
            (Some(new_end), Some(existing_range)) => {
                self.0.replace_range(existing_range, new_end.as_str());
            }
            (Some(new_end), None) => {
                if let Some(range) = self.as_task().find_marker() {
                    self.0.insert_str(range.end, new_end.as_str());
                    self.0.insert(range.end, ' ');
                }
            }
            (None, Some(existing_range)) => {
                self.remove_range_with_whitespace(existing_range);
            }
            (None, None) => {}
        }
    }

    pub fn set_priority(&mut self, pri: Option<Priority>) {
        let existing_range = match self.as_task().find_priority() {
            Some((_, range)) => Some(range),
            _ => None,
        };

        match (pri, existing_range) {
            (Some(new_pri), Some(existing_range)) => {
                let buf: [u8; 3] = [b'(', new_pri.as_u8(), b')'];
                let new_pri = unsafe { std::str::from_utf8_unchecked(&buf) };
                self.0.replace_range(existing_range, new_pri);
            }
            (Some(new_pri), None) => {
                let buf: [u8; 3] = [b'(', new_pri.as_u8(), b')'];
                let new_pri = unsafe { std::str::from_utf8_unchecked(&buf) };
                if let Some((_, entry_range)) = self.as_task().find_entry() {
                    self.0.insert(entry_range.start, ' ');
                    self.0.insert_str(entry_range.start, new_pri);
                } else {
                    let body_start = self.as_task().find_body_start();
                    if self.0.len() != body_start {
                        self.0.insert(body_start, ' ');
                    }
                    self.0.insert_str(body_start, new_pri);
                }
            }
            (None, Some(existing_range)) => {
                self.remove_range_with_whitespace(existing_range);
            }
            (None, None) => {}
        }
    }

    pub fn set_entry(&mut self, entry: Option<Entry>) {
        let existing_range = match self.as_task().find_entry() {
            Some((_, range)) => Some(range),
            _ => None,
        };

        match (entry, existing_range) {
            (Some(new_entry), Some(existing_range)) => {
                self.0.replace_range(existing_range, new_entry.as_str());
            }
            (Some(new_entry), None) => {
                let body_start = self.as_task().find_body_start();
                if self.0.len() != body_start {
                    self.0.insert(body_start, ' ');
                }
                self.0.insert_str(body_start, new_entry.as_str());
            }
            (None, Some(existing_range)) => {
                self.remove_range_with_whitespace(existing_range);
            }
            (None, None) => {}
        }
    }

    fn remove_range_with_whitespace(&mut self, mut range: Range<usize>) {
        if let Some(trailing_space) = self
            .0
            .get(range.end..)
            .and_then(|str| str.split(|c: char| !c.is_ascii_whitespace()).next())
            .filter(|str| !str.is_empty())
        {
            range.end += trailing_space.len();
        } else if let Some(preceeding_space) = self
            .0
            .get(..range.start)
            .and_then(|str| str.rsplit(|c: char| !c.is_ascii_whitespace()).next())
            .filter(|str| !str.is_empty())
        {
            range.start -= preceeding_space.len();
        }
        self.0.replace_range(range, "");
    }
}