Added Todo editor and highlighter. Restructured modules
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@ test having lots of items +planit @tests
|
|||||||
test having lots of items +planit @tests
|
test having lots of items +planit @tests
|
||||||
x 2020-01-01 2020-01-02 test having lots of items +planit @tests
|
x 2020-01-01 2020-01-02 test having lots of items +planit @tests
|
||||||
2020-01-01 test having lots of items +planit @tests
|
2020-01-01 test having lots of items +planit @tests
|
||||||
(B) test having lots of items +planit @tests
|
(B) (A) test having lots of items +planit @tests
|
||||||
x (A) test having lots of items +planit @tests
|
x (A) test having lots of items +planit @tests
|
||||||
test having lots of items +planit @tests
|
test having lots of items +planit @tests
|
||||||
test having lots of items +planit @tests
|
test having lots of items +planit @tests
|
||||||
|
|||||||
+30
-46
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::fl;
|
use crate::fl;
|
||||||
use crate::todo_editor::TodoHighlighter;
|
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};
|
||||||
@@ -11,7 +11,6 @@ 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::{self, Content};
|
||||||
use cosmic::widget::{self, about::About, menu};
|
use cosmic::widget::{self, about::About, menu};
|
||||||
|
|
||||||
use futures_util::SinkExt;
|
use futures_util::SinkExt;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path;
|
use std::path;
|
||||||
@@ -19,7 +18,7 @@ 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::{Todo, TodoPart};
|
use crate::todo::parser::Todo;
|
||||||
|
|
||||||
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");
|
||||||
@@ -44,8 +43,8 @@ pub struct AppModel {
|
|||||||
watch_is_active: bool,
|
watch_is_active: bool,
|
||||||
/// Current Todo data
|
/// Current Todo data
|
||||||
todo: Option<Todo>,
|
todo: Option<Todo>,
|
||||||
|
editor: TodoEditor,
|
||||||
dialog_opt: Option<Dialog<Message>>,
|
dialog_opt: Option<Dialog<Message>>,
|
||||||
content: Content,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Messages emitted by the application and its widgets.
|
/// Messages emitted by the application and its widgets.
|
||||||
@@ -58,7 +57,7 @@ pub enum Message {
|
|||||||
OpenFileDialog,
|
OpenFileDialog,
|
||||||
DialogMessage(DialogMessage),
|
DialogMessage(DialogMessage),
|
||||||
OpenFileResult(DialogResult),
|
OpenFileResult(DialogResult),
|
||||||
Edit(text_editor::Action),
|
Edit(text_editor::Action, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a COSMIC application from the app model
|
/// Create a COSMIC application from the app model
|
||||||
@@ -114,7 +113,8 @@ impl cosmic::Application for AppModel {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let todo = config.current_file.clone().map(|path| Todo::new(path));
|
let todo = config_load_todo(&config);
|
||||||
|
let editor = TodoEditor::new(todo.as_ref());
|
||||||
|
|
||||||
// Construct the app model with the runtime's core.
|
// Construct the app model with the runtime's core.
|
||||||
let mut app = AppModel {
|
let mut app = AppModel {
|
||||||
@@ -128,8 +128,8 @@ impl cosmic::Application for AppModel {
|
|||||||
time: 0,
|
time: 0,
|
||||||
watch_is_active: false,
|
watch_is_active: false,
|
||||||
todo: todo,
|
todo: todo,
|
||||||
|
editor: editor,
|
||||||
dialog_opt: None,
|
dialog_opt: None,
|
||||||
content: Content::with_text("test"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a startup command that sets the window title.
|
// Create a startup command that sets the window title.
|
||||||
@@ -182,37 +182,21 @@ impl cosmic::Application for AppModel {
|
|||||||
fn view(&self) -> Element<'_, Self::Message> {
|
fn view(&self) -> Element<'_, Self::Message> {
|
||||||
let space_s = cosmic::theme::spacing().space_s;
|
let space_s = cosmic::theme::spacing().space_s;
|
||||||
|
|
||||||
let header = widget::row::with_capacity(2)
|
let header = widget::row::with_capacity(1)
|
||||||
.push(widget::text::title1(fl!("welcome")))
|
.push(widget::text::title1(fl!("welcome")))
|
||||||
.push(
|
|
||||||
widget::TextEditor::new(&self.content)
|
|
||||||
.on_action(Message::Edit)
|
|
||||||
.highlight_with::<TodoHighlighter>((), |hl, _t| hl.to_format()),
|
|
||||||
)
|
|
||||||
.align_y(Alignment::End)
|
.align_y(Alignment::End)
|
||||||
.spacing(space_s);
|
.spacing(space_s);
|
||||||
|
|
||||||
let section;
|
|
||||||
|
|
||||||
if let Some(todo) = self.todo.as_ref() {
|
|
||||||
section = widget::Column::with_children(
|
|
||||||
todo.todos.iter().map(|item| view_todo(item.get_str())),
|
|
||||||
)
|
|
||||||
.spacing(20);
|
|
||||||
} else {
|
|
||||||
section = widget::Column::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
let content: Element<_> = widget::column::with_capacity(2)
|
let content: Element<_> = widget::column::with_capacity(2)
|
||||||
.push(header)
|
.push(header)
|
||||||
.push(section)
|
.push(&self.editor)
|
||||||
.spacing(space_s)
|
.spacing(space_s)
|
||||||
.height(Length::Fill)
|
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
widget::container(content)
|
widget::container(content)
|
||||||
.width(600)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
.padding(50)
|
||||||
.apply(widget::container)
|
.apply(widget::container)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.align_x(Horizontal::Center)
|
.align_x(Horizontal::Center)
|
||||||
@@ -335,8 +319,10 @@ impl cosmic::Application for AppModel {
|
|||||||
}
|
}
|
||||||
DialogResult::Cancel => todo!(),
|
DialogResult::Cancel => todo!(),
|
||||||
},
|
},
|
||||||
Message::Edit(action) => {
|
Message::Edit(action, line) => {
|
||||||
self.content.perform(action);
|
if let Err(err) = self.editor.edit(action, line) {
|
||||||
|
log::error!("error in update: {:?}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -363,23 +349,6 @@ impl AppModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_todo<'a>(todo: Vec<TodoPart<'a>>) -> Element<'a, Message> {
|
|
||||||
widget::row::with_children(todo.iter().map(|part| view_text(*part).into())).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_text<'a>(text: TodoPart<'a>) -> widget::Text<'a, Theme, Renderer> {
|
|
||||||
let theme = &cosmic::theme::COSMIC_DARK;
|
|
||||||
let colour = match text {
|
|
||||||
TodoPart::Context(_) => theme.accent_color(),
|
|
||||||
TodoPart::Description(_) => theme.on_primary_component_color(),
|
|
||||||
TodoPart::Project(_) => theme.palette.accent_green,
|
|
||||||
TodoPart::Tag(_) => theme.palette.accent_purple,
|
|
||||||
};
|
|
||||||
let colour = cosmic::iced::Color::from(colour);
|
|
||||||
let text: &str = text.into();
|
|
||||||
widget::text(text).class(colour)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The context page to display in the context drawer.
|
/// The context page to display in the context drawer.
|
||||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||||
pub enum ContextPage {
|
pub enum ContextPage {
|
||||||
@@ -403,3 +372,18 @@ impl menu::action::MenuAction for MenuAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn config_load_todo(config: &Config) -> Option<Todo> {
|
||||||
|
let path = config.current_file.clone()?;
|
||||||
|
let mut todo = Todo::new(path);
|
||||||
|
if let Err(err) = todo.load() {
|
||||||
|
log::error!(
|
||||||
|
"Unable to load configured todo file: {:?}\n{:?}",
|
||||||
|
todo.get_path(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(todo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ mod app;
|
|||||||
mod config;
|
mod config;
|
||||||
mod i18n;
|
mod i18n;
|
||||||
mod todo;
|
mod todo;
|
||||||
mod todo_editor;
|
|
||||||
|
|
||||||
fn main() -> cosmic::iced::Result {
|
fn main() -> cosmic::iced::Result {
|
||||||
// Get the system's preferred languages.
|
// Get the system's preferred languages.
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use crate::app::Message;
|
||||||
|
use crate::todo::highlighter::TodoHighlighter;
|
||||||
|
use crate::todo::parser::Todo;
|
||||||
|
|
||||||
|
use cosmic::Element;
|
||||||
|
use cosmic::iced::Alignment;
|
||||||
|
use cosmic::iced::Length;
|
||||||
|
use cosmic::widget::Column;
|
||||||
|
use cosmic::widget::TextEditor;
|
||||||
|
use cosmic::widget::scrollable;
|
||||||
|
use cosmic::widget::text_editor::Action;
|
||||||
|
use cosmic::widget::text_editor::Content;
|
||||||
|
|
||||||
|
pub enum TodoEditorError {
|
||||||
|
InactiveEdit,
|
||||||
|
LineNotFound(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for TodoEditorError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::InactiveEdit => write!(f, "A line must be active for edit to be valid"),
|
||||||
|
Self::LineNotFound(line) => {
|
||||||
|
write!(f, "Error trying to edit line {} - Line not found", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct TodoLine {
|
||||||
|
content: Content,
|
||||||
|
dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TodoEditor {
|
||||||
|
lines: Vec<TodoLine>,
|
||||||
|
active: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TodoEditor {
|
||||||
|
pub fn new(todo: Option<&Todo>) -> Self {
|
||||||
|
if let Some(todo) = todo {
|
||||||
|
let lines = todo
|
||||||
|
.todos
|
||||||
|
.iter()
|
||||||
|
.map(|item| TodoLine {
|
||||||
|
content: Content::with_text(item.get_desc()),
|
||||||
|
dirty: false,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
lines,
|
||||||
|
active: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
|
lines: vec![TodoLine::default()],
|
||||||
|
active: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn edit(&mut self, edit: Action, line_num: usize) -> Result<(), TodoEditorError> {
|
||||||
|
let line = self
|
||||||
|
.lines
|
||||||
|
.get_mut(line_num)
|
||||||
|
.ok_or(TodoEditorError::LineNotFound(line_num))?;
|
||||||
|
|
||||||
|
line.content.perform(edit);
|
||||||
|
line.dirty = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> From<&'a TodoEditor> for Element<'b, Message>
|
||||||
|
where
|
||||||
|
'a: 'b,
|
||||||
|
{
|
||||||
|
fn from(value: &'a TodoEditor) -> Self {
|
||||||
|
let mut col = Column::with_capacity(value.lines.len())
|
||||||
|
.spacing(cosmic::theme::spacing().space_s)
|
||||||
|
.align_x(Alignment::Center)
|
||||||
|
.width(Length::Fill);
|
||||||
|
for (i, line) in (&value.lines).iter().enumerate() {
|
||||||
|
col = col.push(
|
||||||
|
TextEditor::new(&line.content)
|
||||||
|
.on_action(move |action| Message::Edit(action, i))
|
||||||
|
.highlight_with::<TodoHighlighter>((), |hl, _t| hl.to_format()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
scrollable(col)
|
||||||
|
.spacing(cosmic::theme::spacing().space_xs)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ static TODOSYNTAX: LazyLock<parsing::SyntaxSet> = LazyLock::new(|| {
|
|||||||
let mut builder = parsing::SyntaxSetBuilder::new();
|
let mut builder = parsing::SyntaxSetBuilder::new();
|
||||||
builder.add(
|
builder.add(
|
||||||
parsing::SyntaxDefinition::load_from_str(
|
parsing::SyntaxDefinition::load_from_str(
|
||||||
std::include_str!("../resources/todo.sublime-syntax"),
|
std::include_str!("../../resources/todo.sublime-syntax"),
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod editor;
|
||||||
|
pub mod highlighter;
|
||||||
|
pub mod parser;
|
||||||
@@ -40,6 +40,10 @@ impl Todo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_path(&self) -> &PathBuf {
|
||||||
|
&self.location
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load(&mut self) -> Result<(), TodoError> {
|
pub fn load(&mut self) -> Result<(), TodoError> {
|
||||||
self.reload()
|
self.reload()
|
||||||
}
|
}
|
||||||
@@ -97,17 +101,6 @@ impl<'a> Into<TodoPart<'a>> for TodoPartRaw {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TodoPartRaw {
|
|
||||||
fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
TodoPartRaw::Description(s) => unsafe { s.as_ref() },
|
|
||||||
TodoPartRaw::Project(s) => unsafe { s.as_ref() },
|
|
||||||
TodoPartRaw::Context(s) => unsafe { s.as_ref() },
|
|
||||||
TodoPartRaw::Tag(s) => unsafe { s.as_ref() },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
pub struct TodoItemKey(u64);
|
pub struct TodoItemKey(u64);
|
||||||
|
|
||||||
@@ -175,6 +168,10 @@ pub struct TodoItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TodoItem {
|
impl TodoItem {
|
||||||
|
pub fn get_raw(&self) -> &str {
|
||||||
|
&*self.raw
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_done(&self) -> bool {
|
pub fn is_done(&self) -> bool {
|
||||||
self.done
|
self.done
|
||||||
}
|
}
|
||||||
@@ -219,11 +216,7 @@ impl TodoItem {
|
|||||||
self.date_made = None;
|
self.date_made = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_str<'a>(&'a self) -> Vec<TodoPart<'a>> {
|
pub fn get_desc(&self) -> &str {
|
||||||
self.desc.iter().map(|p| (*p).into()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_raw_str(&self) -> &str {
|
|
||||||
// Safety
|
// Safety
|
||||||
// ensure that the pointer is convertible to a reference.
|
// ensure that the pointer is convertible to a reference.
|
||||||
// The pointee will always be valid for the pointer's lifetime
|
// The pointee will always be valid for the pointer's lifetime
|
||||||
@@ -242,7 +235,7 @@ impl TodoItem {
|
|||||||
T: Into<TodoItemKey>,
|
T: Into<TodoItemKey>,
|
||||||
{
|
{
|
||||||
let tag_pointer = self.tags.get(&tag.into())?;
|
let tag_pointer = self.tags.get(&tag.into())?;
|
||||||
// Safety: see get_raw_str
|
// Safety: see get_desc
|
||||||
Some(unsafe { tag_pointer.as_ref() })
|
Some(unsafe { tag_pointer.as_ref() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -408,7 +401,7 @@ mod tests {
|
|||||||
"X (A) 2020-05-01 2020-01-01 help me please".to_string()
|
"X (A) 2020-05-01 2020-01-01 help me please".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
todo.get_raw_str(),
|
todo.get_desc(),
|
||||||
"X (A) 2020-05-01 2020-01-01 help me please"
|
"X (A) 2020-05-01 2020-01-01 help me please"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -431,7 +424,7 @@ mod tests {
|
|||||||
*todo.raw,
|
*todo.raw,
|
||||||
"x (A) 2020-05-01 2020-01-01 help me please artist: :music".to_string()
|
"x (A) 2020-05-01 2020-01-01 help me please artist: :music".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(todo.get_raw_str(), "help me please artist: :music");
|
assert_eq!(todo.get_desc(), "help me please artist: :music");
|
||||||
|
|
||||||
assert!(todo.projects.is_empty());
|
assert!(todo.projects.is_empty());
|
||||||
assert!(todo.contexts.is_empty());
|
assert!(todo.contexts.is_empty());
|
||||||
@@ -452,7 +445,7 @@ mod tests {
|
|||||||
*todo.raw,
|
*todo.raw,
|
||||||
"x (A) 2020-05-01 2020-01-01 help me please a+beatles +\tmusic".to_string()
|
"x (A) 2020-05-01 2020-01-01 help me please a+beatles +\tmusic".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(todo.get_raw_str(), "help me please a+beatles +\tmusic");
|
assert_eq!(todo.get_desc(), "help me please a+beatles +\tmusic");
|
||||||
|
|
||||||
assert!(todo.projects.is_empty());
|
assert!(todo.projects.is_empty());
|
||||||
assert!(todo.contexts.is_empty());
|
assert!(todo.contexts.is_empty());
|
||||||
@@ -473,7 +466,7 @@ mod tests {
|
|||||||
*todo.raw,
|
*todo.raw,
|
||||||
"x (A) 2020-05-01 2020-01-01 help me please a@beatles @\tmusic".to_string()
|
"x (A) 2020-05-01 2020-01-01 help me please a@beatles @\tmusic".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(todo.get_raw_str(), "help me please a@beatles @\tmusic");
|
assert_eq!(todo.get_desc(), "help me please a@beatles @\tmusic");
|
||||||
|
|
||||||
assert!(todo.projects.is_empty());
|
assert!(todo.projects.is_empty());
|
||||||
assert!(todo.contexts.is_empty());
|
assert!(todo.contexts.is_empty());
|
||||||
@@ -496,7 +489,7 @@ mod tests {
|
|||||||
"x (A) 2020-05-01 2020-01-01 help me please artist:beatles category:music".to_string()
|
"x (A) 2020-05-01 2020-01-01 help me please artist:beatles category:music".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
todo.get_raw_str(),
|
todo.get_desc(),
|
||||||
"help me please artist:beatles category:music"
|
"help me please artist:beatles category:music"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -521,7 +514,7 @@ mod tests {
|
|||||||
*todo.raw,
|
*todo.raw,
|
||||||
"x (A) 2020-05-01 2020-01-01 help me please +beatles +music".to_string()
|
"x (A) 2020-05-01 2020-01-01 help me please +beatles +music".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(todo.get_raw_str(), "help me please +beatles +music");
|
assert_eq!(todo.get_desc(), "help me please +beatles +music");
|
||||||
|
|
||||||
let mut projects = HashSet::with_hasher(TodoBuildHasher);
|
let mut projects = HashSet::with_hasher(TodoBuildHasher);
|
||||||
projects.insert("+beatles".into());
|
projects.insert("+beatles".into());
|
||||||
@@ -547,7 +540,7 @@ mod tests {
|
|||||||
"x (A) 2020-05-01 2020-01-01 help me please @beatles @music".to_string()
|
"x (A) 2020-05-01 2020-01-01 help me please @beatles @music".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(todo.get_raw_str(), "help me please @beatles @music");
|
assert_eq!(todo.get_desc(), "help me please @beatles @music");
|
||||||
let mut contexts = HashSet::with_hasher(TodoBuildHasher);
|
let mut contexts = HashSet::with_hasher(TodoBuildHasher);
|
||||||
contexts.insert("@beatles".into());
|
contexts.insert("@beatles".into());
|
||||||
contexts.insert("@music".into());
|
contexts.insert("@music".into());
|
||||||
Reference in New Issue
Block a user