2021-01-31 21:50:21 +01:00
|
|
|
// Copyright: Ankitects Pty Ltd and contributors
|
|
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
2020-03-15 10:11:45 +01:00
|
|
|
mod parser;
|
2020-03-19 23:20:47 +01:00
|
|
|
mod sqlwriter;
|
rework filtered deck screen & search errors
- Filtered deck creation now happens as an atomic operation, and is
undoable.
- The logic for initial search text, normalizing searches and so on
has been pushed into the backend.
- Use protobuf to pass the filtered deck to the updated dialog, so
we don't need to deal with untyped JSON.
- Change the "revise your search?" prompt to be a simple info box -
user has access to cancel and build buttons, and doesn't need a separate
prompt. Tweak the wording so the 'show excluded' button should be more
obvious.
- Filtered decks have a time appended to them instead of a number,
primarily because it's easier to implement. No objections going back to
the old behaviour if someone wants to contribute a clean patch.
The standard de-duplication will happen if two decks are created in the
same minute with the same name.
- Tweak the default sort order, and start with two searches. The UI
will still hide the second search by default, but by starting with two,
the frontend doesn't need logic for creating the starting text.
- Search errors now have their own error type, instead of using
InvalidInput, as that was intended mainly for bad API calls. The markdown
conversion is done when the error is converted from the backend, allowing
errors to printed as a string without any special handling by the calling
code.
TODO: when building a new filtered deck, update_active() is clobbering
the undo log when the overview is refreshed
2021-03-24 12:52:48 +01:00
|
|
|
pub(crate) mod writer;
|
2020-03-19 23:20:47 +01:00
|
|
|
|
2021-04-18 10:29:20 +02:00
|
|
|
use std::borrow::Cow;
|
|
|
|
|
2021-02-11 08:11:17 +01:00
|
|
|
pub use parser::{
|
|
|
|
parse as parse_search, Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind,
|
2021-01-07 12:50:57 +01:00
|
|
|
};
|
2021-03-29 08:12:26 +02:00
|
|
|
use rusqlite::types::FromSql;
|
2021-04-18 10:29:20 +02:00
|
|
|
use sqlwriter::{RequiredTable, SqlWriter};
|
|
|
|
pub use writer::{concatenate_searches, replace_search_node, write_nodes, BoolSeparator};
|
2021-03-29 08:12:26 +02:00
|
|
|
|
|
|
|
use crate::{
|
2021-04-18 10:29:20 +02:00
|
|
|
browser_table::Column,
|
|
|
|
card::{CardId, CardType},
|
|
|
|
collection::Collection,
|
|
|
|
error::Result,
|
|
|
|
notes::NoteId,
|
|
|
|
prelude::AnkiError,
|
2021-03-29 08:12:26 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
2021-04-10 10:14:41 +02:00
|
|
|
pub enum ReturnItemType {
|
2021-03-29 08:12:26 +02:00
|
|
|
Cards,
|
|
|
|
Notes,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
|
|
pub enum SortMode {
|
|
|
|
NoOrder,
|
2021-04-09 18:03:29 +02:00
|
|
|
Builtin { column: Column, reverse: bool },
|
2021-03-29 08:12:26 +02:00
|
|
|
Custom(String),
|
|
|
|
}
|
|
|
|
|
2021-04-10 10:14:41 +02:00
|
|
|
pub trait AsReturnItemType {
|
|
|
|
fn as_return_item_type() -> ReturnItemType;
|
2021-03-29 08:12:26 +02:00
|
|
|
}
|
|
|
|
|
2021-04-10 10:14:41 +02:00
|
|
|
impl AsReturnItemType for CardId {
|
|
|
|
fn as_return_item_type() -> ReturnItemType {
|
|
|
|
ReturnItemType::Cards
|
2021-03-29 08:12:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-10 10:14:41 +02:00
|
|
|
impl AsReturnItemType for NoteId {
|
|
|
|
fn as_return_item_type() -> ReturnItemType {
|
|
|
|
ReturnItemType::Notes
|
2021-03-29 08:12:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-10 10:14:41 +02:00
|
|
|
impl ReturnItemType {
|
2021-03-29 08:12:26 +02:00
|
|
|
fn required_table(&self) -> RequiredTable {
|
|
|
|
match self {
|
2021-04-10 10:14:41 +02:00
|
|
|
ReturnItemType::Cards => RequiredTable::Cards,
|
|
|
|
ReturnItemType::Notes => RequiredTable::Notes,
|
2021-03-29 08:12:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SortMode {
|
|
|
|
fn required_table(&self) -> RequiredTable {
|
|
|
|
match self {
|
|
|
|
SortMode::NoOrder => RequiredTable::CardsOrNotes,
|
2021-04-09 18:03:29 +02:00
|
|
|
SortMode::Builtin { column, .. } => column.required_table(),
|
2021-03-29 08:12:26 +02:00
|
|
|
SortMode::Custom(ref text) => {
|
|
|
|
if text.contains("n.") {
|
|
|
|
if text.contains("c.") {
|
|
|
|
RequiredTable::CardsAndNotes
|
|
|
|
} else {
|
|
|
|
RequiredTable::Notes
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
RequiredTable::Cards
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-09 18:03:29 +02:00
|
|
|
impl Column {
|
2021-03-29 08:12:26 +02:00
|
|
|
fn required_table(self) -> RequiredTable {
|
|
|
|
match self {
|
2021-04-09 18:03:29 +02:00
|
|
|
Column::Cards
|
|
|
|
| Column::NoteCreation
|
|
|
|
| Column::NoteMod
|
|
|
|
| Column::Notetype
|
|
|
|
| Column::SortField
|
|
|
|
| Column::Tags => RequiredTable::Notes,
|
|
|
|
_ => RequiredTable::CardsOrNotes,
|
2021-03-29 08:12:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-30 03:37:55 +02:00
|
|
|
pub trait TryIntoSearch {
|
|
|
|
fn try_into_search(self) -> Result<Node, AnkiError>;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TryIntoSearch for &str {
|
|
|
|
fn try_into_search(self) -> Result<Node, AnkiError> {
|
|
|
|
parser::parse(self).map(Node::Group)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TryIntoSearch for &String {
|
|
|
|
fn try_into_search(self) -> Result<Node, AnkiError> {
|
|
|
|
parser::parse(self).map(Node::Group)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<T> TryIntoSearch for T
|
|
|
|
where
|
|
|
|
T: Into<Node>,
|
|
|
|
{
|
|
|
|
fn try_into_search(self) -> Result<Node, AnkiError> {
|
|
|
|
Ok(self.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Collection {
|
|
|
|
pub fn search_cards<N>(&mut self, search: N, mode: SortMode) -> Result<Vec<CardId>>
|
|
|
|
where
|
|
|
|
N: TryIntoSearch,
|
|
|
|
{
|
|
|
|
self.search(search, mode)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn search_notes<N>(&mut self, search: N, mode: SortMode) -> Result<Vec<NoteId>>
|
|
|
|
where
|
|
|
|
N: TryIntoSearch,
|
|
|
|
{
|
|
|
|
self.search(search, mode)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn search_notes_unordered<N>(&mut self, search: N) -> Result<Vec<NoteId>>
|
|
|
|
where
|
|
|
|
N: TryIntoSearch,
|
|
|
|
{
|
|
|
|
self.search(search, SortMode::NoOrder)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-29 08:12:26 +02:00
|
|
|
impl Collection {
|
2021-04-30 03:37:55 +02:00
|
|
|
fn search<T, N>(&mut self, search: N, mode: SortMode) -> Result<Vec<T>>
|
2021-03-29 08:12:26 +02:00
|
|
|
where
|
2021-04-30 03:37:55 +02:00
|
|
|
N: TryIntoSearch,
|
2021-04-10 10:14:41 +02:00
|
|
|
T: FromSql + AsReturnItemType,
|
2021-03-29 08:12:26 +02:00
|
|
|
{
|
2021-04-10 10:14:41 +02:00
|
|
|
let item_type = T::as_return_item_type();
|
2021-04-30 03:37:55 +02:00
|
|
|
let top_node = search.try_into_search()?;
|
2021-04-10 10:14:41 +02:00
|
|
|
let writer = SqlWriter::new(self, item_type);
|
2021-03-29 08:12:26 +02:00
|
|
|
|
|
|
|
let (mut sql, args) = writer.build_query(&top_node, mode.required_table())?;
|
2021-04-10 10:14:41 +02:00
|
|
|
self.add_order(&mut sql, item_type, mode)?;
|
2021-03-29 08:12:26 +02:00
|
|
|
|
|
|
|
let mut stmt = self.storage.db.prepare(&sql)?;
|
|
|
|
let ids: Vec<_> = stmt
|
|
|
|
.query_map(&args, |row| row.get(0))?
|
|
|
|
.collect::<std::result::Result<_, _>>()?;
|
|
|
|
|
|
|
|
Ok(ids)
|
|
|
|
}
|
|
|
|
|
2021-04-10 10:14:41 +02:00
|
|
|
fn add_order(
|
|
|
|
&mut self,
|
|
|
|
sql: &mut String,
|
|
|
|
item_type: ReturnItemType,
|
|
|
|
mode: SortMode,
|
|
|
|
) -> Result<()> {
|
2021-03-29 08:12:26 +02:00
|
|
|
match mode {
|
|
|
|
SortMode::NoOrder => (),
|
2021-04-09 18:03:29 +02:00
|
|
|
SortMode::Builtin { column, reverse } => {
|
2021-04-10 10:14:41 +02:00
|
|
|
prepare_sort(self, column, item_type)?;
|
2021-03-29 08:12:26 +02:00
|
|
|
sql.push_str(" order by ");
|
2021-04-10 10:14:41 +02:00
|
|
|
write_order(sql, item_type, column, reverse)?;
|
2021-03-29 08:12:26 +02:00
|
|
|
}
|
|
|
|
SortMode::Custom(order_clause) => {
|
|
|
|
sql.push_str(" order by ");
|
|
|
|
sql.push_str(&order_clause);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Place the matched card ids into a temporary 'search_cids' table
|
|
|
|
/// instead of returning them. Use clear_searched_cards() to remove it.
|
|
|
|
/// Returns number of added cards.
|
2021-04-30 03:37:55 +02:00
|
|
|
pub(crate) fn search_cards_into_table<N>(&mut self, search: N, mode: SortMode) -> Result<usize>
|
|
|
|
where
|
|
|
|
N: TryIntoSearch,
|
|
|
|
{
|
|
|
|
let top_node = search.try_into_search()?;
|
2021-04-10 10:14:41 +02:00
|
|
|
let writer = SqlWriter::new(self, ReturnItemType::Cards);
|
2021-03-29 08:12:26 +02:00
|
|
|
let want_order = mode != SortMode::NoOrder;
|
|
|
|
|
|
|
|
let (mut sql, args) = writer.build_query(&top_node, mode.required_table())?;
|
2021-04-10 10:14:41 +02:00
|
|
|
self.add_order(&mut sql, ReturnItemType::Cards, mode)?;
|
2021-03-29 08:12:26 +02:00
|
|
|
|
|
|
|
if want_order {
|
|
|
|
self.storage
|
|
|
|
.setup_searched_cards_table_to_preserve_order()?;
|
|
|
|
} else {
|
|
|
|
self.storage.setup_searched_cards_table()?;
|
|
|
|
}
|
|
|
|
let sql = format!("insert into search_cids {}", sql);
|
|
|
|
|
|
|
|
self.storage
|
|
|
|
.db
|
|
|
|
.prepare(&sql)?
|
|
|
|
.execute(&args)
|
|
|
|
.map_err(Into::into)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Add the order clause to the sql.
|
2021-04-10 10:14:41 +02:00
|
|
|
fn write_order(
|
|
|
|
sql: &mut String,
|
|
|
|
item_type: ReturnItemType,
|
|
|
|
column: Column,
|
|
|
|
reverse: bool,
|
|
|
|
) -> Result<()> {
|
|
|
|
let order = match item_type {
|
|
|
|
ReturnItemType::Cards => card_order_from_sort_column(column),
|
|
|
|
ReturnItemType::Notes => note_order_from_sort_column(column),
|
2021-03-29 08:12:26 +02:00
|
|
|
};
|
|
|
|
if order.is_empty() {
|
2021-04-01 09:37:18 +02:00
|
|
|
return Err(AnkiError::invalid_input(format!(
|
|
|
|
"Can't sort {:?} by {:?}.",
|
2021-04-10 10:14:41 +02:00
|
|
|
item_type, column
|
2021-04-01 09:37:18 +02:00
|
|
|
)));
|
2021-03-29 08:12:26 +02:00
|
|
|
}
|
|
|
|
if reverse {
|
|
|
|
sql.push_str(
|
|
|
|
&order
|
|
|
|
.to_ascii_lowercase()
|
|
|
|
.replace(" desc", "")
|
|
|
|
.replace(" asc", " desc"),
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
sql.push_str(&order);
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-04-09 18:03:29 +02:00
|
|
|
fn card_order_from_sort_column(column: Column) -> Cow<'static, str> {
|
|
|
|
match column {
|
|
|
|
Column::CardMod => "c.mod asc".into(),
|
|
|
|
Column::Cards => concat!(
|
2021-03-29 08:12:26 +02:00
|
|
|
"coalesce((select pos from sort_order where ntid = n.mid and ord = c.ord),",
|
|
|
|
// need to fall back on ord 0 for cloze cards
|
|
|
|
"(select pos from sort_order where ntid = n.mid and ord = 0)) asc"
|
|
|
|
)
|
|
|
|
.into(),
|
2021-04-09 18:03:29 +02:00
|
|
|
Column::Deck => "(select pos from sort_order where did = c.did) asc".into(),
|
|
|
|
Column::Due => "c.type asc, c.due asc".into(),
|
|
|
|
Column::Ease => format!("c.type = {} asc, c.factor asc", CardType::New as i8).into(),
|
|
|
|
Column::Interval => "c.ivl asc".into(),
|
|
|
|
Column::Lapses => "c.lapses asc".into(),
|
|
|
|
Column::NoteCreation => "n.id asc, c.ord asc".into(),
|
|
|
|
Column::NoteMod => "n.mod asc, c.ord asc".into(),
|
|
|
|
Column::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
|
|
|
|
Column::Reps => "c.reps asc".into(),
|
|
|
|
Column::SortField => "n.sfld collate nocase asc, c.ord asc".into(),
|
|
|
|
Column::Tags => "n.tags asc".into(),
|
|
|
|
Column::Answer | Column::Custom | Column::Question => "".into(),
|
2021-03-29 08:12:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-09 18:03:29 +02:00
|
|
|
fn note_order_from_sort_column(column: Column) -> Cow<'static, str> {
|
|
|
|
match column {
|
|
|
|
Column::CardMod
|
|
|
|
| Column::Cards
|
|
|
|
| Column::Deck
|
|
|
|
| Column::Due
|
|
|
|
| Column::Ease
|
|
|
|
| Column::Interval
|
|
|
|
| Column::Lapses
|
|
|
|
| Column::Reps => "(select pos from sort_order where nid = n.id) asc".into(),
|
|
|
|
Column::NoteCreation => "n.id asc".into(),
|
|
|
|
Column::NoteMod => "n.mod asc".into(),
|
|
|
|
Column::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
|
|
|
|
Column::SortField => "n.sfld collate nocase asc".into(),
|
|
|
|
Column::Tags => "n.tags asc".into(),
|
|
|
|
Column::Answer | Column::Custom | Column::Question => "".into(),
|
2021-03-29 08:12:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-10 10:14:41 +02:00
|
|
|
fn prepare_sort(col: &mut Collection, column: Column, item_type: ReturnItemType) -> Result<()> {
|
|
|
|
let sql = match item_type {
|
|
|
|
ReturnItemType::Cards => match column {
|
2021-04-09 18:03:29 +02:00
|
|
|
Column::Cards => include_str!("template_order.sql"),
|
|
|
|
Column::Deck => include_str!("deck_order.sql"),
|
|
|
|
Column::Notetype => include_str!("notetype_order.sql"),
|
|
|
|
_ => return Ok(()),
|
|
|
|
},
|
2021-04-10 10:14:41 +02:00
|
|
|
ReturnItemType::Notes => match column {
|
2021-04-09 18:03:29 +02:00
|
|
|
Column::Cards => include_str!("note_cards_order.sql"),
|
|
|
|
Column::CardMod => include_str!("card_mod_order.sql"),
|
|
|
|
Column::Deck => include_str!("note_decks_order.sql"),
|
|
|
|
Column::Due => include_str!("note_due_order.sql"),
|
|
|
|
Column::Ease => include_str!("note_ease_order.sql"),
|
|
|
|
Column::Interval => include_str!("note_interval_order.sql"),
|
|
|
|
Column::Lapses => include_str!("note_lapses_order.sql"),
|
|
|
|
Column::Reps => include_str!("note_reps_order.sql"),
|
|
|
|
Column::Notetype => include_str!("notetype_order.sql"),
|
|
|
|
_ => return Ok(()),
|
|
|
|
},
|
2021-03-29 08:12:26 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
col.storage.db.execute_batch(sql)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|