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