From cc395f7c448ee0ba268077a1e21fb8e0b80e99f4 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 21 Jun 2025 20:15:19 +0800 Subject: [PATCH 1/7] Upgrade to nom 8.0.0 (#4105) * bump nom to 8.0.0 * update cloze.rs * update template.rs * update imageocclusion.rs * update search/parser.rs * update card_rendering/parser.rs * replace use of fold_many0 with many0 in nom 8, `many0` doesn't accumulate when used within `recognize` --- Cargo.lock | 13 ++++- Cargo.toml | 2 +- cargo/licenses.json | 9 +++ rslib/src/card_rendering/parser.rs | 61 ++++++++++++--------- rslib/src/cloze.rs | 10 ++-- rslib/src/image_occlusion/imageocclusion.rs | 6 +- rslib/src/search/parser.rs | 32 ++++++----- rslib/src/template.rs | 7 ++- 8 files changed, 90 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66173027b..03f9e63c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,7 +117,7 @@ dependencies = [ "id_tree", "inflections", "itertools 0.14.0", - "nom", + "nom 8.0.0", "num_cpus", "num_enum", "once_cell", @@ -4117,6 +4117,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "normpath" version = "1.3.0" @@ -6258,7 +6267,7 @@ dependencies = [ "bytesize", "lazy_static", "libc", - "nom", + "nom 7.1.3", "time", "winapi", ] diff --git a/Cargo.toml b/Cargo.toml index 980956b05..61cca8649 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ junction = "1.2.0" libc = "0.2" libc-stdhandle = "0.1" maplit = "1.0.2" -nom = "7.1.3" +nom = "8.0.0" num-format = "0.4.4" num_cpus = "1.17.0" num_enum = "0.7.3" diff --git a/cargo/licenses.json b/cargo/licenses.json index c16b20aa7..f2695ac76 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -2645,6 +2645,15 @@ "license_file": null, "description": "A byte-oriented, zero-copy, parser combinators library" }, + { + "name": "nom", + "version": "8.0.0", + "authors": "contact@geoffroycouprie.com", + "repository": "https://github.com/rust-bakery/nom", + "license": "MIT", + "license_file": null, + "description": "A byte-oriented, zero-copy, parser combinators library" + }, { "name": "ntapi", "version": "0.4.1", diff --git a/rslib/src/card_rendering/parser.rs b/rslib/src/card_rendering/parser.rs index 6f1cc662e..b124c069d 100644 --- a/rslib/src/card_rendering/parser.rs +++ b/rslib/src/card_rendering/parser.rs @@ -14,14 +14,14 @@ use nom::combinator::recognize; use nom::combinator::rest; use nom::combinator::success; use nom::combinator::value; -use nom::multi::fold_many0; use nom::multi::many0; use nom::sequence::delimited; use nom::sequence::pair; use nom::sequence::preceded; use nom::sequence::separated_pair; use nom::sequence::terminated; -use nom::sequence::tuple; +use nom::Input; +use nom::Parser; use super::CardNodes; use super::Directive; @@ -86,9 +86,12 @@ impl<'a> Directive<'a> { } /// Consume 0 or more of anything in " \t\r\n" after `parser`. -fn trailing_whitespace0<'parser, 's, P, O>(parser: P) -> impl FnMut(&'s str) -> IResult<'s, O> +fn trailing_whitespace0(parser: P) -> impl Parser where - P: FnMut(&'s str) -> IResult<'s, O> + 'parser, + I: Input, + ::Item: nom::AsChar, + E: nom::error::ParseError, + P: Parser, { terminated(parser, multispace0) } @@ -97,11 +100,11 @@ where fn is_not0<'parser, 'arr: 'parser, 's: 'parser>( arr: &'arr str, ) -> impl FnMut(&'s str) -> IResult<'s, &'s str> + 'parser { - alt((is_not(arr), success(""))) + move |s| alt((is_not(arr), success(""))).parse(s) } fn node(s: &str) -> IResult { - alt((sound_node, tag_node, text_node))(s) + alt((sound_node, tag_node, text_node)).parse(s) } /// A sound tag `[sound:resource]`, where `resource` is pointing to a sound or @@ -110,11 +113,11 @@ fn sound_node(s: &str) -> IResult { map( delimited(tag("[sound:"), is_not("]"), tag("]")), Node::SoundOrVideo, - )(s) + ) + .parse(s) } fn take_till_potential_tag_start(s: &str) -> IResult<&str> { - use nom::InputTake; // first char could be '[', but wasn't part of a node, so skip (eof ends parse) let (after, offset) = anychar(s).map(|(s, c)| (s, c.len_utf8()))?; Ok(match after.find('[') { @@ -127,7 +130,7 @@ fn take_till_potential_tag_start(s: &str) -> IResult<&str> { fn tag_node(s: &str) -> IResult { /// Match the start of an opening tag and return its name. fn name(s: &str) -> IResult<&str> { - preceded(tag("[anki:"), is_not("] \t\r\n"))(s) + preceded(tag("[anki:"), is_not("] \t\r\n")).parse(s) } /// Return a parser to match an opening `name` tag and return its options. @@ -138,31 +141,35 @@ fn tag_node(s: &str) -> IResult { /// empty. fn options(s: &str) -> IResult> { fn key(s: &str) -> IResult<&str> { - is_not("] \t\r\n=")(s) + is_not("] \t\r\n=").parse(s) } fn val(s: &str) -> IResult<&str> { alt(( delimited(tag("\""), is_not0("\""), tag("\"")), is_not0("] \t\r\n\""), - ))(s) + )) + .parse(s) } - many0(trailing_whitespace0(separated_pair(key, tag("="), val)))(s) + many0(trailing_whitespace0(separated_pair(key, tag("="), val))).parse(s) } - delimited( - pair(tag("[anki:"), trailing_whitespace0(tag(name))), - options, - tag("]"), - ) + move |s| { + delimited( + pair(tag("[anki:"), trailing_whitespace0(tag(name))), + options, + tag("]"), + ) + .parse(s) + } } /// Return a parser to match a closing `name` tag. fn closing_parser<'parser, 'name: 'parser, 's: 'parser>( name: &'name str, ) -> impl FnMut(&'s str) -> IResult<'s, ()> + 'parser { - value((), tuple((tag("[/anki:"), tag(name), tag("]")))) + move |s| value((), (tag("[/anki:"), tag(name), tag("]"))).parse(s) } /// Return a parser to match and return anything until a closing `name` tag @@ -170,12 +177,13 @@ fn tag_node(s: &str) -> IResult { fn content_parser<'parser, 'name: 'parser, 's: 'parser>( name: &'name str, ) -> impl FnMut(&'s str) -> IResult<'s, &'s str> + 'parser { - recognize(fold_many0( - pair(not(closing_parser(name)), take_till_potential_tag_start), - // we don't need to accumulate anything - || (), - |_, _| (), - )) + move |s| { + recognize(many0(pair( + not(closing_parser(name)), + take_till_potential_tag_start, + ))) + .parse(s) + } } let (_, tag_name) = name(s)?; @@ -185,11 +193,12 @@ fn tag_node(s: &str) -> IResult { closing_parser(tag_name), ), |(options, content)| Node::Directive(Directive::new(tag_name, options, content)), - )(s) + ) + .parse(s) } fn text_node(s: &str) -> IResult { - map(take_till_potential_tag_start, Node::Text)(s) + map(take_till_potential_tag_start, Node::Text).parse(s) } #[cfg(test)] diff --git a/rslib/src/cloze.rs b/rslib/src/cloze.rs index f57d07ab0..208a2f4ed 100644 --- a/rslib/src/cloze.rs +++ b/rslib/src/cloze.rs @@ -15,6 +15,7 @@ use nom::bytes::complete::tag; use nom::bytes::complete::take_while; use nom::combinator::map; use nom::IResult; +use nom::Parser; use regex::Captures; use regex::Regex; @@ -72,7 +73,7 @@ fn tokenize(mut text: &str) -> impl Iterator { } fn close_cloze(text: &str) -> IResult<&str, Token> { - map(tag("}}"), |_| Token::CloseCloze)(text) + map(tag("}}"), |_| Token::CloseCloze).parse(text) } /// Match a run of text until an open/close marker is encountered. @@ -87,7 +88,7 @@ fn tokenize(mut text: &str) -> impl Iterator { // start with the no-match case let mut index = text.len(); for (idx, _) in text.char_indices() { - if other_token(&text[idx..]).is_ok() { + if other_token.parse(&text[idx..]).is_ok() { index = idx; break; } @@ -99,8 +100,9 @@ fn tokenize(mut text: &str) -> impl Iterator { if text.is_empty() { None } else { - let (remaining_text, token) = - alt((open_cloze, close_cloze, normal_text))(text).unwrap(); + let (remaining_text, token) = alt((open_cloze, close_cloze, normal_text)) + .parse(text) + .unwrap(); text = remaining_text; Some(token) } diff --git a/rslib/src/image_occlusion/imageocclusion.rs b/rslib/src/image_occlusion/imageocclusion.rs index 2ba83374f..e2eea9a39 100644 --- a/rslib/src/image_occlusion/imageocclusion.rs +++ b/rslib/src/image_occlusion/imageocclusion.rs @@ -13,6 +13,7 @@ use nom::character::complete::char; use nom::error::ErrorKind; use nom::sequence::preceded; use nom::sequence::separated_pair; +use nom::Parser; fn unescape(text: &str) -> String { text.replace("\\:", ":") @@ -22,11 +23,12 @@ pub fn parse_image_cloze(text: &str) -> Option { if let Some((shape, _)) = text.split_once(':') { let mut properties = vec![]; let mut remaining = &text[shape.len()..]; - while let Ok((rem, (name, value))) = separated_pair::<_, _, _, _, (_, ErrorKind), _, _, _>( + while let Ok((rem, (name, value))) = separated_pair::<_, _, _, (_, ErrorKind), _, _, _>( preceded(tag(":"), is_not("=")), tag("="), escaped(is_not("\\:"), '\\', char(':')), - )(remaining) + ) + .parse(remaining) { remaining = rem; let value = unescape(value); diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 93df4ea08..041ec4948 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -19,6 +19,7 @@ use nom::error::ErrorKind as NomErrorKind; use nom::multi::many0; use nom::sequence::preceded; use nom::sequence::separated_pair; +use nom::Parser; use regex::Captures; use regex::Regex; @@ -202,18 +203,19 @@ fn group_inner(input: &str) -> IResult> { } fn whitespace0(s: &str) -> IResult> { - many0(one_of(" \u{3000}"))(s) + many0(one_of(" \u{3000}")).parse(s) } /// Optional leading space, then a (negated) group or text fn node(s: &str) -> IResult { - preceded(whitespace0, alt((negated_node, group, text)))(s) + preceded(whitespace0, alt((negated_node, group, text))).parse(s) } fn negated_node(s: &str) -> IResult { map(preceded(char('-'), alt((group, text))), |node| { Node::Not(Box::new(node)) - })(s) + }) + .parse(s) } /// One or more nodes surrounded by brackets, eg (one OR two) @@ -233,7 +235,7 @@ fn group(s: &str) -> IResult { /// Either quoted or unquoted text fn text(s: &str) -> IResult { - alt((quoted_term, partially_quoted_term, unquoted_term))(s) + alt((quoted_term, partially_quoted_term, unquoted_term)).parse(s) } /// Quoted text, including the outer double quotes. @@ -248,7 +250,8 @@ fn partially_quoted_term(s: &str) -> IResult { escaped(is_not("\"(): \u{3000}\\"), '\\', none_of(" \u{3000}")), char(':'), quoted_term_str, - )(s)?; + ) + .parse(s)?; Ok(( remaining, Node::Search(search_node_for_text_with_argument(key, val)?), @@ -296,7 +299,7 @@ fn unquoted_term(s: &str) -> IResult { fn quoted_term_str(s: &str) -> IResult<&str> { let (opened, _) = char('"')(s)?; if let Ok((tail, inner)) = - escaped::<_, ParseError, _, _, _, _>(is_not(r#""\"#), '\\', anychar)(opened) + escaped::<_, ParseError, _, _>(is_not(r#""\"#), '\\', anychar).parse(opened) { if let Ok((remaining, _)) = char::<_, ParseError>('"')(tail) { Ok((remaining, inner)) @@ -321,7 +324,8 @@ fn search_node_for_text(s: &str) -> ParseResult { // leading : is only possible error for well-formed input let (tail, head) = verify(escaped(is_not(r":\"), '\\', anychar), |t: &str| { !t.is_empty() - })(s) + }) + .parse(s) .map_err(|_: nom::Err| parse_failure(s, FailKind::MissingKey))?; if tail.is_empty() { Ok(SearchNode::UnqualifiedText(unescape(head)?)) @@ -407,7 +411,7 @@ fn parse_resched(s: &str) -> ParseResult { /// eg prop:ivl>3, prop:ease!=2.5 fn parse_prop(prop_clause: &str) -> ParseResult { - let (tail, prop) = alt::<_, _, ParseError, _>(( + let (tail, prop) = alt(( tag("ivl"), tag("due"), tag("reps"), @@ -421,8 +425,9 @@ fn parse_prop(prop_clause: &str) -> ParseResult { tag("r"), recognize(preceded(tag("cdn:"), alphanumeric1)), recognize(preceded(tag("cds:"), alphanumeric1)), - ))(prop_clause) - .map_err(|_| { + )) + .parse(prop_clause) + .map_err(|_: nom::Err| { parse_failure( prop_clause, FailKind::InvalidPropProperty { @@ -431,15 +436,16 @@ fn parse_prop(prop_clause: &str) -> ParseResult { ) })?; - let (num, operator) = alt::<_, _, ParseError, _>(( + let (num, operator) = alt(( tag("<="), tag(">="), tag("!="), tag("="), tag("<"), tag(">"), - ))(tail) - .map_err(|_| { + )) + .parse(tail) + .map_err(|_: nom::Err| { parse_failure( prop_clause, FailKind::InvalidPropOperator { diff --git a/rslib/src/template.rs b/rslib/src/template.rs index d09ade580..e3a900a2b 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -13,6 +13,7 @@ use nom::bytes::complete::tag; use nom::bytes::complete::take_until; use nom::combinator::map; use nom::sequence::delimited; +use nom::Parser; use regex::Regex; use crate::cloze::cloze_number_in_fields; @@ -67,7 +68,8 @@ impl TemplateMode { tag(self.end_tag()), ), |out| classify_handle(out), - )(s) + ) + .parse(s) } /// Return the next handlebar, comment or text token. @@ -127,7 +129,8 @@ fn comment_token(s: &str) -> nom::IResult<&str, Token> { tag(COMMENT_END), ), Token::Comment, - )(s) + ) + .parse(s) } fn tokens(mut template: &str) -> impl Iterator>> { From 88538d8badeb11e76d534077caa05f93d5d07267 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Sat, 21 Jun 2025 20:15:30 +0800 Subject: [PATCH 2/7] Fix/set due date on intraday learning card (#4101) - Introduced `next_day_start` parameter to `set_due_date` for improved due date handling. - Updated logic to account for Unix epoch timestamps when calculating due dates. --- rslib/src/scheduler/reviews.rs | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/rslib/src/scheduler/reviews.rs b/rslib/src/scheduler/reviews.rs index 7e61447da..06390e57d 100644 --- a/rslib/src/scheduler/reviews.rs +++ b/rslib/src/scheduler/reviews.rs @@ -17,6 +17,7 @@ use crate::collection::Collection; use crate::config::StringKey; use crate::error::Result; use crate::prelude::*; +use crate::scheduler::timing::is_unix_epoch_timestamp; impl Card { /// Make card due in `days_from_today`. @@ -27,6 +28,7 @@ impl Card { fn set_due_date( &mut self, today: u32, + next_day_start: i64, days_from_today: u32, ease_factor: f32, force_reset: bool, @@ -34,8 +36,15 @@ impl Card { let new_due = (today + days_from_today) as i32; let fsrs_enabled = self.memory_state.is_some(); let new_interval = if fsrs_enabled { - self.interval - .saturating_add_signed(new_due - self.original_or_current_due()) + let due = self.original_or_current_due(); + let due_diff = if is_unix_epoch_timestamp(due) { + let offset = (due as i64 - next_day_start) / 86_400; + let due = (today as i64 + offset) as i32; + new_due - due + } else { + new_due - due + }; + self.interval.saturating_add_signed(due_diff) } else if force_reset || !matches!(self.ctype, CardType::Review | CardType::Relearn) { days_from_today.max(1) } else { @@ -114,6 +123,7 @@ impl Collection { let spec = parse_due_date_str(days)?; let usn = self.usn()?; let today = self.timing_today()?.days_elapsed; + let next_day_start = self.timing_today()?.next_day_at.0; let mut rng = rand::rng(); let distribution = Uniform::new_inclusive(spec.min, spec.max).unwrap(); let mut decks_initial_ease: HashMap = HashMap::new(); @@ -137,7 +147,13 @@ impl Collection { }; let original = card.clone(); let days_from_today = distribution.sample(&mut rng); - card.set_due_date(today, days_from_today, ease_factor, spec.force_reset); + card.set_due_date( + today, + next_day_start, + days_from_today, + ease_factor, + spec.force_reset, + ); col.log_manually_scheduled_review(&card, original.interval, usn)?; col.update_card_inner(&mut card, original, usn)?; } @@ -228,26 +244,26 @@ mod test { let mut c = Card::new(NoteId(0), 0, DeckId(0), 0); // setting the due date of a new card will convert it - c.set_due_date(5, 2, 1.8, false); + c.set_due_date(5, 0, 2, 1.8, false); assert_eq!(c.ctype, CardType::Review); assert_eq!(c.due, 7); assert_eq!(c.interval, 2); assert_eq!(c.ease_factor, 1800); // reschedule it again the next day, shifting it from day 7 to day 9 - c.set_due_date(6, 3, 2.5, false); + c.set_due_date(6, 0, 3, 2.5, false); assert_eq!(c.due, 9); assert_eq!(c.interval, 2); assert_eq!(c.ease_factor, 1800); // interval doesn't change // we can bring cards forward too - return it to its original due date - c.set_due_date(6, 1, 2.4, false); + c.set_due_date(6, 0, 1, 2.4, false); assert_eq!(c.due, 7); assert_eq!(c.interval, 2); assert_eq!(c.ease_factor, 1800); // interval doesn't change // we can force the interval to be reset instead of shifted - c.set_due_date(6, 3, 2.3, true); + c.set_due_date(6, 0, 3, 2.3, true); assert_eq!(c.due, 9); assert_eq!(c.interval, 3); assert_eq!(c.ease_factor, 1800); // interval doesn't change @@ -259,7 +275,7 @@ mod test { c.original_deck_id = DeckId(1); c.due = -10000; c.queue = CardQueue::New; - c.set_due_date(6, 1, 2.2, false); + c.set_due_date(6, 0, 1, 2.2, false); assert_eq!(c.due, 7); assert_eq!(c.interval, 2); assert_eq!(c.ease_factor, 2200); @@ -271,7 +287,7 @@ mod test { c.ctype = CardType::Relearn; c.original_due = c.due; c.due = 12345678; - c.set_due_date(6, 10, 2.1, false); + c.set_due_date(6, 0, 10, 2.1, false); assert_eq!(c.due, 16); assert_eq!(c.interval, 2); assert_eq!(c.ease_factor, 2200); // interval doesn't change From c28306eb94f23c45f6695ac286cd722ff149c0d0 Mon Sep 17 00:00:00 2001 From: user1823 <92206575+user1823@users.noreply.github.com> Date: Sat, 21 Jun 2025 17:46:54 +0530 Subject: [PATCH 3/7] Save dr and decay in card even if item is None (#4106) * Document the purpose of storing dr and decay in card * Format * Fix type mismatch errors * Update memory_state.rs * Save dr and decay in card even if item is None * Format * Fix mismatched types * Update memory_state.rs --- rslib/src/scheduler/fsrs/memory_state.rs | 135 +++++++++++++---------- 1 file changed, 74 insertions(+), 61 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index f5af97674..d01cde767 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -105,77 +105,92 @@ impl Collection { progress.update(true, |state| state.current_cards = idx as u32 + 1)?; let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; let original = card.clone(); - if let (Some(req), Some(item)) = (&req, item) { - card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?; + if let Some(req) = &req { + // Store decay and desired retention in the card so that add-ons, card info, + // stats and browser search/sorts don't need to access the deck config. + // Unlike memory states, scheduler doesn't use decay and dr stored in the card. card.desired_retention = desired_retention; card.decay = decay; - // if rescheduling - if let Some(reviews) = &last_revlog_info { - // and we have a last review time for the card - if let Some(last_info) = reviews.get(&card.id) { - if let Some(last_review) = &last_info.last_reviewed_at { - let days_elapsed = - timing.next_day_at.elapsed_days_since(*last_review) as i32; - // and the card's not new - if let Some(state) = &card.memory_state { - // or in (re)learning - if card.ctype == CardType::Review { - let deck = self - .get_deck(card.original_or_current_deck_id())? - .or_not_found(card.original_or_current_deck_id())?; - let deckconfig_id = deck.config_id().unwrap(); - // reschedule it - let original_interval = card.interval; - let interval = fsrs.next_interval( - Some(state.stability), - card.desired_retention.unwrap(), - 0, - ); - card.interval = rescheduler - .as_mut() - .and_then(|r| { - r.find_interval( - interval, - 1, - req.max_interval, - days_elapsed as u32, - deckconfig_id, - get_fuzz_seed(&card, true), - ) - }) - .unwrap_or_else(|| { - with_review_fuzz( - card.get_fuzz_factor(true), - interval, - 1, - req.max_interval, - ) - }); - let due = if card.original_due != 0 { - &mut card.original_due - } else { - &mut card.due - }; - let new_due = (timing.days_elapsed as i32) - days_elapsed - + card.interval as i32; - if let Some(rescheduler) = &mut rescheduler { - rescheduler.update_due_cnt_per_day( - *due, - new_due, - deckconfig_id, + if let Some(item) = item { + card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?; + // if rescheduling + if let Some(reviews) = &last_revlog_info { + // and we have a last review time for the card + if let Some(last_info) = reviews.get(&card.id) { + if let Some(last_review) = &last_info.last_reviewed_at { + let days_elapsed = + timing.next_day_at.elapsed_days_since(*last_review) as i32; + // and the card's not new + if let Some(state) = &card.memory_state { + // or in (re)learning + if card.ctype == CardType::Review { + let deck = self + .get_deck(card.original_or_current_deck_id())? + .or_not_found(card.original_or_current_deck_id())?; + let deckconfig_id = deck.config_id().unwrap(); + // reschedule it + let original_interval = card.interval; + let interval = fsrs.next_interval( + Some(state.stability), + desired_retention.unwrap(), + 0, ); + card.interval = rescheduler + .as_mut() + .and_then(|r| { + r.find_interval( + interval, + 1, + req.max_interval, + days_elapsed as u32, + deckconfig_id, + get_fuzz_seed(&card, true), + ) + }) + .unwrap_or_else(|| { + with_review_fuzz( + card.get_fuzz_factor(true), + interval, + 1, + req.max_interval, + ) + }); + let due = if card.original_due != 0 { + &mut card.original_due + } else { + &mut card.due + }; + let new_due = (timing.days_elapsed as i32) + - days_elapsed + + card.interval as i32; + if let Some(rescheduler) = &mut rescheduler { + rescheduler.update_due_cnt_per_day( + *due, + new_due, + deckconfig_id, + ); + } + *due = new_due; + // Add a rescheduled revlog entry + self.log_rescheduled_review( + &card, + original_interval, + usn, + )?; } - *due = new_due; - // Add a rescheduled revlog entry - self.log_rescheduled_review(&card, original_interval, usn)?; } } } } + } else { + // clear memory states if item is None + card.memory_state = None; } } else { + // clear FSRS data if FSRS is disabled card.memory_state = None; card.desired_retention = None; + card.decay = None; } self.update_card_inner(&mut card, original, usn)?; } @@ -213,8 +228,6 @@ impl Collection { decay, }) } else { - card.memory_state = None; - card.desired_retention = None; Ok(ComputeMemoryStateResponse { state: None, desired_retention, From 5cc3a2276ba3adeea3e53bb532af3d343c7f475b Mon Sep 17 00:00:00 2001 From: user1823 <92206575+user1823@users.noreply.github.com> Date: Sat, 21 Jun 2025 17:47:18 +0530 Subject: [PATCH 4/7] Fix repeated ticks in reviews graph (#4108) Regressed in #4086 --- ts/routes/graphs/reviews.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 164fc4199..fd3786b5f 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -141,7 +141,7 @@ export function renderReviews( const yTickFormat = (n: number): string => { if (showTime) { - return timeSpan(n / 1000, true, false, TimespanUnit.Hours); + return timeSpan(n / 1000, true, true, TimespanUnit.Hours); } else { if (Math.round(n) != n) { return ""; From cfd448565ae0e9e917a0cdff1d3a5c5d89ac0fb0 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 21 Jun 2025 19:03:23 +0700 Subject: [PATCH 5/7] Fix sync-server separate compile https://forums.ankiweb.net/t/anki-25-06-beta/62271/96 --- rslib/sync/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/sync/Cargo.toml b/rslib/sync/Cargo.toml index e2d960503..7a8f8534a 100644 --- a/rslib/sync/Cargo.toml +++ b/rslib/sync/Cargo.toml @@ -13,4 +13,4 @@ path = "main.rs" name = "anki-sync-server" [dependencies] -anki.workspace = true +anki = { workspace = true, features = ["rustls"] } From 246fa75a35b3ace140ddc97b45f137eea07ddd1c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 21 Jun 2025 19:13:26 +0700 Subject: [PATCH 6/7] Create release wheel as part of normal build Avoids the need for a separate publish --- qt/release/{update.sh => build.sh} | 7 +++++-- qt/release/publish.sh | 13 ------------- tools/build | 4 +++- 3 files changed, 8 insertions(+), 16 deletions(-) rename qt/release/{update.sh => build.sh} (91%) delete mode 100755 qt/release/publish.sh diff --git a/qt/release/update.sh b/qt/release/build.sh similarity index 91% rename from qt/release/update.sh rename to qt/release/build.sh index ddd99c398..423638bc4 100755 --- a/qt/release/update.sh +++ b/qt/release/build.sh @@ -2,7 +2,7 @@ set -e -test -f update.sh || { +test -f build.sh || { echo "run from release folder" exit 1 } @@ -63,6 +63,9 @@ echo "Generated pyproject.toml with version $VERSION" # Show diff if .old file exists if [ -f pyproject.toml.old ]; then echo - echo "Differences from previous version:" + echo "Differences from previous release version:" diff -u --color=always pyproject.toml.old pyproject.toml || true fi + +echo "Building wheel..." +"$UV" build --wheel --out-dir "$PROJ_ROOT/out/wheels" diff --git a/qt/release/publish.sh b/qt/release/publish.sh deleted file mode 100755 index 273e34953..000000000 --- a/qt/release/publish.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# Get the project root (two levels up from qt/release) -PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" - -# Use extracted uv binary -UV="$PROJ_ROOT/out/extracted/uv/uv" - -rm -rf dist -"$UV" build --wheel - -#UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test) "$UV" publish --index testpypi -UV_PUBLISH_TOKEN=$(pass show w/pypi-api) "$UV" publish diff --git a/tools/build b/tools/build index 3df9456ed..4074ff98d 100755 --- a/tools/build +++ b/tools/build @@ -1,6 +1,8 @@ #!/bin/bash -set -e +set -eo pipefail + rm -rf out/wheels/* RELEASE=2 ./ninja wheels +(cd qt/release && ./build.sh) echo "wheels are in out/wheels" From 782645d92e25b7d84a8f75ae8cfc3eaca39d16de Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 21 Jun 2025 19:13:30 +0700 Subject: [PATCH 7/7] Bump beta version --- .version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.version b/.version index 9079196d0..a38238a29 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -25.06b4 +25.06b5