feat: removed unsafe code from parser
This commit is contained in:
Generated
+446
-483
File diff suppressed because it is too large
Load Diff
+30
-41
@@ -6,19 +6,16 @@ use crate::todo::editor::TodoEditor;
|
|||||||
use cosmic::app::context_drawer;
|
use cosmic::app::context_drawer;
|
||||||
use cosmic::cosmic_config::{self, ConfigSet, CosmicConfigEntry};
|
use cosmic::cosmic_config::{self, ConfigSet, CosmicConfigEntry};
|
||||||
use cosmic::iced::alignment::{Horizontal, Vertical};
|
use cosmic::iced::alignment::{Horizontal, Vertical};
|
||||||
use cosmic::iced::futures::channel::mpsc::Sender;
|
use cosmic::iced::{Alignment, Length, Subscription};
|
||||||
use cosmic::iced::{Alignment, Length, Subscription, stream};
|
|
||||||
use cosmic::prelude::*;
|
use cosmic::prelude::*;
|
||||||
use cosmic::widget::text_editor::{self, Content};
|
use cosmic::widget::text_editor;
|
||||||
use cosmic::widget::{self, about::About, menu};
|
use cosmic::widget::{self, about::About, menu};
|
||||||
use futures_util::SinkExt;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path;
|
use std::path;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use cosmic_files::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings};
|
use cosmic_files::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings};
|
||||||
|
|
||||||
use crate::todo::parser::Todo;
|
use crate::todo::parser::{Todo, TodoError};
|
||||||
|
|
||||||
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
|
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
|
||||||
const APP_ICON: &[u8] = include_bytes!("../resources/icons/hicolor/scalable/apps/icon.svg");
|
const APP_ICON: &[u8] = include_bytes!("../resources/icons/hicolor/scalable/apps/icon.svg");
|
||||||
@@ -39,8 +36,6 @@ pub struct AppModel {
|
|||||||
config: Config,
|
config: Config,
|
||||||
/// Time active
|
/// Time active
|
||||||
time: u32,
|
time: u32,
|
||||||
/// Toggle the watch subscription
|
|
||||||
watch_is_active: bool,
|
|
||||||
/// Current Todo data
|
/// Current Todo data
|
||||||
todo: Option<Todo>,
|
todo: Option<Todo>,
|
||||||
editor: TodoEditor,
|
editor: TodoEditor,
|
||||||
@@ -55,8 +50,10 @@ pub enum Message {
|
|||||||
UpdateConfig(Config),
|
UpdateConfig(Config),
|
||||||
WatchTick(u32),
|
WatchTick(u32),
|
||||||
OpenFileDialog,
|
OpenFileDialog,
|
||||||
DialogMessage(DialogMessage),
|
Dialog(Box<DialogMessage>),
|
||||||
OpenFileResult(DialogResult),
|
OpenFileResult(DialogResult),
|
||||||
|
TodoLoaded(Todo),
|
||||||
|
TodoLoadError(TodoError),
|
||||||
Edit(text_editor::Action, usize),
|
Edit(text_editor::Action, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +209,7 @@ impl cosmic::Application for AppModel {
|
|||||||
/// indefinitely.
|
/// indefinitely.
|
||||||
fn subscription(&self) -> Subscription<Self::Message> {
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
// Add subscriptions which are always active.
|
// Add subscriptions which are always active.
|
||||||
let mut subscriptions = vec![
|
let subscriptions = vec![
|
||||||
// Watch for application configuration changes.
|
// Watch for application configuration changes.
|
||||||
self.core()
|
self.core()
|
||||||
.watch_config::<Config>(Self::APP_ID)
|
.watch_config::<Config>(Self::APP_ID)
|
||||||
@@ -225,22 +222,6 @@ impl cosmic::Application for AppModel {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Conditionally enables a timer that emits a message every second.
|
|
||||||
if self.watch_is_active {
|
|
||||||
subscriptions.push(Subscription::run(|| {
|
|
||||||
stream::channel(1, |mut emitter: Sender<Message>| async move {
|
|
||||||
let mut time = 1;
|
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(1));
|
|
||||||
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
_ = emitter.send(Message::WatchTick(time)).await;
|
|
||||||
time += 1;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Subscription::batch(subscriptions)
|
Subscription::batch(subscriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,21 +257,21 @@ impl cosmic::Application for AppModel {
|
|||||||
if self.dialog_opt.is_none() {
|
if self.dialog_opt.is_none() {
|
||||||
let (dialog, command) = Dialog::new(
|
let (dialog, command) = Dialog::new(
|
||||||
DialogSettings::new().kind(DialogKind::OpenFile),
|
DialogSettings::new().kind(DialogKind::OpenFile),
|
||||||
Message::DialogMessage,
|
|msg| Message::Dialog(Box::new(msg)),
|
||||||
Message::OpenFileResult,
|
Message::OpenFileResult,
|
||||||
);
|
);
|
||||||
self.dialog_opt = Some(dialog);
|
self.dialog_opt = Some(dialog);
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::DialogMessage(dialog_message) => {
|
Message::Dialog(dialog_message) => {
|
||||||
if let Some(dialog) = &mut self.dialog_opt {
|
if let Some(dialog) = &mut self.dialog_opt {
|
||||||
return dialog.update(dialog_message);
|
return dialog.update(*dialog_message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::OpenFileResult(dialog_result) => match dialog_result {
|
Message::OpenFileResult(dialog_result) => match dialog_result {
|
||||||
DialogResult::Open(paths) => {
|
DialogResult::Open(paths) => {
|
||||||
for path in paths {
|
if let Some(path) = paths.into_iter().next() {
|
||||||
let cpath = match std::fs::canonicalize(&path) {
|
let cpath = match std::fs::canonicalize(&path) {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(err) => match path::absolute(&path) {
|
Err(err) => match path::absolute(&path) {
|
||||||
@@ -303,27 +284,35 @@ impl cosmic::Application for AppModel {
|
|||||||
};
|
};
|
||||||
let mut todo = Todo::new(cpath.clone());
|
let mut todo = Todo::new(cpath.clone());
|
||||||
|
|
||||||
if let Some(cfg) = &self.config_handler {
|
if let Some(cfg) = &self.config_handler
|
||||||
if let Err(err) = cfg.set("current_file", Some(cpath)) {
|
&& let Err(err) = cfg.set("current_file", Some(cpath))
|
||||||
log::error!("failed to save config {:?}", err);
|
{
|
||||||
}
|
log::error!("failed to save config {:?}", err);
|
||||||
}
|
|
||||||
// self.config.current_file = Some(cpath);
|
|
||||||
// Todo: this might need to be more async
|
|
||||||
if let Err(err) = todo.load() {
|
|
||||||
log::error!("failed to load file {:?} {:?}", path, err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.todo = Some(todo);
|
return cosmic::task::future(async move {
|
||||||
|
if let Err(err) = todo.async_load().await {
|
||||||
|
Message::TodoLoadError(err)
|
||||||
|
} else {
|
||||||
|
Message::TodoLoaded(todo)
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DialogResult::Cancel => todo!(),
|
DialogResult::Cancel => {}
|
||||||
},
|
},
|
||||||
Message::Edit(action, line) => {
|
Message::Edit(action, line) => {
|
||||||
if let Err(err) = self.editor.edit(action, line) {
|
if let Err(err) = self.editor.edit(action, line) {
|
||||||
log::error!("error in update: {:?}", err);
|
log::error!("error in update: {:?}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::TodoLoaded(todo) => {
|
||||||
|
log::info!("Loaded {} todos", todo.todos.len());
|
||||||
|
self.todo = Some(todo);
|
||||||
|
}
|
||||||
|
Message::TodoLoadError(todo_error) => {
|
||||||
|
log::error!("Error loading todo: {:?}", todo_error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -98,7 +98,7 @@ where
|
|||||||
.spacing(cosmic::theme::spacing().space_s)
|
.spacing(cosmic::theme::spacing().space_s)
|
||||||
.align_x(Alignment::Center)
|
.align_x(Alignment::Center)
|
||||||
.width(Length::Fill);
|
.width(Length::Fill);
|
||||||
for (i, line) in (&value.lines).iter().enumerate() {
|
for (i, line) in (value.lines).iter().enumerate() {
|
||||||
col = col.push(
|
col = col.push(
|
||||||
TextEditor::new(&line.content)
|
TextEditor::new(&line.content)
|
||||||
.on_action(move |action| Message::Edit(action, i))
|
.on_action(move |action| Message::Edit(action, i))
|
||||||
|
|||||||
+11
-8
@@ -19,7 +19,7 @@ static TODOSYNTAX: LazyLock<parsing::SyntaxSet> = LazyLock::new(|| {
|
|||||||
builder.build()
|
builder.build()
|
||||||
});
|
});
|
||||||
|
|
||||||
static TODOTHEME: LazyLock<highlighting::Theme> = LazyLock::new(|| get_current_theme());
|
static TODOTHEME: LazyLock<highlighting::Theme> = LazyLock::new(get_current_theme);
|
||||||
|
|
||||||
pub struct TodoHighlighter {
|
pub struct TodoHighlighter {
|
||||||
syntax: &'static parsing::SyntaxReference,
|
syntax: &'static parsing::SyntaxReference,
|
||||||
@@ -44,8 +44,8 @@ impl TodoHighlightSettings {
|
|||||||
let mut parser = parsing::ParseState::new(syntax);
|
let mut parser = parsing::ParseState::new(syntax);
|
||||||
parser
|
parser
|
||||||
.parse_line(
|
.parse_line(
|
||||||
"", // Zero-width space unicode to force parser into desc context
|
"\u{200B}", // Zero-width space unicode to force parser into desc context
|
||||||
&*TODOSYNTAX,
|
&TODOSYNTAX,
|
||||||
)
|
)
|
||||||
.expect("init static parser from TodoHighlightSettings");
|
.expect("init static parser from TodoHighlightSettings");
|
||||||
parser
|
parser
|
||||||
@@ -54,8 +54,8 @@ impl TodoHighlightSettings {
|
|||||||
let mut parser = parsing::ParseState::new(syntax);
|
let mut parser = parsing::ParseState::new(syntax);
|
||||||
parser
|
parser
|
||||||
.parse_line(
|
.parse_line(
|
||||||
"x ", // done header + Zero-width space unicode to force parser into desc context
|
"x \u{200B}", // done header + Zero-width space unicode to force parser into desc context
|
||||||
&*TODOSYNTAX,
|
&TODOSYNTAX,
|
||||||
)
|
)
|
||||||
.expect("init static parser from TodoHighlightSettings");
|
.expect("init static parser from TodoHighlightSettings");
|
||||||
parser
|
parser
|
||||||
@@ -84,8 +84,11 @@ impl Highlighter for TodoHighlighter {
|
|||||||
type Iterator<'a> = Box<dyn Iterator<Item = (std::ops::Range<usize>, Self::Highlight)> + 'a>;
|
type Iterator<'a> = Box<dyn Iterator<Item = (std::ops::Range<usize>, Self::Highlight)> + 'a>;
|
||||||
|
|
||||||
fn new(settings: &Self::Settings) -> Self {
|
fn new(settings: &Self::Settings) -> Self {
|
||||||
let syntax = TODOSYNTAX.syntaxes().get(0).expect("Todo Syntax Reference");
|
let syntax = TODOSYNTAX
|
||||||
let highlighter = highlighting::Highlighter::new(&*TODOTHEME);
|
.syntaxes()
|
||||||
|
.first()
|
||||||
|
.expect("Todo Syntax Reference");
|
||||||
|
let highlighter = highlighting::Highlighter::new(&TODOTHEME);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
syntax,
|
syntax,
|
||||||
@@ -113,7 +116,7 @@ impl Highlighter for TodoHighlighter {
|
|||||||
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
|
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
|
||||||
let ops = self
|
let ops = self
|
||||||
.start_state
|
.start_state
|
||||||
.parse_line(line, &*TODOSYNTAX)
|
.parse_line(line, &TODOSYNTAX)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
Box::new(scope_iterator(
|
Box::new(scope_iterator(
|
||||||
|
|||||||
+63
-73
@@ -2,16 +2,19 @@ use std::{
|
|||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fs,
|
fs,
|
||||||
hash::{BuildHasher, DefaultHasher, Hash, Hasher},
|
hash::{BuildHasher, DefaultHasher, Hash, Hasher},
|
||||||
io::{self, BufRead, BufReader},
|
io,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
ptr::NonNull,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use tokio::io::AsyncBufReadExt;
|
||||||
|
|
||||||
use time::{Date, macros::format_description};
|
use time::{Date, macros::format_description};
|
||||||
|
|
||||||
const DATE_FMT: time::format_description::StaticFormatDescription =
|
const DATE_FMT: time::format_description::StaticFormatDescription =
|
||||||
format_description!("[year]-[month]-[day]");
|
format_description!("[year]-[month]-[day]");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Todo {
|
pub struct Todo {
|
||||||
/// Location of todo.txt file
|
/// Location of todo.txt file
|
||||||
location: PathBuf,
|
location: PathBuf,
|
||||||
@@ -20,15 +23,15 @@ pub struct Todo {
|
|||||||
pub todos: Vec<TodoItem>,
|
pub todos: Vec<TodoItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum TodoError {
|
pub enum TodoError {
|
||||||
Parse,
|
Parse,
|
||||||
Io(io::Error),
|
Io(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for TodoError {
|
impl From<io::Error> for TodoError {
|
||||||
fn from(value: io::Error) -> Self {
|
fn from(value: io::Error) -> Self {
|
||||||
Self::Io(value)
|
Self::Io(value.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +53,9 @@ impl Todo {
|
|||||||
|
|
||||||
pub fn reload(&mut self) -> Result<(), TodoError> {
|
pub fn reload(&mut self) -> Result<(), TodoError> {
|
||||||
let file = fs::File::open(&self.location)?;
|
let file = fs::File::open(&self.location)?;
|
||||||
let reader = BufReader::new(file);
|
let reader = io::BufReader::new(file);
|
||||||
|
|
||||||
|
use io::BufRead;
|
||||||
let mut todos = Vec::new();
|
let mut todos = Vec::new();
|
||||||
for line in reader.lines() {
|
for line in reader.lines() {
|
||||||
// Pin the string so it can be referenced but cruicially not modified
|
// Pin the string so it can be referenced but cruicially not modified
|
||||||
@@ -62,42 +66,26 @@ impl Todo {
|
|||||||
self.todos = todos;
|
self.todos = todos;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum TodoPart<'a> {
|
|
||||||
Description(&'a str),
|
|
||||||
Project(&'a str),
|
|
||||||
Context(&'a str),
|
|
||||||
Tag(&'a str),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Into<&'a str> for TodoPart<'a> {
|
pub async fn async_load(&mut self) -> Result<(), TodoError> {
|
||||||
fn into(self) -> &'a str {
|
self.async_reload().await
|
||||||
match self {
|
|
||||||
TodoPart::Description(s) => s,
|
|
||||||
TodoPart::Project(s) => s,
|
|
||||||
TodoPart::Context(s) => s,
|
|
||||||
TodoPart::Tag(s) => s,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
pub async fn async_reload(&mut self) -> Result<(), TodoError> {
|
||||||
enum TodoPartRaw {
|
let file = tokio::fs::File::open(&self.location).await?;
|
||||||
Description(NonNull<str>),
|
let reader = tokio::io::BufReader::new(file);
|
||||||
Project(NonNull<str>),
|
|
||||||
Context(NonNull<str>),
|
|
||||||
Tag(NonNull<str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Into<TodoPart<'a>> for TodoPartRaw {
|
let mut todos = Vec::new();
|
||||||
fn into<'b>(self) -> TodoPart<'a> {
|
let mut lines = reader.lines();
|
||||||
match self {
|
|
||||||
TodoPartRaw::Description(s) => TodoPart::Description(unsafe { s.as_ref() }),
|
while let Some(line) = lines.next_line().await? {
|
||||||
TodoPartRaw::Project(s) => TodoPart::Project(unsafe { s.as_ref() }),
|
// Pin the string so it can be referenced but cruicially not modified
|
||||||
TodoPartRaw::Context(s) => TodoPart::Context(unsafe { s.as_ref() }),
|
let line = Pin::new(line);
|
||||||
TodoPartRaw::Tag(s) => TodoPart::Tag(unsafe { s.as_ref() }),
|
todos.push(line.try_into()?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.todos = todos;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,20 +144,18 @@ pub struct TodoItem {
|
|||||||
/// Optional - Creation Date (must be sepecified if completion date is)
|
/// Optional - Creation Date (must be sepecified if completion date is)
|
||||||
date_made: Option<Date>,
|
date_made: Option<Date>,
|
||||||
/// Description - raw str slice for description
|
/// Description - raw str slice for description
|
||||||
raw_desc: NonNull<str>,
|
desc_start_index: usize,
|
||||||
/// Formatted Description - list of description components
|
|
||||||
desc: Vec<TodoPartRaw>,
|
|
||||||
/// Project tags
|
/// Project tags
|
||||||
projects: HashSet<TodoItemKey, TodoBuildHasher>,
|
projects: HashSet<TodoItemKey, TodoBuildHasher>,
|
||||||
/// Context tags
|
/// Context tags
|
||||||
contexts: HashSet<TodoItemKey, TodoBuildHasher>,
|
contexts: HashSet<TodoItemKey, TodoBuildHasher>,
|
||||||
/// Special Key/Value tags
|
/// Special Key/Value tags
|
||||||
tags: HashMap<TodoItemKey, NonNull<str>, TodoBuildHasher>,
|
tags: HashMap<TodoItemKey, (usize, usize), TodoBuildHasher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TodoItem {
|
impl TodoItem {
|
||||||
pub fn get_raw(&self) -> &str {
|
pub fn get_raw(&self) -> &str {
|
||||||
&*self.raw
|
&self.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_done(&self) -> bool {
|
pub fn is_done(&self) -> bool {
|
||||||
@@ -217,13 +203,9 @@ impl TodoItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_desc(&self) -> &str {
|
pub fn get_desc(&self) -> &str {
|
||||||
// Safety
|
assert!(self.desc_start_index <= self.raw.len());
|
||||||
// ensure that the pointer is convertible to a reference.
|
|
||||||
// The pointee will always be valid for the pointer's lifetime
|
&self.raw[self.desc_start_index..]
|
||||||
// as it points to a pinned string owned by the same object.
|
|
||||||
// i.e., the String will only be dropped when the pointer is
|
|
||||||
// Additionally the String is never mutated.
|
|
||||||
unsafe { self.raw_desc.as_ref() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_project(&self, project: TodoItemKey) -> bool {
|
fn has_project(&self, project: TodoItemKey) -> bool {
|
||||||
@@ -234,9 +216,12 @@ impl TodoItem {
|
|||||||
where
|
where
|
||||||
T: Into<TodoItemKey>,
|
T: Into<TodoItemKey>,
|
||||||
{
|
{
|
||||||
let tag_pointer = self.tags.get(&tag.into())?;
|
let tag_bounds = self.tags.get(&tag.into())?;
|
||||||
// Safety: see get_desc
|
|
||||||
Some(unsafe { tag_pointer.as_ref() })
|
assert!(tag_bounds.0 <= self.raw.len());
|
||||||
|
assert!(tag_bounds.1 <= self.raw.len());
|
||||||
|
|
||||||
|
Some(&self.raw[tag_bounds.0..tag_bounds.1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,29 +244,31 @@ impl TryFrom<Pin<String>> for TodoItem {
|
|||||||
let mut projects = HashSet::with_hasher(TodoBuildHasher);
|
let mut projects = HashSet::with_hasher(TodoBuildHasher);
|
||||||
let mut contexts = HashSet::with_hasher(TodoBuildHasher);
|
let mut contexts = HashSet::with_hasher(TodoBuildHasher);
|
||||||
let mut tags = HashMap::with_hasher(TodoBuildHasher);
|
let mut tags = HashMap::with_hasher(TodoBuildHasher);
|
||||||
let raw_desc = NonNull::from_ref(&raw[pos..]);
|
let desc_start_index = pos;
|
||||||
let mut desc = Vec::new();
|
|
||||||
|
|
||||||
while let Some((size, word)) = get_word(&raw[pos..]) {
|
while let Some((start, end, skip)) = get_word_indices(&raw[pos..]) {
|
||||||
|
let word = &raw[start..end];
|
||||||
if word.starts_with('@') {
|
if word.starts_with('@') {
|
||||||
if word.len() > 1 {
|
if word.len() > 1 {
|
||||||
contexts.insert(word.into());
|
contexts.insert(word.into());
|
||||||
desc.push(TodoPartRaw::Context(NonNull::from_ref(word)));
|
|
||||||
}
|
}
|
||||||
} else if word.starts_with('+') {
|
} else if word.starts_with('+') {
|
||||||
if word.len() > 1 {
|
if word.len() > 1 {
|
||||||
projects.insert(word.into());
|
projects.insert(word.into());
|
||||||
desc.push(TodoPartRaw::Project(NonNull::from_ref(word)));
|
|
||||||
}
|
}
|
||||||
} else if let Some((k, v)) = word.split_once(':') {
|
} else if let Some((k, v)) = word.split_once(':')
|
||||||
if !(k.contains(':') || v.contains(':') || k.len() == 0 || v.len() == 0) {
|
&& !k.contains(':')
|
||||||
tags.insert(k.into(), NonNull::from_ref(v));
|
&& !v.contains(':')
|
||||||
desc.push(TodoPartRaw::Tag(NonNull::from_ref(word)));
|
&& !k.is_empty()
|
||||||
}
|
&& !v.is_empty()
|
||||||
} else {
|
{
|
||||||
desc.push(TodoPartRaw::Description(NonNull::from_ref(word)));
|
// <-v:3->
|
||||||
|
// <-- size:7 -->
|
||||||
|
// 0 1 2 3 4 5 6 7 8 9
|
||||||
|
// k e y : v a l
|
||||||
|
tags.insert(k.into(), (pos + start + k.len(), pos + end - 1));
|
||||||
}
|
}
|
||||||
pos += size;
|
pos += skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -290,8 +277,7 @@ impl TryFrom<Pin<String>> for TodoItem {
|
|||||||
prio,
|
prio,
|
||||||
date_done,
|
date_done,
|
||||||
date_made,
|
date_made,
|
||||||
raw_desc,
|
desc_start_index,
|
||||||
desc,
|
|
||||||
projects,
|
projects,
|
||||||
contexts,
|
contexts,
|
||||||
tags,
|
tags,
|
||||||
@@ -300,6 +286,10 @@ impl TryFrom<Pin<String>> for TodoItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_word(raw: &str) -> Option<(usize, &str)> {
|
fn get_word(raw: &str) -> Option<(usize, &str)> {
|
||||||
|
get_word_indices(raw).map(|(start, end, skip)| (skip, &raw[start..end]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_word_indices(raw: &str) -> Option<(usize, usize, usize)> {
|
||||||
let mut chars = raw.char_indices();
|
let mut chars = raw.char_indices();
|
||||||
let (next_char, _) = chars.find(|(_, c)| !c.is_whitespace())?;
|
let (next_char, _) = chars.find(|(_, c)| !c.is_whitespace())?;
|
||||||
let next_ws = chars.find(|(_, c)| c.is_whitespace());
|
let next_ws = chars.find(|(_, c)| c.is_whitespace());
|
||||||
@@ -308,11 +298,11 @@ fn get_word(raw: &str) -> Option<(usize, &str)> {
|
|||||||
Some((end, _)) => {
|
Some((end, _)) => {
|
||||||
let next_word = chars.find(|(_, c)| !c.is_whitespace());
|
let next_word = chars.find(|(_, c)| !c.is_whitespace());
|
||||||
match next_word {
|
match next_word {
|
||||||
Some((nw_start, _)) => Some((nw_start, &raw[next_char..end])),
|
Some((nw_start, _)) => Some((next_char, end, nw_start)),
|
||||||
None => Some((end, &raw[next_char..end])),
|
None => Some((next_char, end, end)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Some((raw.len(), &raw[next_char..])),
|
None => Some((next_char, raw.len(), raw.len())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,10 +371,10 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use time::macros::date;
|
use time::macros::date;
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn parse_from_file() {
|
async fn parse_from_file() {
|
||||||
let mut todo = Todo::new("samples/todo.txt".into());
|
let mut todo = Todo::new("samples/todo.txt".into());
|
||||||
todo.load().expect("Error loading todo file");
|
todo.async_load().await.expect("Error loading todo file");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user