From 3f62f54f14056445fc3716e80f1eeddd326993c4 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 10:14:11 +1000 Subject: [PATCH 01/16] more perform_op() tweaks - pass the handler directly - reviewer special-cases for flags and notes are now applied at call site - drop the kind attribute on OpChanges which is not needed --- pylib/anki/collection.py | 5 --- qt/aqt/browser.py | 11 ++++--- qt/aqt/deckbrowser.py | 8 +++-- qt/aqt/editcurrent.py | 8 +++-- qt/aqt/editor.py | 2 +- qt/aqt/main.py | 19 +++++------ qt/aqt/operations/__init__.py | 14 -------- qt/aqt/operations/card.py | 18 +++++++++-- qt/aqt/operations/deck.py | 3 +- qt/aqt/operations/note.py | 3 +- qt/aqt/operations/tag.py | 12 +++++-- qt/aqt/overview.py | 6 ++-- qt/aqt/reviewer.py | 61 ++++++++++++++++++++++------------- qt/aqt/sidebar.py | 7 ++-- qt/aqt/table.py | 5 +-- qt/tools/genhooks_gui.py | 2 +- rslib/backend.proto | 29 ++++++----------- rslib/src/backend/ops.rs | 14 -------- 18 files changed, 114 insertions(+), 113 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 44a102bb7..1fe89a700 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -879,11 +879,6 @@ table.review-log {{ {revlog_style} }} assert_exhaustive(self._undo) assert False - def op_affects_study_queue(self, changes: OpChanges) -> bool: - if changes.kind == changes.SET_CARD_FLAG: - return False - return changes.card or changes.deck or changes.preference - def op_made_changes(self, changes: OpChanges) -> bool: for field in changes.DESCRIPTOR.fields: if field.name != "kind": diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 97174c2fa..2fc5ba1e2 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -24,7 +24,6 @@ from aqt.editor import Editor from aqt.exporting import ExportDialog from aqt.find_and_replace import FindAndReplaceDialog from aqt.main import ResetReason -from aqt.operations import OpMeta from aqt.operations.card import set_card_deck, set_card_flag from aqt.operations.collection import undo from aqt.operations.note import remove_notes @@ -128,12 +127,14 @@ class Browser(QMainWindow): gui_hooks.browser_will_show(self) self.show() - def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None: + def on_operation_did_execute( + self, changes: OpChanges, handler: Optional[object] + ) -> None: focused = current_top_level_widget() == self - self.table.op_executed(changes, meta, focused) - self.sidebar.op_executed(changes, meta, focused) + self.table.op_executed(changes, handler, focused) + self.sidebar.op_executed(changes, handler, focused) if changes.note or changes.notetype: - if meta.handler is not self.editor: + if handler is not self.editor: # fixme: this will leave the splitter shown, but with no current # note being edited note = self.editor.note diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 34c371eb6..1d2f58ec5 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -5,7 +5,7 @@ from __future__ import annotations from copy import deepcopy from dataclasses import dataclass -from typing import Any +from typing import Any, Optional import aqt from anki.collection import OpChanges @@ -76,8 +76,10 @@ class DeckBrowser: if self._refresh_needed: self.refresh() - def op_executed(self, changes: OpChanges, focused: bool) -> bool: - if self.mw.col.op_affects_study_queue(changes): + def op_executed( + self, changes: OpChanges, handler: Optional[object], focused: bool + ) -> bool: + if changes.study_queues: self._refresh_needed = True if focused: diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index 3a3a67a63..209623e37 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -1,11 +1,11 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from typing import Optional import aqt.editor from anki.collection import OpChanges from anki.errors import NotFoundError from aqt import gui_hooks -from aqt.operations import OpMeta from aqt.qt import * from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr @@ -31,8 +31,10 @@ class EditCurrent(QDialog): gui_hooks.operation_did_execute.append(self.on_operation_did_execute) self.show() - def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None: - if changes.editor and meta.handler is not self.editor: + def on_operation_did_execute( + self, changes: OpChanges, handler: Optional[object] + ) -> None: + if changes.editor and handler is not self.editor: # reload note note = self.editor.note try: diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 82f795994..999a8dfa6 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -100,7 +100,7 @@ class Editor: redrawing. The editor will cause that hook to be fired when it saves changes. To avoid - an unwanted refresh, the parent widget should check if meta.handler + an unwanted refresh, the parent widget should check if handler corresponds to this editor instance, and ignore the change if it does. """ diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 6611f5cd1..7090c4a6b 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -61,7 +61,6 @@ from aqt.emptycards import show_empty_cards from aqt.legacy import install_pylib_legacy from aqt.mediacheck import check_media_db from aqt.mediasync import MediaSyncer -from aqt.operations import OpMeta from aqt.operations.collection import undo from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * @@ -773,7 +772,7 @@ class AnkiQt(QMainWindow): success: PerformOpOptionalSuccessCallback = None, failure: PerformOpOptionalFailureCallback = None, after_hooks: Optional[Callable[[], None]] = None, - meta: OpMeta = OpMeta(), + handler: Optional[object] = None, ) -> None: """Run the provided operation on a background thread. @@ -827,7 +826,7 @@ class AnkiQt(QMainWindow): status = self.col.undo_status() self._update_undo_actions_for_status_and_save(status) # fire change hooks - self._fire_change_hooks_after_op_performed(result, after_hooks, meta) + self._fire_change_hooks_after_op_performed(result, after_hooks, handler) self.taskman.with_progress(op, wrapped_done) @@ -846,7 +845,7 @@ class AnkiQt(QMainWindow): self, result: ResultWithChanges, after_hooks: Optional[Callable[[], None]], - meta: OpMeta, + handler: Optional[object], ) -> None: if isinstance(result, OpChanges): changes = result @@ -856,7 +855,7 @@ class AnkiQt(QMainWindow): # fire new hook print("op changes:") print(changes) - gui_hooks.operation_did_execute(changes, meta) + gui_hooks.operation_did_execute(changes, handler) # fire legacy hook so old code notices changes if self.col.op_made_changes(changes): gui_hooks.state_did_reset() @@ -872,15 +871,17 @@ class AnkiQt(QMainWindow): setattr(op, field.name, True) gui_hooks.operation_did_execute(op, None) - def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None: + def on_operation_did_execute( + self, changes: OpChanges, handler: Optional[object] + ) -> None: "Notify current screen of changes." focused = current_top_level_widget() == self if self.state == "review": - dirty = self.reviewer.op_executed(changes, focused) + dirty = self.reviewer.op_executed(changes, handler, focused) elif self.state == "overview": - dirty = self.overview.op_executed(changes, focused) + dirty = self.overview.op_executed(changes, handler, focused) elif self.state == "deckBrowser": - dirty = self.deckBrowser.op_executed(changes, focused) + dirty = self.deckBrowser.op_executed(changes, handler, focused) else: dirty = False diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index dd3d825c6..e3d8b5638 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -1,16 +1,2 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -from dataclasses import dataclass -from typing import Optional - - -@dataclass -class OpMeta: - """Metadata associated with an operation. - - The `handler` field can be used by screens to ignore change - events they initiated themselves, if they have already made - the required changes.""" - - handler: Optional[object] = None diff --git a/qt/aqt/operations/card.py b/qt/aqt/operations/card.py index 91c4ac0d2..fd994c475 100644 --- a/qt/aqt/operations/card.py +++ b/qt/aqt/operations/card.py @@ -3,16 +3,28 @@ from __future__ import annotations -from typing import Sequence +from typing import Optional, Sequence from anki.cards import CardId from anki.decks import DeckId from aqt import AnkiQt +from aqt.main import PerformOpOptionalSuccessCallback def set_card_deck(*, mw: AnkiQt, card_ids: Sequence[CardId], deck_id: DeckId) -> None: mw.perform_op(lambda: mw.col.set_deck(card_ids, deck_id)) -def set_card_flag(*, mw: AnkiQt, card_ids: Sequence[CardId], flag: int) -> None: - mw.perform_op(lambda: mw.col.set_user_flag_for_cards(flag, card_ids)) +def set_card_flag( + *, + mw: AnkiQt, + card_ids: Sequence[CardId], + flag: int, + handler: Optional[object] = None, + success: PerformOpOptionalSuccessCallback = None, +) -> None: + mw.perform_op( + lambda: mw.col.set_user_flag_for_cards(flag, card_ids), + handler=handler, + success=success, + ) diff --git a/qt/aqt/operations/deck.py b/qt/aqt/operations/deck.py index e32e69b0c..fc6ca6250 100644 --- a/qt/aqt/operations/deck.py +++ b/qt/aqt/operations/deck.py @@ -8,7 +8,6 @@ from typing import Callable, Optional, Sequence from anki.decks import DeckCollapseScope, DeckId from aqt import AnkiQt, QWidget from aqt.main import PerformOpOptionalSuccessCallback -from aqt.operations import OpMeta from aqt.utils import getOnlyText, tooltip, tr @@ -83,5 +82,5 @@ def set_deck_collapsed( lambda: mw.col.decks.set_collapsed( deck_id=deck_id, collapsed=collapsed, scope=scope ), - meta=OpMeta(handler=handler), + handler=handler, ) diff --git a/qt/aqt/operations/note.py b/qt/aqt/operations/note.py index 4c0d8c3d2..dfe28ba92 100644 --- a/qt/aqt/operations/note.py +++ b/qt/aqt/operations/note.py @@ -9,7 +9,6 @@ from anki.decks import DeckId from anki.notes import Note, NoteId from aqt import AnkiQt from aqt.main import PerformOpOptionalSuccessCallback -from aqt.operations import OpMeta def add_note( @@ -25,7 +24,7 @@ def add_note( def update_note(*, mw: AnkiQt, note: Note, handler: Optional[object]) -> None: mw.perform_op( lambda: mw.col.update_note(note), - meta=OpMeta(handler=handler), + handler=handler, ) diff --git a/qt/aqt/operations/tag.py b/qt/aqt/operations/tag.py index ee1e767cb..49b38cb2c 100644 --- a/qt/aqt/operations/tag.py +++ b/qt/aqt/operations/tag.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Callable, Sequence +from typing import Callable, Optional, Sequence from anki.collection import OpChangesWithCount from anki.notes import NoteId @@ -18,9 +18,12 @@ def add_tags_to_notes( note_ids: Sequence[NoteId], space_separated_tags: str, success: PerformOpOptionalSuccessCallback = None, + handler: Optional[object] = None, ) -> None: mw.perform_op( - lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags), success=success + lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags), + success=success, + handler=handler, ) @@ -30,9 +33,12 @@ def remove_tags_from_notes( note_ids: Sequence[NoteId], space_separated_tags: str, success: PerformOpOptionalSuccessCallback = None, + handler: Optional[object] = None, ) -> None: mw.perform_op( - lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags), success=success + lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags), + success=success, + handler=handler, ) diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 8d2df0d76..2e7e01ea7 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -64,8 +64,10 @@ class Overview: if self._refresh_needed: self.refresh() - def op_executed(self, changes: OpChanges, focused: bool) -> bool: - if self.mw.col.op_affects_study_queue(changes): + def op_executed( + self, changes: OpChanges, handler: Optional[object], focused: bool + ) -> bool: + if changes.study_queues: self._refresh_needed = True if focused: diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index feb9d8e4d..3f665d1a4 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -14,7 +14,7 @@ from PyQt5.QtCore import Qt from anki import hooks from anki.cards import Card, CardId -from anki.collection import Config, OpChanges +from anki.collection import Config, OpChanges, OpChangesWithCount from anki.tags import MARKED_TAG from anki.utils import stripHTML from aqt import AnkiQt, gui_hooks @@ -38,7 +38,6 @@ from aqt.webview import AnkiWebView class RefreshNeeded(Enum): - NO = auto() NOTE_TEXT = auto() QUEUES = auto() @@ -71,7 +70,7 @@ class Reviewer: self._recordedAudio: Optional[str] = None self.typeCorrect: str = None # web init happens before this is set self.state: Optional[str] = None - self._refresh_needed = RefreshNeeded.NO + self._refresh_needed: Optional[RefreshNeeded] = None self.bottom = BottomBar(mw, mw.bottomWeb) hooks.card_did_leech.append(self.onLeech) @@ -102,29 +101,25 @@ class Reviewer: self.mw.col.reset() self.nextCard() self.mw.fade_in_webview() - self._refresh_needed = RefreshNeeded.NO + self._refresh_needed = None elif self._refresh_needed is RefreshNeeded.NOTE_TEXT: self._redraw_current_card() self.mw.fade_in_webview() - self._refresh_needed = RefreshNeeded.NO + self._refresh_needed = None - def op_executed(self, changes: OpChanges, focused: bool) -> bool: - if changes.note and changes.kind == OpChanges.UPDATE_NOTE_TAGS: - self.card.load() - self._update_mark_icon() - elif changes.card and changes.kind == OpChanges.SET_CARD_FLAG: - # fixme: v3 mtime check - self.card.load() - self._update_flag_icon() - elif self.mw.col.op_affects_study_queue(changes): - self._refresh_needed = RefreshNeeded.QUEUES - elif changes.note or changes.notetype or changes.tag: - self._refresh_needed = RefreshNeeded.NOTE_TEXT + def op_executed( + self, changes: OpChanges, handler: Optional[object], focused: bool + ) -> bool: + if handler is not self: + if changes.study_queues: + self._refresh_needed = RefreshNeeded.QUEUES + elif changes.editor: + self._refresh_needed = RefreshNeeded.NOTE_TEXT - if focused and self._refresh_needed is not RefreshNeeded.NO: + if focused and self._refresh_needed: self.refresh_if_needed() - return self._refresh_needed is not RefreshNeeded.NO + return bool(self._refresh_needed) def _redraw_current_card(self) -> None: self.card.load() @@ -830,23 +825,45 @@ time = %(time)d; self.mw.onDeckConf(self.mw.col.decks.get(self.card.current_deck_id())) def set_flag_on_current_card(self, desired_flag: int) -> None: + def redraw_flag(out: OpChanges) -> None: + self.card.load() + self._update_flag_icon() + # need to toggle off? if self.card.user_flag() == desired_flag: flag = 0 else: flag = desired_flag - set_card_flag(mw=self.mw, card_ids=[self.card.id], flag=flag) + set_card_flag( + mw=self.mw, + card_ids=[self.card.id], + flag=flag, + handler=self, + success=redraw_flag, + ) def toggle_mark_on_current_note(self) -> None: + def redraw_mark(out: OpChangesWithCount) -> None: + self.card.load() + self._update_mark_icon() + note = self.card.note() if note.has_tag(MARKED_TAG): remove_tags_from_notes( - mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG + mw=self.mw, + note_ids=[note.id], + space_separated_tags=MARKED_TAG, + handler=self, + success=redraw_mark, ) else: add_tags_to_notes( - mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG + mw=self.mw, + note_ids=[note.id], + space_separated_tags=MARKED_TAG, + handler=self, + success=redraw_mark, ) def on_set_due(self) -> None: diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f8d83c354..e9b9b1d1c 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -16,7 +16,6 @@ from anki.types import assert_exhaustive from aqt import colors, gui_hooks from aqt.clayout import CardLayout from aqt.models import Models -from aqt.operations import OpMeta from aqt.operations.deck import ( remove_decks, rename_deck, @@ -420,8 +419,10 @@ class SidebarTreeView(QTreeView): # Refreshing ########################### - def op_executed(self, changes: OpChanges, meta: OpMeta, focused: bool) -> None: - if changes.browser_sidebar and not meta.handler is self: + def op_executed( + self, changes: OpChanges, handler: Optional[object], focused: bool + ) -> None: + if changes.browser_sidebar and not handler is self: self._refresh_needed = True if focused: self.refresh_if_needed() diff --git a/qt/aqt/table.py b/qt/aqt/table.py index aec9d7b71..cc94b8f44 100644 --- a/qt/aqt/table.py +++ b/qt/aqt/table.py @@ -29,7 +29,6 @@ from anki.errors import NotFoundError from anki.notes import Note, NoteId from anki.utils import ids2str, isWin from aqt import colors, gui_hooks -from aqt.operations import OpMeta from aqt.qt import * from aqt.theme import theme_manager from aqt.utils import ( @@ -181,7 +180,9 @@ class Table: def redraw_cells(self) -> None: self._model.redraw_cells() - def op_executed(self, changes: OpChanges, meta: OpMeta, focused: bool) -> None: + def op_executed( + self, changes: OpChanges, handler: Optional[object], focused: bool + ) -> None: if changes.browser_table: self._model.mark_cache_stale() if focused: diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index ebcf373b1..a939112cf 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -459,7 +459,7 @@ hooks = [ ), Hook( name="operation_did_execute", - args=["changes: anki.collection.OpChanges", "meta: aqt.operations.OpMeta"], + args=["changes: anki.collection.OpChanges", "handler: Optional[object]"], doc="""Called after an operation completes. Changes can be inspected to determine whether the UI needs updating. diff --git a/rslib/backend.proto b/rslib/backend.proto index 06fe920e1..e5430c48d 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -1498,26 +1498,17 @@ message GetQueuedCardsOut { } message OpChanges { - // this is not an exhaustive list; we can add more cases as we need them - enum Kind { - OTHER = 0; - UPDATE_NOTE_TAGS = 1; - SET_CARD_FLAG = 2; - UPDATE_NOTE = 3; - } + bool card = 1; + bool note = 2; + bool deck = 3; + bool tag = 4; + bool notetype = 5; + bool preference = 6; - Kind kind = 1; - bool card = 2; - bool note = 3; - bool deck = 4; - bool tag = 5; - bool notetype = 6; - bool preference = 7; - - bool browser_table = 8; - bool browser_sidebar = 9; - bool editor = 10; - bool study_queues = 11; + bool browser_table = 7; + bool browser_sidebar = 8; + bool editor = 9; + bool study_queues = 10; } message UndoStatus { diff --git a/rslib/src/backend/ops.rs b/rslib/src/backend/ops.rs index d634093ab..001357923 100644 --- a/rslib/src/backend/ops.rs +++ b/rslib/src/backend/ops.rs @@ -1,8 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use pb::op_changes::Kind; - use crate::{ backend_proto as pb, ops::OpChanges, @@ -10,21 +8,9 @@ use crate::{ undo::{UndoOutput, UndoStatus}, }; -impl From for Kind { - fn from(o: Op) -> Self { - match o { - Op::SetFlag => Kind::SetCardFlag, - Op::UpdateTag => Kind::UpdateNoteTags, - Op::UpdateNote => Kind::UpdateNote, - _ => Kind::Other, - } - } -} - impl From for pb::OpChanges { fn from(c: OpChanges) -> Self { pb::OpChanges { - kind: Kind::from(c.op) as i32, card: c.changes.card, note: c.changes.note, deck: c.changes.deck, From 1ece868d029bb7ed360b07df5434d915957e63b4 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 11:18:13 +1000 Subject: [PATCH 02/16] shift keep-current-selection logic into sidebar's refresh() By calling refresh() manually after performing an op, we were refreshing twice, and the selection was being lost when changes were made outside of the sidebar. Also drop the after_hooks arg to perform_op(), since nothing is using it now. --- qt/aqt/main.py | 10 +------ qt/aqt/operations/deck.py | 5 ++-- qt/aqt/operations/tag.py | 4 +-- qt/aqt/sidebar.py | 59 ++++++++++++++++++++++----------------- 4 files changed, 37 insertions(+), 41 deletions(-) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 7090c4a6b..be5f4bf7a 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -771,7 +771,6 @@ class AnkiQt(QMainWindow): *, success: PerformOpOptionalSuccessCallback = None, failure: PerformOpOptionalFailureCallback = None, - after_hooks: Optional[Callable[[], None]] = None, handler: Optional[object] = None, ) -> None: """Run the provided operation on a background thread. @@ -795,10 +794,6 @@ class AnkiQt(QMainWindow): If op() throws an exception, it will be shown in a popup, or passed to failure() if it is provided. - - after_hooks() will be called after hooks are fired, if it is provided. - Components can use this to ignore change notices generated by operations - they invoke themselves, or perform some subsequent action. """ self._increase_background_ops() @@ -826,7 +821,7 @@ class AnkiQt(QMainWindow): status = self.col.undo_status() self._update_undo_actions_for_status_and_save(status) # fire change hooks - self._fire_change_hooks_after_op_performed(result, after_hooks, handler) + self._fire_change_hooks_after_op_performed(result, handler) self.taskman.with_progress(op, wrapped_done) @@ -844,7 +839,6 @@ class AnkiQt(QMainWindow): def _fire_change_hooks_after_op_performed( self, result: ResultWithChanges, - after_hooks: Optional[Callable[[], None]], handler: Optional[object], ) -> None: if isinstance(result, OpChanges): @@ -859,8 +853,6 @@ class AnkiQt(QMainWindow): # fire legacy hook so old code notices changes if self.col.op_made_changes(changes): gui_hooks.state_did_reset() - if after_hooks: - after_hooks() def _synthesize_op_did_execute_from_reset(self) -> None: """Fire the `operation_did_execute` hook with everything marked as changed, diff --git a/qt/aqt/operations/deck.py b/qt/aqt/operations/deck.py index fc6ca6250..4081626cf 100644 --- a/qt/aqt/operations/deck.py +++ b/qt/aqt/operations/deck.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Callable, Optional, Sequence +from typing import Optional, Sequence from anki.decks import DeckCollapseScope, DeckId from aqt import AnkiQt, QWidget @@ -41,10 +41,9 @@ def rename_deck( mw: AnkiQt, deck_id: DeckId, new_name: str, - after_rename: Callable[[], None] = None, ) -> None: mw.perform_op( - lambda: mw.col.decks.rename(deck_id, new_name), after_hooks=after_rename + lambda: mw.col.decks.rename(deck_id, new_name), ) diff --git a/qt/aqt/operations/tag.py b/qt/aqt/operations/tag.py index 49b38cb2c..0f22ff4cb 100644 --- a/qt/aqt/operations/tag.py +++ b/qt/aqt/operations/tag.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Callable, Optional, Sequence +from typing import Optional, Sequence from anki.collection import OpChangesWithCount from anki.notes import NoteId @@ -57,7 +57,6 @@ def rename_tag( parent: QWidget, current_name: str, new_name: str, - after_rename: Callable[[], None], ) -> None: def success(out: OpChangesWithCount) -> None: if out.count: @@ -68,7 +67,6 @@ def rename_tag( mw.perform_op( lambda: mw.col.tags.rename(old=current_name, new=new_name), success=success, - after_hooks=after_rename, ) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index e9b9b1d1c..22bec6766 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -172,6 +172,18 @@ class SidebarItem: ) return self._search_matches_self or self._search_matches_child + def has_same_id(self, other: SidebarItem) -> bool: + "True if `other` is same type, with same id/name." + if other.item_type == self.item_type: + if self.item_type == SidebarItemType.TAG: + return self.full_name == other.full_name + elif self.item_type == SidebarItemType.SAVED_SEARCH: + return self.name == other.name + else: + return other.id == self.id + + return False + class SidebarModel(QAbstractItemModel): def __init__(self, sidebar: SidebarTreeView, root: SidebarItem) -> None: @@ -432,13 +444,16 @@ class SidebarTreeView(QTreeView): self.refresh() self._refresh_needed = False - def refresh( - self, is_current: Optional[Callable[[SidebarItem], bool]] = None - ) -> None: + def refresh(self) -> None: "Refresh list. No-op if sidebar is not visible." if not self.isVisible(): return + if self.model() and (idx := self.currentIndex()): + current_item = self.model().item_for_index(idx) + else: + current_item = None + def on_done(root: SidebarItem) -> None: # user may have closed browser if sip.isdeleted(self): @@ -454,8 +469,8 @@ class SidebarTreeView(QTreeView): self.search_for(self.current_search) else: self._expand_where_necessary(model) - if is_current: - self.restore_current(is_current) + if current_item: + self.restore_current(current_item) self.setUpdatesEnabled(True) @@ -464,8 +479,8 @@ class SidebarTreeView(QTreeView): self.mw.query_op(self._root_tree, success=on_done) - def restore_current(self, is_current: Callable[[SidebarItem], bool]) -> None: - if current := self.find_item(is_current): + def restore_current(self, current: SidebarItem) -> None: + if current := self.find_item(current.has_same_id): index = self.model().index_for_item(current) self.selectionModel().setCurrentIndex( index, QItemSelectionModel.SelectCurrent @@ -1165,21 +1180,21 @@ class SidebarTreeView(QTreeView): def rename_deck(self, item: SidebarItem, new_name: str) -> None: if not new_name: return - new_name = item.name_prefix + new_name + + # update UI immediately, to avoid redraw + item.name = new_name + + full_name = item.name_prefix + new_name deck_id = DeckId(item.id) def after_fetch(deck: Deck) -> None: - if new_name == deck.name: + if full_name == deck.name: return rename_deck( mw=self.mw, deck_id=deck_id, - new_name=new_name, - after_rename=lambda: self.refresh( - lambda other: other.item_type == SidebarItemType.DECK - and other.id == item.id - ), + new_name=full_name, ) self.mw.query_op(lambda: self.mw.col.get_deck(deck_id), success=after_fetch) @@ -1208,16 +1223,13 @@ class SidebarTreeView(QTreeView): new_name = item.name_prefix + new_name item.name = new_name_base + item.full_name = new_name rename_tag( mw=self.mw, parent=self.browser, current_name=old_name, new_name=new_name, - after_rename=lambda: self.refresh( - lambda item: item.item_type == SidebarItemType.TAG - and item.full_name == new_name - ), ) # Saved searches @@ -1250,10 +1262,7 @@ class SidebarTreeView(QTreeView): return conf[name] = search self._set_saved_searches(conf) - self.refresh( - lambda item: item.item_type == SidebarItemType.SAVED_SEARCH - and item.name == name - ) + self.refresh() def remove_saved_searches(self, _item: SidebarItem) -> None: selected = self._selected_saved_searches() @@ -1277,10 +1286,8 @@ class SidebarTreeView(QTreeView): conf[new_name] = filt del conf[old_name] self._set_saved_searches(conf) - self.refresh( - lambda item: item.item_type == SidebarItemType.SAVED_SEARCH - and item.name == new_name - ) + item.name = new_name + self.refresh() def save_current_search(self) -> None: if (search := self._get_current_search()) is None: From b8fc195cdfd5cc5a396ca23eada3e548cbb869a8 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 12:47:55 +1000 Subject: [PATCH 03/16] start migrating perform_op() into builder in separate file By passing back the builder to the calling code to run, we don't need to plumb extra arguments like success= and handler= through each operation, and the ability to override the default tooltip behaviour comes free on all operations --- qt/aqt/browser.py | 21 ++---- qt/aqt/main.py | 35 +++------ qt/aqt/operations/__init__.py | 131 ++++++++++++++++++++++++++++++++++ qt/aqt/operations/tag.py | 84 ++++++++++------------ qt/aqt/reviewer.py | 14 ++-- qt/aqt/sidebar.py | 13 ++-- qt/tools/genhooks_gui.py | 1 - 7 files changed, 196 insertions(+), 103 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 2fc5ba1e2..d4eadfaee 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -695,13 +695,8 @@ where id in %s""" if not (tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_add())): return add_tags_to_notes( - mw=self.mw, - note_ids=self.selected_notes(), - space_separated_tags=tags, - success=lambda out: tooltip( - tr.browsing_notes_updated(count=out.count), parent=self - ), - ) + parent=self, note_ids=self.selected_notes(), space_separated_tags=tags + ).run(handler=self) @ensure_editor_saved_on_trigger def remove_tags_from_selected_notes(self, tags: Optional[str] = None) -> None: @@ -710,14 +705,10 @@ where id in %s""" tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_delete()) ): return + remove_tags_from_notes( - mw=self.mw, - note_ids=self.selected_notes(), - space_separated_tags=tags, - success=lambda out: tooltip( - tr.browsing_notes_updated(count=out.count), parent=self - ), - ) + parent=self, note_ids=self.selected_notes(), space_separated_tags=tags + ).run(handler=self) def _prompt_for_tags(self, prompt: str) -> Optional[str]: (tags, ok) = getTag(self, self.col, prompt) @@ -728,7 +719,7 @@ where id in %s""" @ensure_editor_saved_on_trigger def clear_unused_tags(self) -> None: - clear_unused_tags(mw=self.mw, parent=self) + clear_unused_tags(parent=self).run() addTags = add_tags_to_selected_notes deleteTags = remove_tags_from_selected_notes diff --git a/qt/aqt/main.py b/qt/aqt/main.py index be5f4bf7a..7dedba7dd 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -61,6 +61,11 @@ from aqt.emptycards import show_empty_cards from aqt.legacy import install_pylib_legacy from aqt.mediacheck import check_media_db from aqt.mediasync import MediaSyncer +from aqt.operations import ( + CollectionOpFailureCallback, + CollectionOpSuccessCallback, + ResultWithChanges, +) from aqt.operations.collection import undo from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * @@ -91,30 +96,6 @@ from aqt.utils import ( tr, ) - -class HasChangesProperty(Protocol): - changes: OpChanges - - -# either an OpChanges object, or an object with .changes on it. This bound -# doesn't actually work for protobuf objects, so new protobuf objects will -# either need to be added here, or cast at call time -ResultWithChanges = TypeVar( - "ResultWithChanges", - bound=Union[ - OpChanges, - OpChangesWithCount, - OpChangesWithId, - OpChangesAfterUndo, - HasChangesProperty, - ], -) - -T = TypeVar("T") - -PerformOpOptionalSuccessCallback = Optional[Callable[[ResultWithChanges], Any]] -PerformOpOptionalFailureCallback = Optional[Callable[[Exception], Any]] - install_pylib_legacy() MainWindowState = Literal[ @@ -122,6 +103,12 @@ MainWindowState = Literal[ ] +T = TypeVar("T") + +PerformOpOptionalSuccessCallback = Optional[CollectionOpSuccessCallback] +PerformOpOptionalFailureCallback = Optional[CollectionOpFailureCallback] + + class AnkiQt(QMainWindow): col: Collection pm: ProfileManagerType diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index e3d8b5638..cb7b4b5d9 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -1,2 +1,133 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +from concurrent.futures._base import Future +from typing import Any, Callable, Generic, Optional, Protocol, TypeVar, Union + +import aqt +from anki.collection import ( + Collection, + OpChanges, + OpChangesAfterUndo, + OpChangesWithCount, + OpChangesWithId, +) +from aqt.qt import QWidget +from aqt.utils import showWarning + + +class HasChangesProperty(Protocol): + changes: OpChanges + + +# either an OpChanges object, or an object with .changes on it. This bound +# doesn't actually work for protobuf objects, so new protobuf objects will +# either need to be added here, or cast at call time +ResultWithChanges = TypeVar( + "ResultWithChanges", + bound=Union[ + OpChanges, + OpChangesWithCount, + OpChangesWithId, + OpChangesAfterUndo, + HasChangesProperty, + ], +) + +T = TypeVar("T") + +CollectionOpSuccessCallback = Callable[[ResultWithChanges], Any] +CollectionOpFailureCallback = Optional[Callable[[Exception], Any]] + + +class CollectionOp(Generic[ResultWithChanges]): + """Helper to perform a mutating DB operation on a background thread, and update UI. + + `op` should either return OpChanges, or an object with a 'changes' + property. The changes will be passed to `operation_did_execute` so that + the UI can decide whether it needs to update itself. + + - Shows progress popup for the duration of the op. + - Ensures the browser doesn't try to redraw during the operation, which can lead + to a frozen UI + - Updates undo state at the end of the operation + - Commits changes + - Fires the `operation_(will|did)_reset` hooks + - Fires the legacy `state_did_reset` hook + + Be careful not to call any UI routines in `op`, as that may crash Qt. + This includes things select .selectedCards() in the browse screen. + + `success` will be called with the return value of op(). + + If op() throws an exception, it will be shown in a popup, or + passed to `failure` if it is provided. + """ + + _success: Optional[CollectionOpSuccessCallback] = None + _failure: Optional[CollectionOpFailureCallback] = None + + def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]): + self._parent = parent + self._op = op + + def success(self, success: Optional[CollectionOpSuccessCallback]) -> CollectionOp: + self._success = success + return self + + def failure(self, failure: Optional[CollectionOpFailureCallback]) -> CollectionOp: + self._failure = failure + return self + + def run(self, *, handler: Optional[object] = None) -> None: + aqt.mw._increase_background_ops() + + def wrapped_op() -> ResultWithChanges: + return self._op(aqt.mw.col) + + def wrapped_done(future: Future) -> None: + aqt.mw._decrease_background_ops() + # did something go wrong? + if exception := future.exception(): + if isinstance(exception, Exception): + if self._failure: + self._failure(exception) + else: + showWarning(str(exception), self._parent) + return + else: + # BaseException like SystemExit; rethrow it + future.result() + + result = future.result() + try: + if self._success: + self._success(result) + finally: + # update undo status + status = aqt.mw.col.undo_status() + aqt.mw._update_undo_actions_for_status_and_save(status) + # fire change hooks + self._fire_change_hooks_after_op_performed(result, handler) + + aqt.mw.taskman.with_progress(wrapped_op, wrapped_done) + + def _fire_change_hooks_after_op_performed( + self, + result: ResultWithChanges, + handler: Optional[object], + ) -> None: + if isinstance(result, OpChanges): + changes = result + else: + changes = result.changes + + # fire new hook + print("op changes:") + print(changes) + aqt.gui_hooks.operation_did_execute(changes, handler) + # fire legacy hook so old code notices changes + if aqt.mw.col.op_made_changes(changes): + aqt.gui_hooks.state_did_reset() diff --git a/qt/aqt/operations/tag.py b/qt/aqt/operations/tag.py index 0f22ff4cb..6c2ea7611 100644 --- a/qt/aqt/operations/tag.py +++ b/qt/aqt/operations/tag.py @@ -3,94 +3,88 @@ from __future__ import annotations -from typing import Optional, Sequence +from typing import Sequence from anki.collection import OpChangesWithCount from anki.notes import NoteId from aqt import AnkiQt, QWidget -from aqt.main import PerformOpOptionalSuccessCallback +from aqt.operations import CollectionOp from aqt.utils import showInfo, tooltip, tr def add_tags_to_notes( *, - mw: AnkiQt, + parent: QWidget, note_ids: Sequence[NoteId], space_separated_tags: str, - success: PerformOpOptionalSuccessCallback = None, - handler: Optional[object] = None, -) -> None: - mw.perform_op( - lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags), - success=success, - handler=handler, +) -> CollectionOp: + return CollectionOp( + parent, lambda col: col.tags.bulk_add(note_ids, space_separated_tags) + ).success( + lambda out: tooltip(tr.browsing_notes_updated(count=out.count), parent=parent) ) def remove_tags_from_notes( *, - mw: AnkiQt, + parent: QWidget, note_ids: Sequence[NoteId], space_separated_tags: str, - success: PerformOpOptionalSuccessCallback = None, - handler: Optional[object] = None, -) -> None: - mw.perform_op( - lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags), - success=success, - handler=handler, +) -> CollectionOp: + return CollectionOp( + parent, lambda col: col.tags.bulk_remove(note_ids, space_separated_tags) + ).success( + lambda out: tooltip(tr.browsing_notes_updated(count=out.count), parent=parent) ) -def clear_unused_tags(*, mw: AnkiQt, parent: QWidget) -> None: - mw.perform_op( - mw.col.tags.clear_unused_tags, - success=lambda out: tooltip( +def clear_unused_tags(*, parent: QWidget) -> CollectionOp: + return CollectionOp(parent, lambda col: col.tags.clear_unused_tags()).success( + lambda out: tooltip( tr.browsing_removed_unused_tags_count(count=out.count), parent=parent - ), + ) ) def rename_tag( *, - mw: AnkiQt, parent: QWidget, current_name: str, new_name: str, -) -> None: +) -> CollectionOp: def success(out: OpChangesWithCount) -> None: if out.count: tooltip(tr.browsing_notes_updated(count=out.count), parent=parent) else: showInfo(tr.browsing_tag_rename_warning_empty(), parent=parent) - mw.perform_op( - lambda: mw.col.tags.rename(old=current_name, new=new_name), - success=success, - ) + return CollectionOp( + parent, + lambda col: col.tags.rename(old=current_name, new=new_name), + ).success(success) def remove_tags_from_all_notes( - *, mw: AnkiQt, parent: QWidget, space_separated_tags: str -) -> None: - mw.perform_op( - lambda: mw.col.tags.remove(space_separated_tags=space_separated_tags), - success=lambda out: tooltip( - tr.browsing_notes_updated(count=out.count), parent=parent - ), + *, parent: QWidget, space_separated_tags: str +) -> CollectionOp: + return CollectionOp( + parent, lambda col: col.tags.remove(space_separated_tags=space_separated_tags) + ).success( + lambda out: tooltip(tr.browsing_notes_updated(count=out.count), parent=parent) ) def reparent_tags( - *, mw: AnkiQt, parent: QWidget, tags: Sequence[str], new_parent: str -) -> None: - mw.perform_op( - lambda: mw.col.tags.reparent(tags=tags, new_parent=new_parent), - success=lambda out: tooltip( - tr.browsing_notes_updated(count=out.count), parent=parent - ), + *, parent: QWidget, tags: Sequence[str], new_parent: str +) -> CollectionOp: + return CollectionOp( + parent, lambda col: col.tags.reparent(tags=tags, new_parent=new_parent) + ).success( + lambda out: tooltip(tr.browsing_notes_updated(count=out.count), parent=parent) ) -def set_tag_collapsed(*, mw: AnkiQt, tag: str, collapsed: bool) -> None: - mw.perform_op(lambda: mw.col.tags.set_collapsed(tag=tag, collapsed=collapsed)) +def set_tag_collapsed(*, parent: QWidget, tag: str, collapsed: bool) -> CollectionOp: + return CollectionOp( + parent, lambda col: col.tags.set_collapsed(tag=tag, collapsed=collapsed) + ) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 3f665d1a4..cebbf24a3 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -851,20 +851,14 @@ time = %(time)d; note = self.card.note() if note.has_tag(MARKED_TAG): remove_tags_from_notes( - mw=self.mw, - note_ids=[note.id], - space_separated_tags=MARKED_TAG, - handler=self, - success=redraw_mark, - ) + parent=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG + ).success(redraw_mark).run(handler=self) else: add_tags_to_notes( - mw=self.mw, + parent=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG, - handler=self, - success=redraw_mark, - ) + ).success(redraw_mark).run(handler=self) def on_set_due(self) -> None: if self.mw.state != "review" or not self.card: diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 22bec6766..77cf27a75 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -652,7 +652,7 @@ class SidebarTreeView(QTreeView): else: new_parent = target.full_name - reparent_tags(mw=self.mw, parent=self.browser, tags=tags, new_parent=new_parent) + reparent_tags(parent=self.browser, tags=tags, new_parent=new_parent).run() return True @@ -947,8 +947,8 @@ class SidebarTreeView(QTreeView): def toggle_expand(node: TagTreeNode) -> Callable[[bool], None]: full_name = head + node.name return lambda expanded: set_tag_collapsed( - mw=self.mw, tag=full_name, collapsed=not expanded - ) + parent=self, tag=full_name, collapsed=not expanded + ).run() for node in nodes: item = SidebarItem( @@ -1209,9 +1209,7 @@ class SidebarTreeView(QTreeView): tags = self.mw.col.tags.join(self._selected_tags()) item.name = "..." - remove_tags_from_all_notes( - mw=self.mw, parent=self.browser, space_separated_tags=tags - ) + remove_tags_from_all_notes(parent=self.browser, space_separated_tags=tags).run() def rename_tag(self, item: SidebarItem, new_name: str) -> None: if not new_name or new_name == item.name: @@ -1226,11 +1224,10 @@ class SidebarTreeView(QTreeView): item.full_name = new_name rename_tag( - mw=self.mw, parent=self.browser, current_name=old_name, new_name=new_name, - ) + ).run() # Saved searches #################################### diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index a939112cf..f4b50570f 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -26,7 +26,6 @@ from anki.hooks import runFilter, runHook from anki.models import NotetypeDict from aqt.qt import QDialog, QEvent, QMenu, QWidget from aqt.tagedit import TagEdit -import aqt.operations """ # Hook list From 27c032a1581deb774d5681034ffd0a3932208fde Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 13:21:49 +1000 Subject: [PATCH 04/16] it seems mypy ignores the generic class bound if it's a type alias --- qt/aqt/operations/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index cb7b4b5d9..eb9098ef2 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -36,8 +36,6 @@ ResultWithChanges = TypeVar( ], ) -T = TypeVar("T") - CollectionOpSuccessCallback = Callable[[ResultWithChanges], Any] CollectionOpFailureCallback = Optional[Callable[[Exception], Any]] @@ -66,18 +64,20 @@ class CollectionOp(Generic[ResultWithChanges]): passed to `failure` if it is provided. """ - _success: Optional[CollectionOpSuccessCallback] = None + _success: Optional[Callable[[ResultWithChanges], Any]] = None _failure: Optional[CollectionOpFailureCallback] = None def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]): self._parent = parent self._op = op - def success(self, success: Optional[CollectionOpSuccessCallback]) -> CollectionOp: + def success( + self, success: Optional[Callable[[ResultWithChanges], Any]] + ) -> CollectionOp[ResultWithChanges]: self._success = success return self - def failure(self, failure: Optional[CollectionOpFailureCallback]) -> CollectionOp: + def failure(self, failure: Optional[CollectionOpFailureCallback]) -> CollectionOp[ResultWithChanges]: self._failure = failure return self From bc78b6ef1734ff728bd14470e5bb6fd52cab8242 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 14:36:13 +1000 Subject: [PATCH 05/16] migrate more ops to CollectionOp --- qt/aqt/browser.py | 14 ++++--- qt/aqt/deckbrowser.py | 17 ++++++--- qt/aqt/main.py | 13 +------ qt/aqt/operations/__init__.py | 8 ++-- qt/aqt/operations/card.py | 25 ++++++------- qt/aqt/operations/collection.py | 20 +++++----- qt/aqt/operations/deck.py | 65 +++++++++++++++------------------ qt/aqt/operations/tag.py | 20 +++++----- qt/aqt/reviewer.py | 14 +++---- qt/aqt/sidebar.py | 27 ++++++++------ qt/aqt/studydeck.py | 4 +- 11 files changed, 111 insertions(+), 116 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index d4eadfaee..ce4d4925c 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -677,7 +677,7 @@ where id in %s""" return did = self.col.decks.id(ret.name) - set_card_deck(mw=self.mw, card_ids=cids, deck_id=did) + set_card_deck(parent=self, card_ids=cids, deck_id=did).run_in_background() # legacy @@ -696,7 +696,7 @@ where id in %s""" return add_tags_to_notes( parent=self, note_ids=self.selected_notes(), space_separated_tags=tags - ).run(handler=self) + ).run_in_background(initiator=self) @ensure_editor_saved_on_trigger def remove_tags_from_selected_notes(self, tags: Optional[str] = None) -> None: @@ -708,7 +708,7 @@ where id in %s""" remove_tags_from_notes( parent=self, note_ids=self.selected_notes(), space_separated_tags=tags - ).run(handler=self) + ).run_in_background(initiator=self) def _prompt_for_tags(self, prompt: str) -> Optional[str]: (tags, ok) = getTag(self, self.col, prompt) @@ -719,7 +719,7 @@ where id in %s""" @ensure_editor_saved_on_trigger def clear_unused_tags(self) -> None: - clear_unused_tags(parent=self).run() + clear_unused_tags(parent=self).run_in_background() addTags = add_tags_to_selected_notes deleteTags = remove_tags_from_selected_notes @@ -760,7 +760,9 @@ where id in %s""" if flag == self.card.user_flag(): flag = 0 - set_card_flag(mw=self.mw, card_ids=self.selected_cards(), flag=flag) + set_card_flag( + parent=self, card_ids=self.selected_cards(), flag=flag + ).run_in_background() def _update_flags_menu(self) -> None: flag = self.card and self.card.user_flag() @@ -859,7 +861,7 @@ where id in %s""" ###################################################################### def undo(self) -> None: - undo(mw=self.mw, parent=self) + undo(parent=self) def onUndoState(self, on: bool) -> None: self.form.actionUndo.setEnabled(on) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 1d2f58ec5..74455ac4a 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -278,7 +278,9 @@ class DeckBrowser: if not new_name or new_name == deck.name: return else: - rename_deck(mw=self.mw, deck_id=did, new_name=new_name) + rename_deck( + parent=self.mw, deck_id=did, new_name=new_name + ).run_in_background() self.mw.query_op(lambda: self.mw.col.get_deck(did), success=prompt) @@ -293,18 +295,20 @@ class DeckBrowser: if node: node.collapsed = not node.collapsed set_deck_collapsed( - mw=self.mw, + parent=self.mw, deck_id=did, collapsed=node.collapsed, scope=DeckCollapseScope.REVIEWER, - ) + ).run_in_background() self._renderPage(reuse=True) def _handle_drag_and_drop(self, source: DeckId, target: DeckId) -> None: - reparent_decks(mw=self.mw, parent=self.mw, deck_ids=[source], new_parent=target) + reparent_decks( + parent=self.mw, deck_ids=[source], new_parent=target + ).run_in_background() def _delete(self, did: DeckId) -> None: - remove_decks(mw=self.mw, parent=self.mw, deck_ids=[did]) + remove_decks(parent=self.mw, deck_ids=[did]).run_in_background() # Top buttons ###################################################################### @@ -335,7 +339,8 @@ class DeckBrowser: openLink(f"{aqt.appShared}decks/") def _on_create(self) -> None: - add_deck_dialog(mw=self.mw, parent=self.mw) + if op := add_deck_dialog(parent=self.mw): + op.run_in_background() ###################################################################### diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 7dedba7dd..a977940e9 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -21,7 +21,6 @@ from typing import ( List, Literal, Optional, - Protocol, Sequence, TextIO, Tuple, @@ -40,15 +39,7 @@ import aqt.toolbar import aqt.webview from anki import hooks from anki._backend import RustBackend as _RustBackend -from anki.collection import ( - Collection, - Config, - OpChanges, - OpChangesAfterUndo, - OpChangesWithCount, - OpChangesWithId, - UndoStatus, -) +from anki.collection import Collection, Config, OpChanges, UndoStatus from anki.decks import DeckDict, DeckId from anki.hooks import runHook from anki.notes import NoteId @@ -1198,7 +1189,7 @@ title="%s" %s>%s""" % ( def undo(self) -> None: "Call collection_ops.py:undo() directly instead." - undo(mw=self, parent=self) + undo(parent=self) def update_undo_actions(self, status: Optional[UndoStatus] = None) -> None: """Update menu text and enable/disable menu item as appropriate. diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index eb9098ef2..360be305d 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -77,11 +77,13 @@ class CollectionOp(Generic[ResultWithChanges]): self._success = success return self - def failure(self, failure: Optional[CollectionOpFailureCallback]) -> CollectionOp[ResultWithChanges]: + def failure( + self, failure: Optional[CollectionOpFailureCallback] + ) -> CollectionOp[ResultWithChanges]: self._failure = failure return self - def run(self, *, handler: Optional[object] = None) -> None: + def run_in_background(self, *, initiator: Optional[object] = None) -> None: aqt.mw._increase_background_ops() def wrapped_op() -> ResultWithChanges: @@ -110,7 +112,7 @@ class CollectionOp(Generic[ResultWithChanges]): status = aqt.mw.col.undo_status() aqt.mw._update_undo_actions_for_status_and_save(status) # fire change hooks - self._fire_change_hooks_after_op_performed(result, handler) + self._fire_change_hooks_after_op_performed(result, initiator) aqt.mw.taskman.with_progress(wrapped_op, wrapped_done) diff --git a/qt/aqt/operations/card.py b/qt/aqt/operations/card.py index fd994c475..2e7f0ac19 100644 --- a/qt/aqt/operations/card.py +++ b/qt/aqt/operations/card.py @@ -3,28 +3,25 @@ from __future__ import annotations -from typing import Optional, Sequence +from typing import Sequence from anki.cards import CardId +from anki.collection import OpChanges from anki.decks import DeckId -from aqt import AnkiQt -from aqt.main import PerformOpOptionalSuccessCallback +from aqt.operations import CollectionOp +from aqt.qt import QWidget -def set_card_deck(*, mw: AnkiQt, card_ids: Sequence[CardId], deck_id: DeckId) -> None: - mw.perform_op(lambda: mw.col.set_deck(card_ids, deck_id)) +def set_card_deck( + *, parent: QWidget, card_ids: Sequence[CardId], deck_id: DeckId +) -> CollectionOp[OpChanges]: + return CollectionOp(parent, lambda col: col.set_deck(card_ids, deck_id)) def set_card_flag( *, - mw: AnkiQt, + parent: QWidget, card_ids: Sequence[CardId], flag: int, - handler: Optional[object] = None, - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.perform_op( - lambda: mw.col.set_user_flag_for_cards(flag, card_ids), - handler=handler, - success=success, - ) +) -> CollectionOp[OpChanges]: + return CollectionOp(parent, lambda col: col.set_user_flag_for_cards(flag, card_ids)) diff --git a/qt/aqt/operations/collection.py b/qt/aqt/operations/collection.py index 95923c055..7d86f3cb7 100644 --- a/qt/aqt/operations/collection.py +++ b/qt/aqt/operations/collection.py @@ -3,34 +3,34 @@ from __future__ import annotations -import aqt -from anki.collection import LegacyCheckpoint, LegacyReviewUndo, OpChangesAfterUndo +from anki.collection import LegacyCheckpoint, LegacyReviewUndo from anki.errors import UndoEmpty from anki.types import assert_exhaustive from aqt import gui_hooks +from aqt.operations import CollectionOp from aqt.qt import QWidget from aqt.utils import showInfo, showWarning, tooltip, tr -def undo(*, mw: aqt.AnkiQt, parent: QWidget) -> None: +def undo(*, parent: QWidget) -> None: "Undo the last operation, and refresh the UI." - def on_success(out: OpChangesAfterUndo) -> None: - mw.update_undo_actions(out.new_status) - tooltip(tr.undo_action_undone(action=out.operation), parent=parent) - def on_failure(exc: Exception) -> None: if isinstance(exc, UndoEmpty): # backend has no undo, but there may be a checkpoint # or v1/v2 review waiting - _legacy_undo(mw=mw, parent=parent) + _legacy_undo(parent=parent) else: showWarning(str(exc), parent=parent) - mw.perform_op(mw.col.undo, success=on_success, failure=on_failure) + CollectionOp(parent, lambda col: col.undo()).success( + lambda out: tooltip(tr.undo_action_undone(action=out.operation), parent=parent) + ).failure(on_failure).run_in_background() -def _legacy_undo(*, mw: aqt.AnkiQt, parent: QWidget) -> None: +def _legacy_undo(*, parent: QWidget) -> None: + from aqt import mw + reviewing = mw.state == "review" just_refresh_reviewer = False diff --git a/qt/aqt/operations/deck.py b/qt/aqt/operations/deck.py index 4081626cf..d4d98adeb 100644 --- a/qt/aqt/operations/deck.py +++ b/qt/aqt/operations/deck.py @@ -5,81 +5,74 @@ from __future__ import annotations from typing import Optional, Sequence +from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId from anki.decks import DeckCollapseScope, DeckId -from aqt import AnkiQt, QWidget -from aqt.main import PerformOpOptionalSuccessCallback +from aqt import QWidget +from aqt.operations import CollectionOp from aqt.utils import getOnlyText, tooltip, tr def remove_decks( *, - mw: AnkiQt, parent: QWidget, deck_ids: Sequence[DeckId], -) -> None: - mw.perform_op( - lambda: mw.col.decks.remove(deck_ids), - success=lambda out: tooltip( - tr.browsing_cards_deleted(count=out.count), parent=parent - ), +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.decks.remove(deck_ids)).success( + lambda out: tooltip(tr.browsing_cards_deleted(count=out.count), parent=parent) ) def reparent_decks( - *, mw: AnkiQt, parent: QWidget, deck_ids: Sequence[DeckId], new_parent: DeckId -) -> None: - mw.perform_op( - lambda: mw.col.decks.reparent(deck_ids=deck_ids, new_parent=new_parent), - success=lambda out: tooltip( + *, parent: QWidget, deck_ids: Sequence[DeckId], new_parent: DeckId +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp( + parent, lambda col: col.decks.reparent(deck_ids=deck_ids, new_parent=new_parent) + ).success( + lambda out: tooltip( tr.browsing_reparented_decks(count=out.count), parent=parent - ), + ) ) def rename_deck( *, - mw: AnkiQt, + parent: QWidget, deck_id: DeckId, new_name: str, -) -> None: - mw.perform_op( - lambda: mw.col.decks.rename(deck_id, new_name), +) -> CollectionOp[OpChanges]: + return CollectionOp( + parent, + lambda col: col.decks.rename(deck_id, new_name), ) def add_deck_dialog( *, - mw: AnkiQt, parent: QWidget, default_text: str = "", - success: PerformOpOptionalSuccessCallback = None, -) -> None: +) -> Optional[CollectionOp[OpChangesWithId]]: if name := getOnlyText( tr.decks_new_deck_name(), default=default_text, parent=parent ).strip(): - add_deck(mw=mw, name=name, success=success) + return add_deck(parent=parent, name=name) + else: + return None -def add_deck( - *, mw: AnkiQt, name: str, success: PerformOpOptionalSuccessCallback = None -) -> None: - mw.perform_op( - lambda: mw.col.decks.add_normal_deck_with_name(name), - success=success, - ) +def add_deck(*, parent: QWidget, name: str) -> CollectionOp[OpChangesWithId]: + return CollectionOp(parent, lambda col: col.decks.add_normal_deck_with_name(name)) def set_deck_collapsed( *, - mw: AnkiQt, + parent: QWidget, deck_id: DeckId, collapsed: bool, scope: DeckCollapseScope.V, - handler: Optional[object] = None, -) -> None: - mw.perform_op( - lambda: mw.col.decks.set_collapsed( +) -> CollectionOp[OpChanges]: + return CollectionOp( + parent, + lambda col: col.decks.set_collapsed( deck_id=deck_id, collapsed=collapsed, scope=scope ), - handler=handler, ) diff --git a/qt/aqt/operations/tag.py b/qt/aqt/operations/tag.py index 6c2ea7611..cd33e2f17 100644 --- a/qt/aqt/operations/tag.py +++ b/qt/aqt/operations/tag.py @@ -5,9 +5,9 @@ from __future__ import annotations from typing import Sequence -from anki.collection import OpChangesWithCount +from anki.collection import OpChanges, OpChangesWithCount from anki.notes import NoteId -from aqt import AnkiQt, QWidget +from aqt import QWidget from aqt.operations import CollectionOp from aqt.utils import showInfo, tooltip, tr @@ -17,7 +17,7 @@ def add_tags_to_notes( parent: QWidget, note_ids: Sequence[NoteId], space_separated_tags: str, -) -> CollectionOp: +) -> CollectionOp[OpChangesWithCount]: return CollectionOp( parent, lambda col: col.tags.bulk_add(note_ids, space_separated_tags) ).success( @@ -30,7 +30,7 @@ def remove_tags_from_notes( parent: QWidget, note_ids: Sequence[NoteId], space_separated_tags: str, -) -> CollectionOp: +) -> CollectionOp[OpChangesWithCount]: return CollectionOp( parent, lambda col: col.tags.bulk_remove(note_ids, space_separated_tags) ).success( @@ -38,7 +38,7 @@ def remove_tags_from_notes( ) -def clear_unused_tags(*, parent: QWidget) -> CollectionOp: +def clear_unused_tags(*, parent: QWidget) -> CollectionOp[OpChangesWithCount]: return CollectionOp(parent, lambda col: col.tags.clear_unused_tags()).success( lambda out: tooltip( tr.browsing_removed_unused_tags_count(count=out.count), parent=parent @@ -51,7 +51,7 @@ def rename_tag( parent: QWidget, current_name: str, new_name: str, -) -> CollectionOp: +) -> CollectionOp[OpChangesWithCount]: def success(out: OpChangesWithCount) -> None: if out.count: tooltip(tr.browsing_notes_updated(count=out.count), parent=parent) @@ -66,7 +66,7 @@ def rename_tag( def remove_tags_from_all_notes( *, parent: QWidget, space_separated_tags: str -) -> CollectionOp: +) -> CollectionOp[OpChangesWithCount]: return CollectionOp( parent, lambda col: col.tags.remove(space_separated_tags=space_separated_tags) ).success( @@ -76,7 +76,7 @@ def remove_tags_from_all_notes( def reparent_tags( *, parent: QWidget, tags: Sequence[str], new_parent: str -) -> CollectionOp: +) -> CollectionOp[OpChangesWithCount]: return CollectionOp( parent, lambda col: col.tags.reparent(tags=tags, new_parent=new_parent) ).success( @@ -84,7 +84,9 @@ def reparent_tags( ) -def set_tag_collapsed(*, parent: QWidget, tag: str, collapsed: bool) -> CollectionOp: +def set_tag_collapsed( + *, parent: QWidget, tag: str, collapsed: bool +) -> CollectionOp[OpChanges]: return CollectionOp( parent, lambda col: col.tags.set_collapsed(tag=tag, collapsed=collapsed) ) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index cebbf24a3..0e3bf90c8 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -835,13 +835,9 @@ time = %(time)d; else: flag = desired_flag - set_card_flag( - mw=self.mw, - card_ids=[self.card.id], - flag=flag, - handler=self, - success=redraw_flag, - ) + set_card_flag(parent=self.mw, card_ids=[self.card.id], flag=flag).success( + redraw_flag + ).run_in_background(initiator=self) def toggle_mark_on_current_note(self) -> None: def redraw_mark(out: OpChangesWithCount) -> None: @@ -852,13 +848,13 @@ time = %(time)d; if note.has_tag(MARKED_TAG): remove_tags_from_notes( parent=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG - ).success(redraw_mark).run(handler=self) + ).success(redraw_mark).run_in_background(initiator=self) else: add_tags_to_notes( parent=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG, - ).success(redraw_mark).run(handler=self) + ).success(redraw_mark).run_in_background(initiator=self) def on_set_due(self) -> None: if self.mw.state != "review" or not self.card: diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 77cf27a75..2a9c78bea 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -631,8 +631,8 @@ class SidebarTreeView(QTreeView): new_parent = DeckId(target.id) reparent_decks( - mw=self.mw, parent=self.browser, deck_ids=deck_ids, new_parent=new_parent - ) + parent=self.browser, deck_ids=deck_ids, new_parent=new_parent + ).run_in_background() return True @@ -652,7 +652,9 @@ class SidebarTreeView(QTreeView): else: new_parent = target.full_name - reparent_tags(parent=self.browser, tags=tags, new_parent=new_parent).run() + reparent_tags( + parent=self.browser, tags=tags, new_parent=new_parent + ).run_in_background() return True @@ -948,7 +950,7 @@ class SidebarTreeView(QTreeView): full_name = head + node.name return lambda expanded: set_tag_collapsed( parent=self, tag=full_name, collapsed=not expanded - ).run() + ).run_in_background() for node in nodes: item = SidebarItem( @@ -993,11 +995,12 @@ class SidebarTreeView(QTreeView): ) -> None: def toggle_expand(node: DeckTreeNode) -> Callable[[bool], None]: return lambda expanded: set_deck_collapsed( - mw=self.mw, + parent=self, deck_id=DeckId(node.deck_id), collapsed=not expanded, scope=DeckCollapseScope.BROWSER, - handler=self, + ).run_in_background( + initiator=self, ) for node in nodes: @@ -1192,15 +1195,15 @@ class SidebarTreeView(QTreeView): return rename_deck( - mw=self.mw, + parent=self, deck_id=deck_id, new_name=full_name, - ) + ).run_in_background() self.mw.query_op(lambda: self.mw.col.get_deck(deck_id), success=after_fetch) def delete_decks(self, _item: SidebarItem) -> None: - remove_decks(mw=self.mw, parent=self.browser, deck_ids=self._selected_decks()) + remove_decks(parent=self, deck_ids=self._selected_decks()).run_in_background() # Tags ########################### @@ -1209,7 +1212,9 @@ class SidebarTreeView(QTreeView): tags = self.mw.col.tags.join(self._selected_tags()) item.name = "..." - remove_tags_from_all_notes(parent=self.browser, space_separated_tags=tags).run() + remove_tags_from_all_notes( + parent=self.browser, space_separated_tags=tags + ).run_in_background() def rename_tag(self, item: SidebarItem, new_name: str) -> None: if not new_name or new_name == item.name: @@ -1227,7 +1232,7 @@ class SidebarTreeView(QTreeView): parent=self.browser, current_name=old_name, new_name=new_name, - ).run() + ).run_in_background() # Saved searches #################################### diff --git a/qt/aqt/studydeck.py b/qt/aqt/studydeck.py index e028e6718..092011791 100644 --- a/qt/aqt/studydeck.py +++ b/qt/aqt/studydeck.py @@ -175,4 +175,6 @@ class StudyDeck(QDialog): QDialog.accept(self) - add_deck_dialog(mw=self.mw, parent=self, default_text=default, success=success) + add_deck_dialog(parent=self, default_text=default).success( + success + ).run_in_background() From 2de8cc1a948217fb3af0c36927e82eea21cfecef Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 14:56:36 +1000 Subject: [PATCH 06/16] update note ops remove_note() now returns the count of removed cards, allowing us to unify the tooltip between browser and review screen I've left the old translation in - we'll need to write a script at one point that gathers all references to translations in the code, and shows ones that are unused. --- pylib/anki/collection.py | 2 +- qt/aqt/addcards.py | 6 +++--- qt/aqt/browser.py | 6 +----- qt/aqt/editor.py | 4 +++- qt/aqt/operations/note.py | 31 +++++++++++++++---------------- qt/aqt/reviewer.py | 9 +-------- rslib/backend.proto | 2 +- rslib/src/backend/notes.rs | 2 +- rslib/src/notes/mod.rs | 6 ++++-- 9 files changed, 30 insertions(+), 38 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 1fe89a700..14dbbafb2 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -385,7 +385,7 @@ class Collection: note.id = NoteId(out.note_id) return out.changes - def remove_notes(self, note_ids: Sequence[NoteId]) -> OpChanges: + def remove_notes(self, note_ids: Sequence[NoteId]) -> OpChangesWithCount: hooks.notes_will_be_deleted(self, note_ids) return self._backend.remove_notes(note_ids=note_ids, card_ids=[]) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index bd60b8365..04e1f16ab 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -208,9 +208,9 @@ class AddCards(QDialog): self._load_new_note(sticky_fields_from=note) gui_hooks.add_cards_did_add_note(note) - add_note( - mw=self.mw, note=note, target_deck_id=target_deck_id, success=on_success - ) + add_note(parent=self, note=note, target_deck_id=target_deck_id).success( + on_success + ).run_in_background() def _note_can_be_added(self, note: Note) -> bool: result = note.duplicate_or_empty() diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index ce4d4925c..0458fe294 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -642,11 +642,7 @@ where id in %s""" self.focusTo = self.editor.currentField self.table.to_next_row() - remove_notes( - mw=self.mw, - note_ids=nids, - success=lambda _: tooltip(tr.browsing_note_deleted(count=len(nids))), - ) + remove_notes(parent=self, note_ids=nids).run_in_background() # legacy diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 999a8dfa6..08539a8e3 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -558,7 +558,9 @@ class Editor: def _save_current_note(self) -> None: "Call after note is updated with data from webview." - update_note(mw=self.mw, note=self.note, handler=self) + update_note(parent=self.widget, note=self.note).run_in_background( + initiator=self + ) def fonts(self) -> List[Tuple[str, int, bool]]: return [ diff --git a/qt/aqt/operations/note.py b/qt/aqt/operations/note.py index dfe28ba92..dae7f8287 100644 --- a/qt/aqt/operations/note.py +++ b/qt/aqt/operations/note.py @@ -3,35 +3,34 @@ from __future__ import annotations -from typing import Optional, Sequence +from typing import Sequence +from anki.collection import OpChanges, OpChangesWithCount from anki.decks import DeckId from anki.notes import Note, NoteId -from aqt import AnkiQt -from aqt.main import PerformOpOptionalSuccessCallback +from aqt.operations import CollectionOp +from aqt.qt import QWidget +from aqt.utils import tooltip, tr def add_note( *, - mw: AnkiQt, + parent: QWidget, note: Note, target_deck_id: DeckId, - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.perform_op(lambda: mw.col.add_note(note, target_deck_id), success=success) +) -> CollectionOp[OpChanges]: + return CollectionOp(parent, lambda col: col.add_note(note, target_deck_id)) -def update_note(*, mw: AnkiQt, note: Note, handler: Optional[object]) -> None: - mw.perform_op( - lambda: mw.col.update_note(note), - handler=handler, - ) +def update_note(*, parent: QWidget, note: Note) -> CollectionOp[OpChanges]: + return CollectionOp(parent, lambda col: col.update_note(note)) def remove_notes( *, - mw: AnkiQt, + parent: QWidget, note_ids: Sequence[NoteId], - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.perform_op(lambda: mw.col.remove_notes(note_ids), success=success) +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.remove_notes(note_ids)).success( + lambda out: tooltip(tr.browsing_cards_deleted(count=out.count)), + ) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 0e3bf90c8..553063d80 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -901,14 +901,7 @@ time = %(time)d; if self.mw.state != "review" or not self.card: return - # fixme: pass this back from the backend method instead - cnt = len(self.card.note().cards()) - - remove_notes( - mw=self.mw, - note_ids=[self.card.nid], - success=lambda _: tooltip(tr.studying_note_and_its_card_deleted(count=cnt)), - ) + remove_notes(parent=self.mw, note_ids=[self.card.nid]).run_in_background() def onRecordVoice(self) -> None: def after_record(path: str) -> None: diff --git a/rslib/backend.proto b/rslib/backend.proto index e5430c48d..6bcfd36c3 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -163,7 +163,7 @@ service NotesService { rpc DefaultDeckForNotetype(NotetypeId) returns (DeckId); rpc UpdateNote(UpdateNoteIn) returns (OpChanges); rpc GetNote(NoteId) returns (Note); - rpc RemoveNotes(RemoveNotesIn) returns (OpChanges); + rpc RemoveNotes(RemoveNotesIn) returns (OpChangesWithCount); rpc ClozeNumbersInNote(Note) returns (ClozeNumbersInNoteOut); rpc AfterNoteUpdates(AfterNoteUpdatesIn) returns (OpChanges); rpc FieldNamesForNotes(FieldNamesForNotesIn) returns (FieldNamesForNotesOut); diff --git a/rslib/src/backend/notes.rs b/rslib/src/backend/notes.rs index 75c6edaca..ceeb53976 100644 --- a/rslib/src/backend/notes.rs +++ b/rslib/src/backend/notes.rs @@ -63,7 +63,7 @@ impl NotesService for Backend { }) } - fn remove_notes(&self, input: pb::RemoveNotesIn) -> Result { + fn remove_notes(&self, input: pb::RemoveNotesIn) -> Result { self.with_col(|col| { if !input.note_ids.is_empty() { col.remove_notes( diff --git a/rslib/src/notes/mod.rs b/rslib/src/notes/mod.rs index 861455c93..14074d088 100644 --- a/rslib/src/notes/mod.rs +++ b/rslib/src/notes/mod.rs @@ -402,19 +402,21 @@ impl Collection { } /// Remove provided notes, and any cards that use them. - pub(crate) fn remove_notes(&mut self, nids: &[NoteId]) -> Result> { + pub(crate) fn remove_notes(&mut self, nids: &[NoteId]) -> Result> { let usn = self.usn()?; self.transact(Op::RemoveNote, |col| { + let mut card_count = 0; for nid in nids { let nid = *nid; if let Some(_existing_note) = col.storage.get_note(nid)? { for card in col.storage.all_cards_of_note(nid)? { + card_count += 1; col.remove_card_and_add_grave_undoable(card, usn)?; } col.remove_note_only_undoable(nid, usn)?; } } - Ok(()) + Ok(card_count) }) } From 84fe309583dbe09643d368cef3036b1a33821f50 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 16:38:42 +1000 Subject: [PATCH 07/16] update scheduling ops - migrate to CollectionOp() - return actual change count when suspending/burying - add helper to convert vec to vec of newtype --- pylib/anki/scheduler/base.py | 25 +++-- pylib/tests/test_schedv1.py | 2 +- qt/aqt/browser.py | 14 +-- qt/aqt/filtered_deck.py | 4 +- qt/aqt/operations/scheduling.py | 143 ++++++++++++------------ qt/aqt/overview.py | 10 +- qt/aqt/reviewer.py | 31 +++-- rslib/backend.proto | 5 +- rslib/src/backend/notes.rs | 2 +- rslib/src/backend/scheduler/mod.rs | 18 ++- rslib/src/prelude.rs | 1 + rslib/src/scheduler/answering/undo.rs | 2 +- rslib/src/scheduler/bury_and_suspend.rs | 12 +- rslib/src/storage/card/mod.rs | 15 ++- rslib/src/types.rs | 15 +++ 15 files changed, 172 insertions(+), 127 deletions(-) diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py index 280d9a7ec..268211e06 100644 --- a/pylib/anki/scheduler/base.py +++ b/pylib/anki/scheduler/base.py @@ -16,7 +16,7 @@ from typing import List, Optional, Sequence from anki.cards import CardId from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW, QUEUE_TYPE_REV from anki.decks import DeckConfigDict, DeckId, DeckTreeNode -from anki.notes import Note +from anki.notes import NoteId from anki.utils import ids2str, intTime CongratsInfo = _pb.CongratsInfoOut @@ -123,20 +123,31 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l ) -> None: self.col._backend.unbury_cards_in_current_deck(mode) - def suspend_cards(self, ids: Sequence[CardId]) -> OpChanges: + def suspend_cards(self, ids: Sequence[CardId]) -> OpChangesWithCount: return self.col._backend.bury_or_suspend_cards( - card_ids=ids, mode=BuryOrSuspend.SUSPEND + card_ids=ids, note_ids=[], mode=BuryOrSuspend.SUSPEND ) - def bury_cards(self, ids: Sequence[CardId], manual: bool = True) -> OpChanges: + def suspend_notes(self, ids: Sequence[NoteId]) -> OpChangesWithCount: + return self.col._backend.bury_or_suspend_cards( + card_ids=[], note_ids=ids, mode=BuryOrSuspend.SUSPEND + ) + + def bury_cards( + self, ids: Sequence[CardId], manual: bool = True + ) -> OpChangesWithCount: if manual: mode = BuryOrSuspend.BURY_USER else: mode = BuryOrSuspend.BURY_SCHED - return self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode) + return self.col._backend.bury_or_suspend_cards( + card_ids=ids, note_ids=[], mode=mode + ) - def bury_note(self, note: Note) -> None: - self.bury_cards(note.card_ids()) + def bury_notes(self, note_ids: Sequence[NoteId]) -> OpChangesWithCount: + return self.col._backend.bury_or_suspend_cards( + card_ids=[], note_ids=note_ids, mode=BuryOrSuspend.BURY_USER + ) # Resetting/rescheduling ########################################################################## diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index ac21d7fa7..c1a4d3c89 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -501,7 +501,7 @@ def test_misc(): col.addNote(note) c = note.cards()[0] # burying - col.sched.bury_note(note) + col.sched.bury_notes([note.id]) col.reset() assert not col.sched.getCard() col.sched.unbury_cards_in_current_deck() diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 0458fe294..88c0a09a8 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -732,9 +732,9 @@ where id in %s""" def suspend_selected_cards(self, checked: bool) -> None: cids = self.selected_cards() if checked: - suspend_cards(mw=self.mw, card_ids=cids) + suspend_cards(parent=self, card_ids=cids).run_in_background() else: - unsuspend_cards(mw=self.mw, card_ids=cids) + unsuspend_cards(parent=self.mw, card_ids=cids).run_in_background() # Exporting ###################################################################### @@ -796,25 +796,23 @@ where id in %s""" return reposition_new_cards_dialog( - mw=self.mw, parent=self, card_ids=self.selected_cards() - ) + parent=self, card_ids=self.selected_cards() + ).run_in_background() @ensure_editor_saved_on_trigger def set_due_date(self) -> None: set_due_date_dialog( - mw=self.mw, parent=self, card_ids=self.selected_cards(), config_key=Config.String.SET_DUE_BROWSER, - ) + ).run_in_background() @ensure_editor_saved_on_trigger def forget_cards(self) -> None: forget_cards( - mw=self.mw, parent=self, card_ids=self.selected_cards(), - ) + ).run_in_background() # Edit: selection ###################################################################### diff --git a/qt/aqt/filtered_deck.py b/qt/aqt/filtered_deck.py index fcce0af09..25af48f80 100644 --- a/qt/aqt/filtered_deck.py +++ b/qt/aqt/filtered_deck.py @@ -310,7 +310,9 @@ class FilteredDeckConfigDialog(QDialog): gui_hooks.filtered_deck_dialog_will_add_or_update_deck(self, self.deck) - add_or_update_filtered_deck(mw=self.mw, deck=self.deck, success=success) + add_or_update_filtered_deck(parent=self, deck=self.deck).success( + success + ).run_in_background() # Step load/save ######################################################## diff --git a/qt/aqt/operations/scheduling.py b/qt/aqt/operations/scheduling.py index acb3dcdff..a9a71c948 100644 --- a/qt/aqt/operations/scheduling.py +++ b/qt/aqt/operations/scheduling.py @@ -7,28 +7,32 @@ from typing import Optional, Sequence import aqt from anki.cards import CardId -from anki.collection import CARD_TYPE_NEW, Config +from anki.collection import ( + CARD_TYPE_NEW, + Config, + OpChanges, + OpChangesWithCount, + OpChangesWithId, +) from anki.decks import DeckId from anki.notes import NoteId from anki.scheduler import FilteredDeckForUpdate -from aqt import AnkiQt -from aqt.main import PerformOpOptionalSuccessCallback +from aqt.operations import CollectionOp from aqt.qt import * from aqt.utils import disable_help_button, getText, tooltip, tr def set_due_date_dialog( *, - mw: aqt.AnkiQt, parent: QWidget, card_ids: Sequence[CardId], config_key: Optional[Config.String.Key.V], -) -> None: +) -> Optional[CollectionOp[OpChanges]]: if not card_ids: - return + return None default_text = ( - mw.col.get_config_string(config_key) if config_key is not None else "" + aqt.mw.col.get_config_string(config_key) if config_key is not None else "" ) prompt = "\n".join( [ @@ -43,34 +47,35 @@ def set_due_date_dialog( title=tr.actions_set_due_date(), ) if not success or not days.strip(): - return - - mw.perform_op( - lambda: mw.col.sched.set_due_date(card_ids, days, config_key), - success=lambda _: tooltip( - tr.scheduling_set_due_date_done(cards=len(card_ids)), - parent=parent, - ), - ) + return None + else: + return CollectionOp( + parent, lambda col: col.sched.set_due_date(card_ids, days, config_key) + ).success( + lambda _: tooltip( + tr.scheduling_set_due_date_done(cards=len(card_ids)), + parent=parent, + ) + ) def forget_cards( - *, mw: aqt.AnkiQt, parent: QWidget, card_ids: Sequence[CardId] -) -> None: - if not card_ids: - return - - mw.perform_op( - lambda: mw.col.sched.schedule_cards_as_new(card_ids), - success=lambda _: tooltip( + *, parent: QWidget, card_ids: Sequence[CardId] +) -> CollectionOp[OpChanges]: + return CollectionOp( + parent, lambda col: col.sched.schedule_cards_as_new(card_ids) + ).success( + lambda _: tooltip( tr.scheduling_forgot_cards(cards=len(card_ids)), parent=parent - ), + ) ) def reposition_new_cards_dialog( - *, mw: AnkiQt, parent: QWidget, card_ids: Sequence[CardId] -) -> None: + *, parent: QWidget, card_ids: Sequence[CardId] +) -> Optional[CollectionOp[OpChangesWithCount]]: + from aqt import mw + assert mw.col.db row = mw.col.db.first( f"select min(due), max(due) from cards where type={CARD_TYPE_NEW} and odid=0" @@ -92,15 +97,14 @@ def reposition_new_cards_dialog( frm.start.selectAll() if not d.exec_(): - return + return None start = frm.start.value() step = frm.step.value() randomize = frm.randomize.isChecked() shift = frm.shift.isChecked() - reposition_new_cards( - mw=mw, + return reposition_new_cards( parent=parent, card_ids=card_ids, starting_from=start, @@ -112,89 +116,80 @@ def reposition_new_cards_dialog( def reposition_new_cards( *, - mw: AnkiQt, parent: QWidget, card_ids: Sequence[CardId], starting_from: int, step_size: int, randomize: bool, shift_existing: bool, -) -> None: - mw.perform_op( - lambda: mw.col.sched.reposition_new_cards( +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp( + parent, + lambda col: col.sched.reposition_new_cards( card_ids=card_ids, starting_from=starting_from, step_size=step_size, randomize=randomize, shift_existing=shift_existing, ), - success=lambda out: tooltip( + ).success( + lambda out: tooltip( tr.browsing_changed_new_position(count=out.count), parent=parent - ), + ) ) def suspend_cards( *, - mw: AnkiQt, + parent: QWidget, card_ids: Sequence[CardId], - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.perform_op(lambda: mw.col.sched.suspend_cards(card_ids), success=success) +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.sched.suspend_cards(card_ids)) def suspend_note( *, - mw: AnkiQt, - note_id: NoteId, - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.taskman.run_in_background( - lambda: mw.col.card_ids_of_note(note_id), - lambda future: suspend_cards(mw=mw, card_ids=future.result(), success=success), - ) + parent: QWidget, + note_ids: Sequence[NoteId], +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.sched.suspend_notes(note_ids)) -def unsuspend_cards(*, mw: AnkiQt, card_ids: Sequence[CardId]) -> None: - mw.perform_op(lambda: mw.col.sched.unsuspend_cards(card_ids)) +def unsuspend_cards( + *, parent: QWidget, card_ids: Sequence[CardId] +) -> CollectionOp[OpChanges]: + return CollectionOp(parent, lambda col: col.sched.unsuspend_cards(card_ids)) def bury_cards( *, - mw: AnkiQt, + parent: QWidget, card_ids: Sequence[CardId], - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.perform_op(lambda: mw.col.sched.bury_cards(card_ids), success=success) +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.sched.bury_cards(card_ids)) -def bury_note( +def bury_notes( *, - mw: AnkiQt, - note_id: NoteId, - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.taskman.run_in_background( - lambda: mw.col.card_ids_of_note(note_id), - lambda future: bury_cards(mw=mw, card_ids=future.result(), success=success), - ) + parent: QWidget, + note_ids: Sequence[NoteId], +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.sched.bury_notes(note_ids)) -def rebuild_filtered_deck(*, mw: AnkiQt, deck_id: DeckId) -> None: - mw.perform_op(lambda: mw.col.sched.rebuild_filtered_deck(deck_id)) +def rebuild_filtered_deck( + *, parent: QWidget, deck_id: DeckId +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.sched.rebuild_filtered_deck(deck_id)) -def empty_filtered_deck(*, mw: AnkiQt, deck_id: DeckId) -> None: - mw.perform_op(lambda: mw.col.sched.empty_filtered_deck(deck_id)) +def empty_filtered_deck(*, parent: QWidget, deck_id: DeckId) -> CollectionOp[OpChanges]: + return CollectionOp(parent, lambda col: col.sched.empty_filtered_deck(deck_id)) def add_or_update_filtered_deck( *, - mw: AnkiQt, + parent: QWidget, deck: FilteredDeckForUpdate, - success: PerformOpOptionalSuccessCallback, -) -> None: - mw.perform_op( - lambda: mw.col.sched.add_or_update_filtered_deck(deck), - success=success, - ) +) -> CollectionOp[OpChangesWithId]: + return CollectionOp(parent, lambda col: col.sched.add_or_update_filtered_deck(deck)) diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 2e7e01ea7..bd98aac3e 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -119,12 +119,14 @@ class Overview: return self.mw.col.decks.current()["dyn"] def rebuild_current_filtered_deck(self) -> None: - if self._current_deck_is_filtered(): - rebuild_filtered_deck(mw=self.mw, deck_id=self.mw.col.decks.selected()) + rebuild_filtered_deck( + parent=self.mw, deck_id=self.mw.col.decks.selected() + ).run_in_background() def empty_current_filtered_deck(self) -> None: - if self._current_deck_is_filtered(): - empty_filtered_deck(mw=self.mw, deck_id=self.mw.col.decks.selected()) + empty_filtered_deck( + parent=self.mw, deck_id=self.mw.col.decks.selected() + ).run_in_background() def onCustomStudyKey(self) -> None: if not self._current_deck_is_filtered(): diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 553063d80..0394bc8fd 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -22,7 +22,7 @@ from aqt.operations.card import set_card_flag from aqt.operations.note import remove_notes from aqt.operations.scheduling import ( bury_cards, - bury_note, + bury_notes, set_due_date_dialog, suspend_cards, suspend_note, @@ -861,39 +861,34 @@ time = %(time)d; return set_due_date_dialog( - mw=self.mw, parent=self.mw, card_ids=[self.card.id], config_key=Config.String.SET_DUE_REVIEWER, - ) + ).run_in_background() def suspend_current_note(self) -> None: suspend_note( - mw=self.mw, - note_id=self.card.nid, - success=lambda _: tooltip(tr.studying_note_suspended()), - ) + parent=self.mw, + note_ids=[self.card.nid], + ).success(lambda _: tooltip(tr.studying_note_suspended())).run_in_background() def suspend_current_card(self) -> None: suspend_cards( - mw=self.mw, + parent=self.mw, card_ids=[self.card.id], - success=lambda _: tooltip(tr.studying_card_suspended()), - ) + ).success(lambda _: tooltip(tr.studying_card_suspended())).run_in_background() def bury_current_note(self) -> None: - bury_note( - mw=self.mw, - note_id=self.card.nid, - success=lambda _: tooltip(tr.studying_note_buried()), - ) + bury_notes( + parent=self.mw, + note_ids=[self.card.nid], + ).success(lambda _: tooltip(tr.studying_note_buried())).run_in_background() def bury_current_card(self) -> None: bury_cards( - mw=self.mw, + parent=self.mw, card_ids=[self.card.id], - success=lambda _: tooltip(tr.studying_card_buried()), - ) + ).success(lambda _: tooltip(tr.studying_card_buried())).run_in_background() def delete_current_note(self) -> None: # need to check state because the shortcut is global to the main diff --git a/rslib/backend.proto b/rslib/backend.proto index 6bcfd36c3..7a0b92de5 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -120,7 +120,7 @@ service SchedulingService { rpc CongratsInfo(Empty) returns (CongratsInfoOut); rpc RestoreBuriedAndSuspendedCards(CardIds) returns (OpChanges); rpc UnburyCardsInCurrentDeck(UnburyCardsInCurrentDeckIn) returns (Empty); - rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (OpChanges); + rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (OpChangesWithCount); rpc EmptyFilteredDeck(DeckId) returns (OpChanges); rpc RebuildFilteredDeck(DeckId) returns (OpChangesWithCount); rpc ScheduleCardsAsNew(ScheduleCardsAsNewIn) returns (OpChanges); @@ -1314,7 +1314,8 @@ message BuryOrSuspendCardsIn { BURY_USER = 2; } repeated int64 card_ids = 1; - Mode mode = 2; + repeated int64 note_ids = 2; + Mode mode = 3; } message ScheduleCardsAsNewIn { diff --git a/rslib/src/backend/notes.rs b/rslib/src/backend/notes.rs index ceeb53976..864c0e5e0 100644 --- a/rslib/src/backend/notes.rs +++ b/rslib/src/backend/notes.rs @@ -131,7 +131,7 @@ impl NotesService for Backend { fn cards_of_note(&self, input: pb::NoteId) -> Result { self.with_col(|col| { col.storage - .all_card_ids_of_note(NoteId(input.nid)) + .all_card_ids_of_note_in_order(NoteId(input.nid)) .map(|v| pb::CardIds { cids: v.into_iter().map(Into::into).collect(), }) diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs index 3f612bc91..49084ad93 100644 --- a/rslib/src/backend/scheduler/mod.rs +++ b/rslib/src/backend/scheduler/mod.rs @@ -87,10 +87,18 @@ impl SchedulingService for Backend { }) } - fn bury_or_suspend_cards(&self, input: pb::BuryOrSuspendCardsIn) -> Result { + fn bury_or_suspend_cards( + &self, + input: pb::BuryOrSuspendCardsIn, + ) -> Result { self.with_col(|col| { let mode = input.mode(); - let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect(); + let cids = if input.card_ids.is_empty() { + col.storage + .card_ids_of_notes(&input.note_ids.into_newtype(NoteId))? + } else { + input.card_ids.into_newtype(CardId) + }; col.bury_or_suspend_cards(&cids, mode).map(Into::into) }) } @@ -105,7 +113,7 @@ impl SchedulingService for Backend { fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewIn) -> Result { self.with_col(|col| { - let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect(); + let cids = input.card_ids.into_newtype(CardId); let log = input.log; col.reschedule_cards_as_new(&cids, log).map(Into::into) }) @@ -114,12 +122,12 @@ impl SchedulingService for Backend { fn set_due_date(&self, input: pb::SetDueDateIn) -> Result { let config = input.config_key.map(Into::into); let days = input.days; - let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect(); + let cids = input.card_ids.into_newtype(CardId); self.with_col(|col| col.set_due_date(&cids, &days, config).map(Into::into)) } fn sort_cards(&self, input: pb::SortCardsIn) -> Result { - let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect(); + let cids = input.card_ids.into_newtype(CardId); let (start, step, random, shift) = ( input.starting_from, input.step_size, diff --git a/rslib/src/prelude.rs b/rslib/src/prelude.rs index 680066210..544c5dac3 100644 --- a/rslib/src/prelude.rs +++ b/rslib/src/prelude.rs @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +pub(crate) use crate::types::IntoNewtypeVec; pub use crate::{ card::{Card, CardId}, collection::Collection, diff --git a/rslib/src/scheduler/answering/undo.rs b/rslib/src/scheduler/answering/undo.rs index 52f1885db..8d86d9bc2 100644 --- a/rslib/src/scheduler/answering/undo.rs +++ b/rslib/src/scheduler/answering/undo.rs @@ -33,7 +33,7 @@ mod test { let queued = col.next_card()?.unwrap(); let nid = note.id; let cid = queued.card.id; - let sibling_cid = col.storage.all_card_ids_of_note(nid)?[1]; + let sibling_cid = col.storage.all_card_ids_of_note_in_order(nid)?[1]; let assert_initial_state = |col: &mut Collection| -> Result<()> { let first = col.storage.get_card(cid)?.unwrap(); diff --git a/rslib/src/scheduler/bury_and_suspend.rs b/rslib/src/scheduler/bury_and_suspend.rs index 640e98c42..31a3771da 100644 --- a/rslib/src/scheduler/bury_and_suspend.rs +++ b/rslib/src/scheduler/bury_and_suspend.rs @@ -89,7 +89,8 @@ impl Collection { /// Bury/suspend cards in search table, and clear it. /// Marks the cards as modified. - fn bury_or_suspend_searched_cards(&mut self, mode: BuryOrSuspendMode) -> Result<()> { + fn bury_or_suspend_searched_cards(&mut self, mode: BuryOrSuspendMode) -> Result { + let mut count = 0; let usn = self.usn()?; let sched = self.scheduler_version(); @@ -113,18 +114,21 @@ impl Collection { card.remove_from_learning(); } card.queue = desired_queue; + count += 1; self.update_card_inner(&mut card, original, usn)?; } } - self.storage.clear_searched_cards_table() + self.storage.clear_searched_cards_table()?; + + Ok(count) } pub fn bury_or_suspend_cards( &mut self, cids: &[CardId], mode: BuryOrSuspendMode, - ) -> Result> { + ) -> Result> { let op = match mode { BuryOrSuspendMode::Suspend => Op::Suspend, BuryOrSuspendMode::BurySched | BuryOrSuspendMode::BuryUser => Op::Bury, @@ -141,7 +145,7 @@ impl Collection { nid: NoteId, include_new: bool, include_reviews: bool, - ) -> Result<()> { + ) -> Result { self.storage .search_siblings_for_bury(cid, nid, include_new, include_reviews)?; self.bury_or_suspend_searched_cards(BuryOrSuspendMode::BurySched) diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 7bd54221f..a700f7c5b 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -308,13 +308,26 @@ impl super::SqliteStorage { .collect() } - pub(crate) fn all_card_ids_of_note(&self, nid: NoteId) -> Result> { + pub(crate) fn all_card_ids_of_note_in_order(&self, nid: NoteId) -> Result> { self.db .prepare_cached("select id from cards where nid = ? order by ord")? .query_and_then(&[nid], |r| Ok(CardId(r.get(0)?)))? .collect() } + pub(crate) fn card_ids_of_notes(&self, nids: &[NoteId]) -> Result> { + let mut stmt = self + .db + .prepare_cached("select id from cards where nid = ?")?; + let mut cids = vec![]; + for nid in nids { + for cid in stmt.query_map(&[nid], |row| row.get(0))? { + cids.push(cid?); + } + } + Ok(cids) + } + /// Place matching card ids into the search table. pub(crate) fn search_siblings_for_bury( &self, diff --git a/rslib/src/types.rs b/rslib/src/types.rs index 1a14b226f..5f1a00619 100644 --- a/rslib/src/types.rs +++ b/rslib/src/types.rs @@ -68,3 +68,18 @@ macro_rules! define_newtype { } define_newtype!(Usn, i32); + +pub(crate) trait IntoNewtypeVec { + fn into_newtype(self, func: F) -> Vec + where + F: FnMut(i64) -> T; +} + +impl IntoNewtypeVec for Vec { + fn into_newtype(self, func: F) -> Vec + where + F: FnMut(i64) -> T, + { + self.into_iter().map(func).collect() + } +} From 5676ad510146aabfb37c6302cba180f6248217cf Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 17:07:38 +1000 Subject: [PATCH 08/16] update find&replace, and remove perform_op() --- qt/aqt/find_and_replace.py | 89 +++++++----------------------- qt/aqt/main.py | 95 ++------------------------------- qt/aqt/operations/__init__.py | 31 ++++++----- qt/aqt/operations/collection.py | 3 ++ qt/aqt/operations/note.py | 30 ++++++++++- qt/aqt/operations/scheduling.py | 3 ++ qt/aqt/operations/tag.py | 26 +++++++++ qt/aqt/taskman.py | 2 +- qt/mypy.ini | 12 +---- qt/tools/genhooks_gui.py | 6 +-- rslib/i18n/build/extract.rs | 2 +- 11 files changed, 108 insertions(+), 191 deletions(-) diff --git a/qt/aqt/find_and_replace.py b/qt/aqt/find_and_replace.py index d0831e2b1..4f6b14e3f 100644 --- a/qt/aqt/find_and_replace.py +++ b/qt/aqt/find_and_replace.py @@ -3,11 +3,13 @@ from __future__ import annotations -from typing import List, Optional, Sequence +from typing import List, Sequence import aqt from anki.notes import NoteId from aqt import AnkiQt, QWidget +from aqt.operations.note import find_and_replace +from aqt.operations.tag import find_and_replace_tag from aqt.qt import QDialog, Qt from aqt.utils import ( HelpPage, @@ -22,63 +24,10 @@ from aqt.utils import ( save_combo_index_for_session, save_is_checked, saveGeom, - tooltip, tr, ) -def find_and_replace( - *, - mw: AnkiQt, - parent: QWidget, - note_ids: Sequence[NoteId], - search: str, - replacement: str, - regex: bool, - field_name: Optional[str], - match_case: bool, -) -> None: - mw.perform_op( - lambda: mw.col.find_and_replace( - note_ids=note_ids, - search=search, - replacement=replacement, - regex=regex, - field_name=field_name, - match_case=match_case, - ), - success=lambda out: tooltip( - tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)), - parent=parent, - ), - ) - - -def find_and_replace_tag( - *, - mw: AnkiQt, - parent: QWidget, - note_ids: Sequence[int], - search: str, - replacement: str, - regex: bool, - match_case: bool, -) -> None: - mw.perform_op( - lambda: mw.col.tags.find_and_replace( - note_ids=note_ids, - search=search, - replacement=replacement, - regex=regex, - match_case=match_case, - ), - success=lambda out: tooltip( - tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)), - parent=parent, - ), - ) - - class FindAndReplaceDialog(QDialog): COMBO_NAME = "BrowserFindAndReplace" @@ -146,10 +95,9 @@ class FindAndReplaceDialog(QDialog): save_is_checked(self.form.re, self.COMBO_NAME + "Regex") save_is_checked(self.form.ignoreCase, self.COMBO_NAME + "ignoreCase") + # tags? if self.form.field.currentIndex() == 1: - # tags find_and_replace_tag( - mw=self.mw, parent=self.parentWidget(), note_ids=self.note_ids, search=search, @@ -157,23 +105,22 @@ class FindAndReplaceDialog(QDialog): regex=regex, match_case=match_case, ) - return - - if self.form.field.currentIndex() == 0: - field = None else: - field = self.field_names[self.form.field.currentIndex() - 2] + # fields + if self.form.field.currentIndex() == 0: + field = None + else: + field = self.field_names[self.form.field.currentIndex() - 2] - find_and_replace( - mw=self.mw, - parent=self.parentWidget(), - note_ids=self.note_ids, - search=search, - replacement=replace, - regex=regex, - field_name=field, - match_case=match_case, - ) + find_and_replace( + parent=self.parentWidget(), + note_ids=self.note_ids, + search=search, + replacement=replace, + regex=regex, + field_name=field, + match_case=match_case, + ) super().accept() diff --git a/qt/aqt/main.py b/qt/aqt/main.py index a977940e9..7b2436558 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -52,11 +52,6 @@ from aqt.emptycards import show_empty_cards from aqt.legacy import install_pylib_legacy from aqt.mediacheck import check_media_db from aqt.mediasync import MediaSyncer -from aqt.operations import ( - CollectionOpFailureCallback, - CollectionOpSuccessCallback, - ResultWithChanges, -) from aqt.operations.collection import undo from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * @@ -96,9 +91,6 @@ MainWindowState = Literal[ T = TypeVar("T") -PerformOpOptionalSuccessCallback = Optional[CollectionOpSuccessCallback] -PerformOpOptionalFailureCallback = Optional[CollectionOpFailureCallback] - class AnkiQt(QMainWindow): col: Collection @@ -710,10 +702,9 @@ class AnkiQt(QMainWindow): ) -> None: """Run an operation that queries the DB on a background thread. - Similar interface to perform_op(), but intended to be used for operations - that do not change collection state. Undo status will not be changed, - and `operation_did_execute` will not fire. No progress window will - be shown either. + Intended to be used for operations that do not change collection + state. Undo status will not be changed, and `operation_did_execute` + will not fire. No progress window will be shown either. `operations_will|did_execute` will still fire, so the UI can defer updates during a background task. @@ -743,66 +734,6 @@ class AnkiQt(QMainWindow): # Resetting state ########################################################################## - def perform_op( - self, - op: Callable[[], ResultWithChanges], - *, - success: PerformOpOptionalSuccessCallback = None, - failure: PerformOpOptionalFailureCallback = None, - handler: Optional[object] = None, - ) -> None: - """Run the provided operation on a background thread. - - op() should either return OpChanges, or an object with a 'changes' - property. The changes will be passed to `operation_did_execute` so that - the UI can decide whether it needs to update itself. - - - Shows progress popup for the duration of the op. - - Ensures the browser doesn't try to redraw during the operation, which can lead - to a frozen UI - - Updates undo state at the end of the operation - - Commits changes - - Fires the `operation_(will|did)_reset` hooks - - Fires the legacy `state_did_reset` hook - - Be careful not to call any UI routines in `op`, as that may crash Qt. - This includes things select .selectedCards() in the browse screen. - - success() will be called with the return value of op(). - - If op() throws an exception, it will be shown in a popup, or - passed to failure() if it is provided. - """ - - self._increase_background_ops() - - def wrapped_done(future: Future) -> None: - self._decrease_background_ops() - # did something go wrong? - if exception := future.exception(): - if isinstance(exception, Exception): - if failure: - failure(exception) - else: - showWarning(str(exception)) - return - else: - # BaseException like SystemExit; rethrow it - future.result() - - result = future.result() - try: - if success: - success(result) - finally: - # update undo status - status = self.col.undo_status() - self._update_undo_actions_for_status_and_save(status) - # fire change hooks - self._fire_change_hooks_after_op_performed(result, handler) - - self.taskman.with_progress(op, wrapped_done) - def _increase_background_ops(self) -> None: if not self._background_op_count: gui_hooks.backend_will_block() @@ -814,24 +745,6 @@ class AnkiQt(QMainWindow): gui_hooks.backend_did_block() assert self._background_op_count >= 0 - def _fire_change_hooks_after_op_performed( - self, - result: ResultWithChanges, - handler: Optional[object], - ) -> None: - if isinstance(result, OpChanges): - changes = result - else: - changes = result.changes - - # fire new hook - print("op changes:") - print(changes) - gui_hooks.operation_did_execute(changes, handler) - # fire legacy hook so old code notices changes - if self.col.op_made_changes(changes): - gui_hooks.state_did_reset() - def _synthesize_op_did_execute_from_reset(self) -> None: """Fire the `operation_did_execute` hook with everything marked as changed, after legacy code has called .reset()""" @@ -879,7 +792,7 @@ class AnkiQt(QMainWindow): def reset(self, unused_arg: bool = False) -> None: """Legacy method of telling UI to refresh after changes made to DB. - New code should use mw.perform_op() instead.""" + New code should use CollectionOp() instead.""" if self.col: # fire new `operation_did_execute` hook first. If the overview # or review screen are currently open, they will rebuild the study diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index 360be305d..f416f1075 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -36,9 +36,6 @@ ResultWithChanges = TypeVar( ], ) -CollectionOpSuccessCallback = Callable[[ResultWithChanges], Any] -CollectionOpFailureCallback = Optional[Callable[[Exception], Any]] - class CollectionOp(Generic[ResultWithChanges]): """Helper to perform a mutating DB operation on a background thread, and update UI. @@ -65,7 +62,7 @@ class CollectionOp(Generic[ResultWithChanges]): """ _success: Optional[Callable[[ResultWithChanges], Any]] = None - _failure: Optional[CollectionOpFailureCallback] = None + _failure: Optional[Optional[Callable[[Exception], Any]]] = None def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]): self._parent = parent @@ -78,19 +75,25 @@ class CollectionOp(Generic[ResultWithChanges]): return self def failure( - self, failure: Optional[CollectionOpFailureCallback] + self, failure: Optional[Optional[Callable[[Exception], Any]]] ) -> CollectionOp[ResultWithChanges]: self._failure = failure return self def run_in_background(self, *, initiator: Optional[object] = None) -> None: - aqt.mw._increase_background_ops() + from aqt import mw + + assert mw + + mw._increase_background_ops() def wrapped_op() -> ResultWithChanges: - return self._op(aqt.mw.col) + assert mw + return self._op(mw.col) def wrapped_done(future: Future) -> None: - aqt.mw._decrease_background_ops() + assert mw + mw._decrease_background_ops() # did something go wrong? if exception := future.exception(): if isinstance(exception, Exception): @@ -109,18 +112,22 @@ class CollectionOp(Generic[ResultWithChanges]): self._success(result) finally: # update undo status - status = aqt.mw.col.undo_status() - aqt.mw._update_undo_actions_for_status_and_save(status) + status = mw.col.undo_status() + mw._update_undo_actions_for_status_and_save(status) # fire change hooks self._fire_change_hooks_after_op_performed(result, initiator) - aqt.mw.taskman.with_progress(wrapped_op, wrapped_done) + mw.taskman.with_progress(wrapped_op, wrapped_done) def _fire_change_hooks_after_op_performed( self, result: ResultWithChanges, handler: Optional[object], ) -> None: + from aqt import mw + + assert mw + if isinstance(result, OpChanges): changes = result else: @@ -131,5 +138,5 @@ class CollectionOp(Generic[ResultWithChanges]): print(changes) aqt.gui_hooks.operation_did_execute(changes, handler) # fire legacy hook so old code notices changes - if aqt.mw.col.op_made_changes(changes): + if mw.col.op_made_changes(changes): aqt.gui_hooks.state_did_reset() diff --git a/qt/aqt/operations/collection.py b/qt/aqt/operations/collection.py index 7d86f3cb7..1eb130b8c 100644 --- a/qt/aqt/operations/collection.py +++ b/qt/aqt/operations/collection.py @@ -31,6 +31,9 @@ def undo(*, parent: QWidget) -> None: def _legacy_undo(*, parent: QWidget) -> None: from aqt import mw + assert mw + assert mw.col + reviewing = mw.state == "review" just_refresh_reviewer = False diff --git a/qt/aqt/operations/note.py b/qt/aqt/operations/note.py index dae7f8287..de5be2f6b 100644 --- a/qt/aqt/operations/note.py +++ b/qt/aqt/operations/note.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Sequence +from typing import Optional, Sequence from anki.collection import OpChanges, OpChangesWithCount from anki.decks import DeckId @@ -34,3 +34,31 @@ def remove_notes( return CollectionOp(parent, lambda col: col.remove_notes(note_ids)).success( lambda out: tooltip(tr.browsing_cards_deleted(count=out.count)), ) + + +def find_and_replace( + *, + parent: QWidget, + note_ids: Sequence[NoteId], + search: str, + replacement: str, + regex: bool, + field_name: Optional[str], + match_case: bool, +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp( + parent, + lambda col: col.find_and_replace( + note_ids=note_ids, + search=search, + replacement=replacement, + regex=regex, + field_name=field_name, + match_case=match_case, + ), + ).success( + lambda out: tooltip( + tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)), + parent=parent, + ) + ) diff --git a/qt/aqt/operations/scheduling.py b/qt/aqt/operations/scheduling.py index a9a71c948..384aa0602 100644 --- a/qt/aqt/operations/scheduling.py +++ b/qt/aqt/operations/scheduling.py @@ -28,6 +28,7 @@ def set_due_date_dialog( card_ids: Sequence[CardId], config_key: Optional[Config.String.Key.V], ) -> Optional[CollectionOp[OpChanges]]: + assert aqt.mw if not card_ids: return None @@ -76,7 +77,9 @@ def reposition_new_cards_dialog( ) -> Optional[CollectionOp[OpChangesWithCount]]: from aqt import mw + assert mw assert mw.col.db + row = mw.col.db.first( f"select min(due), max(due) from cards where type={CARD_TYPE_NEW} and odid=0" ) diff --git a/qt/aqt/operations/tag.py b/qt/aqt/operations/tag.py index cd33e2f17..33ee3b9b5 100644 --- a/qt/aqt/operations/tag.py +++ b/qt/aqt/operations/tag.py @@ -90,3 +90,29 @@ def set_tag_collapsed( return CollectionOp( parent, lambda col: col.tags.set_collapsed(tag=tag, collapsed=collapsed) ) + + +def find_and_replace_tag( + *, + parent: QWidget, + note_ids: Sequence[int], + search: str, + replacement: str, + regex: bool, + match_case: bool, +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp( + parent, + lambda col: col.tags.find_and_replace( + note_ids=note_ids, + search=search, + replacement=replacement, + regex=regex, + match_case=match_case, + ), + ).success( + lambda out: tooltip( + tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)), + parent=parent, + ), + ) diff --git a/qt/aqt/taskman.py b/qt/aqt/taskman.py index 3189024f3..a1759ed1c 100644 --- a/qt/aqt/taskman.py +++ b/qt/aqt/taskman.py @@ -4,7 +4,7 @@ """ Helper for running tasks on background threads. -See mw.query_op() and mw.perform_op() for slightly higher-level routines. +See mw.query_op() and CollectionOp() for higher-level routines. """ from __future__ import annotations diff --git a/qt/mypy.ini b/qt/mypy.ini index d14f07fba..72f33a3a4 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -9,17 +9,7 @@ check_untyped_defs = true disallow_untyped_defs = True strict_equality = true -[mypy-aqt.scheduling_ops] -no_strict_optional = false -[mypy-aqt.note_ops] -no_strict_optional = false -[mypy-aqt.card_ops] -no_strict_optional = false -[mypy-aqt.deck_ops] -no_strict_optional = false -[mypy-aqt.find_and_replace] -no_strict_optional = false -[mypy-aqt.tag_ops] +[mypy-aqt.operations.*] no_strict_optional = false [mypy-aqt.winpaths] diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index f4b50570f..5b4f2b4ab 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -451,7 +451,7 @@ hooks = [ Hook( name="state_did_reset", legacy_hook="reset", - doc="""Legacy 'reset' hook. Called by mw.reset() and mw.perform_op() to redraw the UI. + doc="""Legacy 'reset' hook. Called by mw.reset() and CollectionOp() to redraw the UI. New code should use `operation_did_execute` instead. """, @@ -476,7 +476,7 @@ hooks = [ ), Hook( name="backend_will_block", - doc="""Called before one or more operations are executed with mw.perform_op(). + doc="""Called before one or more DB tasks are run in the background. Subscribers can use this to set a flag to avoid DB queries until the operation completes, as doing so will freeze the UI until the long-running operation @@ -485,7 +485,7 @@ hooks = [ ), Hook( name="backend_did_block", - doc="""Called after one or more operations are executed with mw.perform_op(). + doc="""Called after one or more DB tasks finish running in the background. Called regardless of the success of individual operations, and only called when there are no outstanding ops. """, diff --git a/rslib/i18n/build/extract.rs b/rslib/i18n/build/extract.rs index 5c78ed2e0..dc09ed095 100644 --- a/rslib/i18n/build/extract.rs +++ b/rslib/i18n/build/extract.rs @@ -163,7 +163,7 @@ impl From for Variable { let kind = match name.as_str() { "cards" | "notes" | "count" | "amount" | "reviews" | "total" | "selected" | "kilobytes" | "daysStart" | "daysEnd" | "days" | "secs-per-card" | "remaining" - | "hourStart" | "hourEnd" | "correct" | "decks" => VariableKind::Int, + | "hourStart" | "hourEnd" | "correct" | "decks" | "changed" => VariableKind::Int, "average-seconds" | "cards-per-minute" => VariableKind::Float, "val" | "found" | "expected" | "part" | "percent" | "day" | "number" | "up" | "down" | "seconds" | "megs" => VariableKind::Any, From 6e954e82a5f9970b050785ea4736b50fe127b175 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 21:37:31 +1000 Subject: [PATCH 09/16] current deck change is now undoable - make sure we set flag in changes when config var changed - move current deck get/set into backend - set_config() now returns a bool indicating whether a change was made, so other operations can be gated off it - active decks generation is deferred until sched.reset() --- pylib/anki/decks.py | 33 +++++++++++++++-------------- pylib/anki/scheduler/base.py | 5 +++-- pylib/anki/scheduler/v1.py | 3 +-- pylib/anki/scheduler/v2.py | 21 ++++++++++++------ pylib/tests/test_decks.py | 1 + qt/aqt/deckbrowser.py | 12 ++++++----- qt/aqt/main.py | 8 +++++-- qt/aqt/operations/deck.py | 4 ++++ rslib/backend.proto | 4 +++- rslib/src/backend/config.rs | 6 +++--- rslib/src/backend/decks.rs | 10 +++++++++ rslib/src/backend/ops.rs | 2 +- rslib/src/card/mod.rs | 2 +- rslib/src/config/bool.rs | 2 +- rslib/src/config/deck.rs | 8 +------ rslib/src/config/mod.rs | 17 ++++++++++++--- rslib/src/config/notetype.rs | 3 ++- rslib/src/config/string.rs | 2 +- rslib/src/config/undo.rs | 14 ++++++++---- rslib/src/decks/current.rs | 41 ++++++++++++++++++++++++++++++++++++ rslib/src/decks/mod.rs | 1 + rslib/src/ops.rs | 17 ++++++++++----- rslib/src/undo/mod.rs | 2 +- 23 files changed, 155 insertions(+), 63 deletions(-) create mode 100644 rslib/src/decks/current.rs diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 8eb6807e6..8bd047a54 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -447,30 +447,31 @@ class DeckManager: # Deck selection ############################################################# - def active(self) -> List[DeckId]: - "The currrently active dids." - return self.col.get_config("activeDecks", [1]) + def get_current(self) -> Deck: + return self.col._backend.get_current_deck() - def selected(self) -> DeckId: - "The currently selected did." - return DeckId(int(self.col.conf["curDeck"])) + def set_current(self, deck: DeckId) -> OpChanges: + return self.col._backend.set_current_deck(deck) + + def get_current_id(self) -> DeckId: + "The currently selected deck ID." + return DeckId(self.get_current().id) + + # legacy def current(self) -> DeckDict: return self.get(self.selected()) def select(self, did: DeckId) -> None: - "Select a new branch." # make sure arg is an int; legacy callers may be passing in a string did = DeckId(did) - current = self.selected() - active = self.deck_and_child_ids(did) - if current != did or active != self.active(): - self.col.conf["curDeck"] = did - self.col.conf["activeDecks"] = active + self.set_current(did) + self.col.reset() - # don't use this, it will likely go away - def update_active(self) -> None: - self.select(self.current()["id"]) + def active(self) -> List[DeckId]: + return self.col.sched.active_decks + + selected = get_current_id # Parents/children ############################################################# @@ -518,7 +519,7 @@ class DeckManager: ) def deck_and_child_ids(self, deck_id: DeckId) -> List[DeckId]: - parent_name = self.get_legacy(deck_id)["name"] + parent_name = self.col.get_deck(deck_id).name out = [deck_id] out.extend(self.child_ids(parent_name)) return out diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py index 268211e06..a9f8b9ea9 100644 --- a/pylib/anki/scheduler/base.py +++ b/pylib/anki/scheduler/base.py @@ -85,8 +85,9 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l # fixme: only used by totalRevForCurrentDeck and old deck stats; # schedv2 defines separate version def _deckLimit(self) -> str: - self.col.decks.update_active() - return ids2str(self.col.decks.active()) + return ids2str( + self.col.decks.deck_and_child_ids(self.col.decks.get_current_id()) + ) # Filtered deck handling ########################################################################## diff --git a/pylib/anki/scheduler/v1.py b/pylib/anki/scheduler/v1.py index 1c3fcc885..74920972a 100644 --- a/pylib/anki/scheduler/v1.py +++ b/pylib/anki/scheduler/v1.py @@ -33,7 +33,7 @@ class Scheduler(V2): def __init__( # pylint: disable=super-init-not-called self, col: anki.collection.Collection ) -> None: - self.col = col.weakref() + super().__init__(col) self.queueLimit = 50 self.reportLimit = 1000 self.dynReportLimit = 99999 @@ -42,7 +42,6 @@ class Scheduler(V2): self.revCount = 0 self.newCount = 0 self._haveQueues = False - self._updateCutoff() def answerCard(self, card: Card, ease: int) -> None: self.col.log() diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py index 22b8a7aa6..22081cf58 100644 --- a/pylib/anki/scheduler/v2.py +++ b/pylib/anki/scheduler/v2.py @@ -48,7 +48,16 @@ class Scheduler(SchedulerBaseWithLegacy): self.reps = 0 self._haveQueues = False self._lrnCutoff = 0 - self._updateCutoff() + self._active_decks: List[DeckId] = [] + self._current_deck_id = DeckId(1) + + @property + def active_decks(self) -> List[DeckId]: + "Caller must make sure to make a copy." + return self._active_decks + + def _update_active_decks(self) -> None: + self._active_decks = self.col.decks.deck_and_child_ids(self._current_deck_id) # Daily cutoff ########################################################################## @@ -65,8 +74,8 @@ class Scheduler(SchedulerBaseWithLegacy): ########################################################################## def reset(self) -> None: - self.col.decks.update_active() - self._updateCutoff() + self._current_deck_id = self.col.decks.selected() + self._update_active_decks() self._reset_counts() self._resetLrn() self._resetRev() @@ -74,10 +83,8 @@ class Scheduler(SchedulerBaseWithLegacy): self._haveQueues = True def _reset_counts(self) -> None: - tree = self.deck_due_tree(self.col.decks.selected()) - node = self.col.decks.find_deck_in_tree( - tree, DeckId(int(self.col.conf["curDeck"])) - ) + tree = self.deck_due_tree(self._current_deck_id) + node = self.col.decks.find_deck_in_tree(tree, self._current_deck_id) if not node: # current deck points to a missing deck self.newCount = 0 diff --git a/pylib/tests/test_decks.py b/pylib/tests/test_decks.py index 74f6e5589..82e0dffcf 100644 --- a/pylib/tests/test_decks.py +++ b/pylib/tests/test_decks.py @@ -18,6 +18,7 @@ def test_basic(): assert col.decks.id("new deck") == parentId # we start with the default col selected assert col.decks.selected() == 1 + col.reset() assert col.decks.active() == [1] # we can select a different col col.decks.select(parentId) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 74455ac4a..b795a8401 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -17,6 +17,7 @@ from aqt.operations.deck import ( remove_decks, rename_deck, reparent_decks, + set_current_deck, set_deck_collapsed, ) from aqt.qt import * @@ -79,7 +80,7 @@ class DeckBrowser: def op_executed( self, changes: OpChanges, handler: Optional[object], focused: bool ) -> bool: - if changes.study_queues: + if changes.study_queues and handler is not self: self._refresh_needed = True if focused: @@ -96,7 +97,7 @@ class DeckBrowser: else: cmd = url if cmd == "open": - self._selDeck(arg) + self.set_current_deck(DeckId(int(arg))) elif cmd == "opts": self._showOptions(arg) elif cmd == "shared": @@ -119,9 +120,10 @@ class DeckBrowser: self.refresh() return False - def _selDeck(self, did: str) -> None: - self.mw.col.decks.select(DeckId(int(did))) - self.mw.onOverview() + def set_current_deck(self, deck_id: DeckId) -> None: + set_current_deck(parent=self.mw, deck_id=deck_id).success( + lambda _: self.mw.onOverview() + ).run_in_background(initiator=self) # HTML generation ########################################################################## diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 7b2436558..ed9bdb9c1 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -53,6 +53,7 @@ from aqt.legacy import install_pylib_legacy from aqt.mediacheck import check_media_db from aqt.mediasync import MediaSyncer from aqt.operations.collection import undo +from aqt.operations.deck import set_current_deck from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * from aqt.qt import sip @@ -1425,8 +1426,11 @@ title="%s" %s>%s""" % ( ret = StudyDeck(self, dyn=True, current=self.col.decks.current()["name"]) if ret.name: - self.col.decks.select(self.col.decks.id(ret.name)) - self.moveToState("overview") + # fixme: this is silly, it should be returning an ID + deck_id = self.col.decks.id(ret.name) + set_current_deck(parent=self, deck_id=deck_id).success( + lambda out: self.moveToState("overview") + ).run_in_background() def onEmptyCards(self) -> None: show_empty_cards(self) diff --git a/qt/aqt/operations/deck.py b/qt/aqt/operations/deck.py index d4d98adeb..a8d69bd89 100644 --- a/qt/aqt/operations/deck.py +++ b/qt/aqt/operations/deck.py @@ -76,3 +76,7 @@ def set_deck_collapsed( deck_id=deck_id, collapsed=collapsed, scope=scope ), ) + + +def set_current_deck(*, parent: QWidget, deck_id: DeckId) -> CollectionOp[OpChanges]: + return CollectionOp(parent, lambda col: col.decks.set_current(deck_id)) diff --git a/rslib/backend.proto b/rslib/backend.proto index 7a0b92de5..f7ba5c40d 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -154,6 +154,8 @@ service DecksService { rpc GetOrCreateFilteredDeck(DeckId) returns (FilteredDeckForUpdate); rpc AddOrUpdateFilteredDeck(FilteredDeckForUpdate) returns (OpChangesWithId); rpc FilteredDeckOrderLabels(Empty) returns (StringList); + rpc SetCurrentDeck(DeckId) returns (OpChanges); + rpc GetCurrentDeck(Empty) returns (Deck); } service NotesService { @@ -1504,7 +1506,7 @@ message OpChanges { bool deck = 3; bool tag = 4; bool notetype = 5; - bool preference = 6; + bool config = 6; bool browser_table = 7; bool browser_sidebar = 8; diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index 837f30df4..a1aeaabd1 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -67,7 +67,7 @@ impl ConfigService for Backend { col.transact_no_undo(|col| { // ensure it's a well-formed object let val: Value = serde_json::from_slice(&input.value_json)?; - col.set_config(input.key.as_str(), &val) + col.set_config(input.key.as_str(), &val).map(|_| ()) }) }) .map(Into::into) @@ -98,7 +98,7 @@ impl ConfigService for Backend { self.with_col(|col| { col.transact_no_undo(|col| col.set_bool(input.key().into(), input.value)) }) - .map(Into::into) + .map(|_| ().into()) } fn get_config_string(&self, input: pb::config::String) -> Result { @@ -113,7 +113,7 @@ impl ConfigService for Backend { self.with_col(|col| { col.transact_no_undo(|col| col.set_string(input.key().into(), &input.value)) }) - .map(Into::into) + .map(|_| ().into()) } fn get_preferences(&self, _input: pb::Empty) -> Result { diff --git a/rslib/src/backend/decks.rs b/rslib/src/backend/decks.rs index a4d72b396..578d6b558 100644 --- a/rslib/src/backend/decks.rs +++ b/rslib/src/backend/decks.rs @@ -187,6 +187,16 @@ impl DecksService for Backend { }) .map(Into::into) } + + fn set_current_deck(&self, input: pb::DeckId) -> Result { + self.with_col(|col| col.set_current_deck(input.did.into())) + .map(Into::into) + } + + fn get_current_deck(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.get_current_deck()) + .map(|deck| (*deck).clone().into()) + } } impl From for DeckId { diff --git a/rslib/src/backend/ops.rs b/rslib/src/backend/ops.rs index 001357923..4bbda0a1a 100644 --- a/rslib/src/backend/ops.rs +++ b/rslib/src/backend/ops.rs @@ -16,7 +16,7 @@ impl From for pb::OpChanges { deck: c.changes.deck, tag: c.changes.tag, notetype: c.changes.notetype, - preference: c.changes.preference, + config: c.changes.config, browser_table: c.requires_browser_table_redraw(), browser_sidebar: c.requires_browser_sidebar_redraw(), editor: c.requires_editor_redraw(), diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index 2b84e682c..6bfdf01b4 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -239,7 +239,7 @@ impl Collection { self.storage.set_search_table_to_card_ids(cards, false)?; let sched = self.scheduler_version(); let usn = self.usn()?; - self.transact(Op::SetDeck, |col| { + self.transact(Op::SetCardDeck, |col| { for mut card in col.storage.all_searched_cards()? { if card.deck_id == deck_id { continue; diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index 3dc0074f1..517a9d499 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -69,7 +69,7 @@ impl Collection { } } - pub(crate) fn set_bool(&mut self, key: BoolKey, value: bool) -> Result<()> { + pub(crate) fn set_bool(&mut self, key: BoolKey, value: bool) -> Result { self.set_config(key, &value) } } diff --git a/rslib/src/config/deck.rs b/rslib/src/config/deck.rs index e5ca56ef9..0eef0474c 100644 --- a/rslib/src/config/deck.rs +++ b/rslib/src/config/deck.rs @@ -1,7 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use super::ConfigKey; use crate::prelude::*; use strum::IntoStaticStr; @@ -20,11 +19,6 @@ impl DeckConfigKey { } impl Collection { - pub(crate) fn get_current_deck_id(&self) -> DeckId { - self.get_config_optional(ConfigKey::CurrentDeckId) - .unwrap_or(DeckId(1)) - } - pub(crate) fn clear_aux_config_for_deck(&self, ntid: DeckId) -> Result<()> { self.remove_config_prefix(&build_aux_deck_key(ntid, "")) } @@ -38,7 +32,7 @@ impl Collection { &mut self, did: DeckId, ntid: NotetypeId, - ) -> Result<()> { + ) -> Result { let key = DeckConfigKey::LastNotetype.for_deck(did); self.set_config(key.as_str(), &ntid) } diff --git a/rslib/src/config/mod.rs b/rslib/src/config/mod.rs index c5cfcc59f..a18b7f733 100644 --- a/rslib/src/config/mod.rs +++ b/rslib/src/config/mod.rs @@ -103,7 +103,8 @@ impl Collection { self.get_config_optional(key).unwrap_or_default() } - pub(crate) fn set_config<'a, T: Serialize, K>(&mut self, key: K, val: &T) -> Result<()> + /// True if added, or new value is different. + pub(crate) fn set_config<'a, T: Serialize, K>(&mut self, key: K, val: &T) -> Result where K: Into<&'a str>, { @@ -148,6 +149,7 @@ impl Collection { columns: Vec, ) -> Result<()> { self.set_config(ConfigKey::DesktopBrowserCardColumns, &columns) + .map(|_| ()) } pub(crate) fn get_desktop_browser_note_columns(&self) -> Option> { @@ -159,6 +161,7 @@ impl Collection { columns: Vec, ) -> Result<()> { self.set_config(ConfigKey::DesktopBrowserNoteColumns, &columns) + .map(|_| ()) } pub(crate) fn get_creation_utc_offset(&self) -> Option { @@ -169,6 +172,7 @@ impl Collection { self.state.scheduler_info = None; if let Some(mins) = mins { self.set_config(ConfigKey::CreationOffset, &mins) + .map(|_| ()) } else { self.remove_config(ConfigKey::CreationOffset) } @@ -180,7 +184,7 @@ impl Collection { pub(crate) fn set_configured_utc_offset(&mut self, mins: i32) -> Result<()> { self.state.scheduler_info = None; - self.set_config(ConfigKey::LocalOffset, &mins) + self.set_config(ConfigKey::LocalOffset, &mins).map(|_| ()) } pub(crate) fn get_v2_rollover(&self) -> Option { @@ -190,7 +194,7 @@ impl Collection { pub(crate) fn set_v2_rollover(&mut self, hour: u32) -> Result<()> { self.state.scheduler_info = None; - self.set_config(ConfigKey::Rollover, &hour) + self.set_config(ConfigKey::Rollover, &hour).map(|_| ()) } pub(crate) fn get_next_card_position(&self) -> u32 { @@ -207,6 +211,7 @@ impl Collection { pub(crate) fn set_next_card_position(&mut self, pos: u32) -> Result<()> { self.set_config(ConfigKey::NextNewCardPosition, &pos) + .map(|_| ()) } pub(crate) fn scheduler_version(&self) -> SchedulerVersion { @@ -218,6 +223,7 @@ impl Collection { pub(crate) fn set_scheduler_version_config_key(&mut self, ver: SchedulerVersion) -> Result<()> { self.state.scheduler_info = None; self.set_config(ConfigKey::SchedulerVersion, &ver) + .map(|_| ()) } pub(crate) fn learn_ahead_secs(&self) -> u32 { @@ -227,6 +233,7 @@ impl Collection { pub(crate) fn set_learn_ahead_secs(&mut self, secs: u32) -> Result<()> { self.set_config(ConfigKey::LearnAheadSecs, &secs) + .map(|_| ()) } pub(crate) fn get_new_review_mix(&self) -> NewReviewMix { @@ -239,6 +246,7 @@ impl Collection { pub(crate) fn set_new_review_mix(&mut self, mix: NewReviewMix) -> Result<()> { self.set_config(ConfigKey::NewReviewMix, &(mix as u8)) + .map(|_| ()) } pub(crate) fn get_first_day_of_week(&self) -> Weekday { @@ -248,6 +256,7 @@ impl Collection { pub(crate) fn set_first_day_of_week(&mut self, weekday: Weekday) -> Result<()> { self.set_config(ConfigKey::FirstDayOfWeek, &weekday) + .map(|_| ()) } pub(crate) fn get_answer_time_limit_secs(&self) -> u32 { @@ -257,6 +266,7 @@ impl Collection { pub(crate) fn set_answer_time_limit_secs(&mut self, secs: u32) -> Result<()> { self.set_config(ConfigKey::AnswerTimeLimitSecs, &secs) + .map(|_| ()) } pub(crate) fn get_last_unburied_day(&self) -> u32 { @@ -266,6 +276,7 @@ impl Collection { pub(crate) fn set_last_unburied_day(&mut self, day: u32) -> Result<()> { self.set_config(ConfigKey::LastUnburiedDay, &day) + .map(|_| ()) } } diff --git a/rslib/src/config/notetype.rs b/rslib/src/config/notetype.rs index 6a61276b0..c672964a9 100644 --- a/rslib/src/config/notetype.rs +++ b/rslib/src/config/notetype.rs @@ -30,6 +30,7 @@ impl Collection { pub(crate) fn set_current_notetype_id(&mut self, ntid: NotetypeId) -> Result<()> { self.set_config(ConfigKey::CurrentNotetypeId, &ntid) + .map(|_| ()) } pub(crate) fn clear_aux_config_for_notetype(&self, ntid: NotetypeId) -> Result<()> { @@ -43,7 +44,7 @@ impl Collection { pub(crate) fn set_last_deck_for_notetype(&mut self, id: NotetypeId, did: DeckId) -> Result<()> { let key = NotetypeConfigKey::LastDeckAddedTo.for_notetype(id); - self.set_config(key.as_str(), &did) + self.set_config(key.as_str(), &did).map(|_| ()) } } diff --git a/rslib/src/config/string.rs b/rslib/src/config/string.rs index d5aa918f8..45bda80fb 100644 --- a/rslib/src/config/string.rs +++ b/rslib/src/config/string.rs @@ -22,7 +22,7 @@ impl Collection { .unwrap_or_else(|| default.to_string()) } - pub(crate) fn set_string(&mut self, key: StringKey, val: &str) -> Result<()> { + pub(crate) fn set_string(&mut self, key: StringKey, val: &str) -> Result { self.set_config(key, &val) } } diff --git a/rslib/src/config/undo.rs b/rslib/src/config/undo.rs index 55e14f895..15288526f 100644 --- a/rslib/src/config/undo.rs +++ b/rslib/src/config/undo.rs @@ -21,16 +21,19 @@ impl Collection { .get_config_entry(&entry.key)? .ok_or_else(|| AnkiError::invalid_input("config disappeared"))?; self.update_config_entry_undoable(entry, current) + .map(|_| ()) } UndoableConfigChange::Removed(entry) => self.add_config_entry_undoable(entry), } } - pub(super) fn set_config_undoable(&mut self, entry: Box) -> Result<()> { + /// True if added, or value changed. + pub(super) fn set_config_undoable(&mut self, entry: Box) -> Result { if let Some(original) = self.storage.get_config_entry(&entry.key)? { self.update_config_entry_undoable(entry, original) } else { - self.add_config_entry_undoable(entry) + self.add_config_entry_undoable(entry)?; + Ok(true) } } @@ -49,16 +52,19 @@ impl Collection { Ok(()) } + /// True if new value differed. fn update_config_entry_undoable( &mut self, entry: Box, original: Box, - ) -> Result<()> { + ) -> Result { if entry.value != original.value { self.save_undo(UndoableConfigChange::Updated(original)); self.storage.set_config_entry(&entry)?; + Ok(true) + } else { + Ok(false) } - Ok(()) } } diff --git a/rslib/src/decks/current.rs b/rslib/src/decks/current.rs new file mode 100644 index 000000000..3b466a052 --- /dev/null +++ b/rslib/src/decks/current.rs @@ -0,0 +1,41 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::sync::Arc; + +use crate::{config::ConfigKey, prelude::*}; + +impl Collection { + pub fn set_current_deck(&mut self, deck: DeckId) -> Result> { + self.transact(Op::SetCurrentDeck, |col| col.set_current_deck_inner(deck)) + } + + /// Fetch the current deck, falling back to the default if the previously + /// selected deck is invalid. + pub fn get_current_deck(&mut self) -> Result> { + if let Some(deck) = self.get_deck(self.get_current_deck_id())? { + return Ok(deck); + } + self.get_deck(DeckId(1))?.ok_or(AnkiError::NotFound) + } +} + +impl Collection { + /// The returned id may reference a deck that does not exist; + /// prefer using get_current_deck() instead. + pub(crate) fn get_current_deck_id(&self) -> DeckId { + self.get_config_optional(ConfigKey::CurrentDeckId) + .unwrap_or(DeckId(1)) + } + + fn set_current_deck_inner(&mut self, deck: DeckId) -> Result<()> { + if self.set_current_deck_id(deck)? { + self.state.card_queues = None; + } + Ok(()) + } + + fn set_current_deck_id(&mut self, did: DeckId) -> Result { + self.set_config(ConfigKey::CurrentDeckId, &did) + } +} diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 22a2e6db9..24fd15f5b 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -2,6 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod counts; +mod current; mod filtered; mod schema11; mod tree; diff --git a/rslib/src/ops.rs b/rslib/src/ops.rs index 90b568ced..47c2848bb 100644 --- a/rslib/src/ops.rs +++ b/rslib/src/ops.rs @@ -23,7 +23,7 @@ pub enum Op { RenameTag, ReparentTag, ScheduleAsNew, - SetDeck, + SetCardDeck, SetDueDate, SetFlag, SortCards, @@ -34,6 +34,7 @@ pub enum Op { UpdateNote, UpdatePreferences, UpdateTag, + SetCurrentDeck, } impl Op { @@ -55,7 +56,7 @@ impl Op { Op::UpdateNote => tr.undo_update_note(), Op::UpdatePreferences => tr.preferences_preferences(), Op::UpdateTag => tr.undo_update_tag(), - Op::SetDeck => tr.browsing_change_deck(), + Op::SetCardDeck => tr.browsing_change_deck(), Op::SetFlag => tr.undo_set_flag(), Op::FindAndReplace => tr.browsing_find_and_replace(), Op::ClearUnusedTags => tr.browsing_clear_unused_tags(), @@ -68,6 +69,7 @@ impl Op { Op::RebuildFilteredDeck => tr.undo_build_filtered_deck(), Op::EmptyFilteredDeck => tr.studying_empty(), Op::ExpandCollapse => tr.undo_expand_collapse(), + Op::SetCurrentDeck => tr.browsing_change_deck(), } .into() } @@ -80,7 +82,7 @@ pub struct StateChanges { pub deck: bool, pub tag: bool, pub notetype: bool, - pub preference: bool, + pub config: bool, } #[derive(Debug, Clone, Copy)] @@ -134,7 +136,12 @@ impl OpChanges { pub fn requires_study_queue_rebuild(&self) -> bool { let c = &self.changes; - !matches!(self.op, Op::AnswerCard | Op::ExpandCollapse) - && (c.card || c.deck || c.preference) + if self.op == Op::AnswerCard { + return false; + } + + c.card + || (c.deck && self.op != Op::ExpandCollapse) + || (c.config && matches!(self.op, Op::SetCurrentDeck)) } } diff --git a/rslib/src/undo/mod.rs b/rslib/src/undo/mod.rs index 94040ce2e..88c5e1897 100644 --- a/rslib/src/undo/mod.rs +++ b/rslib/src/undo/mod.rs @@ -126,7 +126,7 @@ impl UndoManager { UndoableChange::Tag(_) => changes.tag = true, UndoableChange::Revlog(_) => {} UndoableChange::Queue(_) => {} - UndoableChange::Config(_) => {} // fixme: preferences? + UndoableChange::Config(_) => changes.config = true, } } From 4975f47ea30e52c1ed010ab72db8adf6afce457b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 7 Apr 2021 14:53:53 +1000 Subject: [PATCH 10/16] update to latest esbuild --- defs.bzl | 3 ++ repos.bzl | 32 -------------- ts/congrats/BUILD.bazel | 2 + ts/editor/BUILD.bazel | 2 + ts/esbuild/README.md | 4 ++ ts/esbuild/esbuild_repo.bzl | 42 ++++++++++++++++++ ts/esbuild/upstream.bzl | 86 +++++++++++++++++++++++++++++-------- ts/graphs/BUILD.bazel | 2 + 8 files changed, 124 insertions(+), 49 deletions(-) create mode 100644 ts/esbuild/README.md create mode 100644 ts/esbuild/esbuild_repo.bzl diff --git a/defs.bzl b/defs.bzl index 420a4b346..c6aca6f0f 100644 --- a/defs.bzl +++ b/defs.bzl @@ -9,6 +9,7 @@ load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install load("@io_bazel_rules_sass//:defs.bzl", "sass_repositories") load("@com_github_ali5h_rules_pip//:defs.bzl", "pip_import") load("//pip/pyqt5:defs.bzl", "install_pyqt5") +load("//ts/esbuild:esbuild_repo.bzl", "esbuild_dependencies") anki_version = "2.1.44" @@ -52,3 +53,5 @@ def setup_deps(): ) sass_repositories() + + esbuild_dependencies() diff --git a/repos.bzl b/repos.bzl index 836ac670b..6c0cb45fa 100644 --- a/repos.bzl +++ b/repos.bzl @@ -91,38 +91,6 @@ def register_repos(): urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.2.2/rules_nodejs-3.2.2.tar.gz"], ) - _ESBUILD_VERSION = "0.8.48" # reminder: update SHAs below when changing this value - - http_archive( - name = "esbuild_darwin", - urls = [ - "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-%s.tgz" % _ESBUILD_VERSION, - ], - strip_prefix = "package", - build_file_content = """exports_files(["bin/esbuild"])""", - sha256 = "d21a722873ed24586f071973b77223553fca466946f3d7e3976eeaccb14424e6", - ) - - http_archive( - name = "esbuild_windows", - urls = [ - "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-%s.tgz" % _ESBUILD_VERSION, - ], - strip_prefix = "package", - build_file_content = """exports_files(["esbuild.exe"])""", - sha256 = "fe5dcb97b4c47f9567012f0a45c19c655f3d2e0d76932f6dd12715dbebbd6eb0", - ) - - http_archive( - name = "esbuild_linux", - urls = [ - "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-%s.tgz" % _ESBUILD_VERSION, - ], - strip_prefix = "package", - build_file_content = """exports_files(["bin/esbuild"])""", - sha256 = "60dabe141e5dfcf99e7113bded6012868132068a582a102b258fb7b1cfdac14b", - ) - # sass ############ diff --git a/ts/congrats/BUILD.bazel b/ts/congrats/BUILD.bazel index 5d389a01e..cf72428fe 100644 --- a/ts/congrats/BUILD.bazel +++ b/ts/congrats/BUILD.bazel @@ -37,6 +37,8 @@ esbuild( args = [ "--global-name=anki", "--inject:$(location //ts:protobuf-shim.js)", + "--resolve-extensions=.mjs,.js", + "--log-level=warning", ], entry_point = "index.ts", external = [ diff --git a/ts/editor/BUILD.bazel b/ts/editor/BUILD.bazel index fb86760af..f2a6d6b0a 100644 --- a/ts/editor/BUILD.bazel +++ b/ts/editor/BUILD.bazel @@ -36,6 +36,8 @@ esbuild( name = "editor", args = [ "--loader:.svg=text", + "--resolve-extensions=.mjs,.js", + "--log-level=warning", ], entry_point = "index_wrapper.ts", visibility = ["//visibility:public"], diff --git a/ts/esbuild/README.md b/ts/esbuild/README.md new file mode 100644 index 000000000..4afa25887 --- /dev/null +++ b/ts/esbuild/README.md @@ -0,0 +1,4 @@ +This folder vendors the esbuild support in rules_nodejs while we wait +for some upstream changes to be applied: + +- https://github.com/bazelbuild/rules_nodejs/pull/2545 diff --git a/ts/esbuild/esbuild_repo.bzl b/ts/esbuild/esbuild_repo.bzl new file mode 100644 index 000000000..1bad45a7d --- /dev/null +++ b/ts/esbuild/esbuild_repo.bzl @@ -0,0 +1,42 @@ +""" Generated code; do not edit +Update by running yarn update-esbuild-versions + +Helper macro for fetching esbuild versions for internal tests and examples in rules_nodejs +""" + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +_VERSION = "0.11.5" + +def esbuild_dependencies(): + """Helper to install required dependencies for the esbuild rules""" + + version = _VERSION + + http_archive( + name = "esbuild_darwin", + urls = [ + "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-%s.tgz" % version, + ], + strip_prefix = "package", + build_file_content = """exports_files(["bin/esbuild"])""", + sha256 = "98436890727bdb0d4beddd9c9e07d0aeff0e8dfe0169f85e568eca0dd43f665e", + ) + http_archive( + name = "esbuild_windows", + urls = [ + "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-%s.tgz" % version, + ], + strip_prefix = "package", + build_file_content = """exports_files(["esbuild.exe"])""", + sha256 = "589c8ff97210bd41de106e6317ce88f9e88d2cacfd8178ae1217f2b857ff6c3a", + ) + http_archive( + name = "esbuild_linux", + urls = [ + "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-%s.tgz" % version, + ], + strip_prefix = "package", + build_file_content = """exports_files(["bin/esbuild"])""", + sha256 = "113c2e84895f4422a3676db4e15d9f01b2b4fac041edab25284fdb9574ba58a0", + ) diff --git a/ts/esbuild/upstream.bzl b/ts/esbuild/upstream.bzl index 53afb7e43..80d37b888 100644 --- a/ts/esbuild/upstream.bzl +++ b/ts/esbuild/upstream.bzl @@ -1,11 +1,4 @@ """ -NOTE: this file was forked from the following repo (Apache2) -https://github.com/bazelbuild/rules_nodejs/blob/c47b770a122e9614516df2e3fdca6fe0bf6e3420/packages/esbuild/esbuild.bzl - -Local changes not in upstream: -https://github.com/bazelbuild/rules_nodejs/pull/2545 -https://github.com/bazelbuild/rules_nodejs/pull/2564 - esbuild rule """ @@ -35,6 +28,9 @@ def _esbuild_impl(ctx): elif hasattr(dep, "files"): deps_depsets.append(dep.files) + if DefaultInfo in dep: + deps_depsets.append(dep[DefaultInfo].data_runfiles.files) + if NpmPackageInfo in dep: deps_depsets.append(dep[NpmPackageInfo].sources) npm_workspaces.append(dep[NpmPackageInfo].workspace) @@ -61,7 +57,12 @@ def _esbuild_impl(ctx): args = ctx.actions.args() args.add("--bundle", entry_point.path) - args.add("--sourcemap") + + if len(ctx.attr.sourcemap) > 0: + args.add_joined(["--sourcemap", ctx.attr.sourcemap], join_with = "=") + else: + args.add("--sourcemap") + args.add("--preserve-symlinks") args.add_joined(["--platform", ctx.attr.platform], join_with = "=") args.add_joined(["--target", ctx.attr.target], join_with = "=") @@ -70,8 +71,8 @@ def _esbuild_impl(ctx): args.add_all(ctx.attr.define, format_each = "--define:%s") args.add_all(ctx.attr.external, format_each = "--external:%s") - # disable the error limit and show all errors - args.add_joined(["--error-limit", "0"], join_with = "=") + # disable the log limit and show all logs + args.add_joined(["--log-limit", "0"], join_with = "=") if ctx.attr.minify: args.add("--minify") @@ -94,8 +95,14 @@ def _esbuild_impl(ctx): args.add_joined(["--outdir", js_out.path], join_with = "=") else: js_out = ctx.outputs.output + outputs.append(js_out) + js_out_map = ctx.outputs.output_map - outputs.extend([js_out, js_out_map]) + if ctx.attr.sourcemap != "inline": + if js_out_map == None: + fail("output_map must be specified if sourcemap is not set to 'inline'") + outputs.append(js_out_map) + if ctx.outputs.output_css: outputs.append(ctx.outputs.output_css) @@ -110,15 +117,23 @@ def _esbuild_impl(ctx): args.add_all([ctx.expand_location(arg) for arg in ctx.attr.args]) + env = {} + if ctx.attr.max_threads > 0: + env["GOMAXPROCS"] = str(ctx.attr.max_threads) + + execution_requirements = {} + if "no-remote-exec" in ctx.attr.tags: + execution_requirements = {"no-remote-exec": "1"} + ctx.actions.run( inputs = inputs, outputs = outputs, executable = ctx.executable.tool, arguments = [args], progress_message = "%s Javascript %s [esbuild]" % ("Bundling" if not ctx.attr.output_dir else "Splitting", entry_point.short_path), - execution_requirements = { - "no-remote-exec": "1", - }, + execution_requirements = execution_requirements, + mnemonic = "esbuild", + env = env, ) return [ @@ -144,6 +159,7 @@ esbuild( ], ) ``` + See https://esbuild.github.io/api/#define for more details """, ), @@ -160,6 +176,7 @@ See https://esbuild.github.io/api/#define for more details "external": attr.string_list( default = [], doc = """A list of module names that are treated as external and not included in the resulting bundle + See https://esbuild.github.io/api/#external for more details """, ), @@ -168,6 +185,7 @@ See https://esbuild.github.io/api/#external for more details mandatory = False, doc = """The output format of the bundle, defaults to iife when platform is browser and cjs when platform is node. If performing code splitting, defaults to esm. + See https://esbuild.github.io/api/#format for more details """, ), @@ -175,11 +193,20 @@ See https://esbuild.github.io/api/#format for more details doc = """Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'. If source files need to be required then they can be copied to the bin_dir with copy_to_bin.""", ), + "max_threads": attr.int( + mandatory = False, + doc = """Sets the `GOMAXPROCS` variable to limit the number of threads that esbuild can run with. +This can be useful if running many esbuild rule invocations in parallel, which has the potential to cause slowdown. +For general use, leave this attribute unset. + """, + ), "minify": attr.bool( default = False, doc = """Minifies the bundle with the built in minification. Removes whitespace, shortens identifieres and uses equivalent but shorter syntax. + Sets all --minify-* flags + See https://esbuild.github.io/api/#minify for more details """, ), @@ -190,6 +217,7 @@ See https://esbuild.github.io/api/#minify for more details "output_dir": attr.bool( default = False, doc = """If true, esbuild produces an output directory containing all the output files from code splitting + See https://esbuild.github.io/api/#splitting for more details """, ), @@ -205,13 +233,23 @@ See https://esbuild.github.io/api/#splitting for more details default = "browser", values = ["node", "browser", "neutral", ""], doc = """The platform to bundle for. + See https://esbuild.github.io/api/#platform for more details """, ), + "sourcemap": attr.string( + values = ["external", "inline", "both", ""], + mandatory = False, + doc = """Defines where sourcemaps are output and how they are included in the bundle. By default, a separate `.js.map` file is generated and referenced by the bundle. If 'external', a separate `.js.map` file is generated but not referenced by the bundle. If 'inline', a sourcemap is generated and its contents are inlined into the bundle (and no external sourcemap file is created). If 'both', a sourcemap is inlined and a `.js.map` file is created. + +See https://esbuild.github.io/api/#sourcemap for more details + """, + ), "sources_content": attr.bool( mandatory = False, default = False, doc = """If False, omits the `sourcesContent` field from generated source maps + See https://esbuild.github.io/api/#sources-content for more details """, ), @@ -219,12 +257,14 @@ See https://esbuild.github.io/api/#sources-content for more details allow_files = True, default = [], doc = """Non-entry point JavaScript source files from the workspace. + You must not repeat file(s) passed to entry_point""", ), "target": attr.string( default = "es2015", doc = """Environment target (e.g. es2017, chrome58, firefox57, safari11, edge16, node10, default esnext) + See https://esbuild.github.io/api/#target for more details """, ), @@ -238,17 +278,20 @@ See https://esbuild.github.io/api/#target for more details }, implementation = _esbuild_impl, doc = """Runs the esbuild bundler under Bazel + For further information about esbuild, see https://esbuild.github.io/ """, ) def esbuild_macro(name, output_dir = False, output_css = False, **kwargs): """esbuild helper macro around the `esbuild_bundle` rule + For a full list of attributes, see the `esbuild_bundle` rule + Args: name: The name used for this rule and output files output_dir: If `True`, produce a code split bundle in an output directory - output_css: If `True`, declare a .css file will be outputted, which is the + output_css: If `True`, declare name.css as an output, which is the case when your code imports a css file. **kwargs: All other args from `esbuild_bundle` """ @@ -260,10 +303,19 @@ def esbuild_macro(name, output_dir = False, output_css = False, **kwargs): **kwargs ) else: + output = "%s.js" % name + if "output" in kwargs: + output = kwargs.pop("output") + + output_map = None + sourcemap = kwargs.get("sourcemap", None) + if sourcemap != "inline": + output_map = "%s.map" % output + esbuild( name = name, - output = "%s.js" % name, - output_map = "%s.js.map" % name, + output = output, + output_map = output_map, output_css = None if not output_css else "%s.css" % name, **kwargs ) diff --git a/ts/graphs/BUILD.bazel b/ts/graphs/BUILD.bazel index 057b572d9..aa2aea72a 100644 --- a/ts/graphs/BUILD.bazel +++ b/ts/graphs/BUILD.bazel @@ -53,6 +53,8 @@ esbuild( args = [ "--global-name=anki", "--inject:$(location //ts:protobuf-shim.js)", + "--resolve-extensions=.mjs,.js", + "--log-level=warning", ], entry_point = "index.ts", external = [ From c1c14419a2312ce98cf1b9c4452147ec010590c1 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 7 Apr 2021 15:19:23 +1000 Subject: [PATCH 11/16] switch esbuild to a toolchain --- ts/esbuild.bzl | 5 ----- ts/esbuild/BUILD.bazel | 8 +++++++ ts/esbuild/esbuild_repo.bzl | 3 +++ ts/esbuild/toolchain.bzl | 42 +++++++++++++++++++++++++++++++++++++ ts/esbuild/upstream.bzl | 13 +++++------- 5 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 ts/esbuild/toolchain.bzl diff --git a/ts/esbuild.bzl b/ts/esbuild.bzl index e0c3021b7..3de24a8f5 100644 --- a/ts/esbuild.bzl +++ b/ts/esbuild.bzl @@ -3,11 +3,6 @@ load("//ts/esbuild:upstream.bzl", _esbuild = "esbuild_macro") def esbuild(name, **kwargs): _esbuild( name = name, - tool = select({ - "@bazel_tools//src/conditions:darwin": "@esbuild_darwin//:bin/esbuild", - "@bazel_tools//src/conditions:windows": "@esbuild_windows//:esbuild.exe", - "@bazel_tools//src/conditions:linux_x86_64": "@esbuild_linux//:bin/esbuild", - }), minify = select({ "//:release": True, "//conditions:default": False, diff --git a/ts/esbuild/BUILD.bazel b/ts/esbuild/BUILD.bazel index e69de29bb..59a42b85e 100644 --- a/ts/esbuild/BUILD.bazel +++ b/ts/esbuild/BUILD.bazel @@ -0,0 +1,8 @@ +load(":toolchain.bzl", "define_default_toolchains", "esbuild_toolchain") + +toolchain_type( + name = "toolchain_type", + visibility = ["//visibility:public"], +) + +define_default_toolchains() diff --git a/ts/esbuild/esbuild_repo.bzl b/ts/esbuild/esbuild_repo.bzl index 1bad45a7d..c507d9809 100644 --- a/ts/esbuild/esbuild_repo.bzl +++ b/ts/esbuild/esbuild_repo.bzl @@ -5,6 +5,7 @@ Helper macro for fetching esbuild versions for internal tests and examples in ru """ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load(":toolchain.bzl", "register_default_toolchains") _VERSION = "0.11.5" @@ -40,3 +41,5 @@ def esbuild_dependencies(): build_file_content = """exports_files(["bin/esbuild"])""", sha256 = "113c2e84895f4422a3676db4e15d9f01b2b4fac041edab25284fdb9574ba58a0", ) + + register_default_toolchains() diff --git a/ts/esbuild/toolchain.bzl b/ts/esbuild/toolchain.bzl new file mode 100644 index 000000000..dfd6d5898 --- /dev/null +++ b/ts/esbuild/toolchain.bzl @@ -0,0 +1,42 @@ +def _esbuild_toolchain_impl(ctx): + return [platform_common.ToolchainInfo( + binary = ctx.executable.binary, + )] + +esbuild_toolchain = rule( + implementation = _esbuild_toolchain_impl, + attrs = { + "binary": attr.label(allow_single_file = True, executable = True, cfg = "exec"), + }, +) + +_package_path = "@net_ankiweb_anki//ts/esbuild" + +TOOLCHAIN = _package_path + ":toolchain_type" + +_default_toolchains = [ + ["@esbuild_darwin//:bin/esbuild", "macos"], + ["@esbuild_linux//:bin/esbuild", "linux"], + ["@esbuild_windows//:esbuild.exe", "windows"], +] + +def define_default_toolchains(): + for repo_path, platform in _default_toolchains: + esbuild_toolchain( + name = "esbuild_" + platform, + binary = repo_path, + ) + + native.toolchain( + name = "esbuild_{}_toolchain".format(platform), + exec_compatible_with = [ + "@platforms//os:" + platform, + "@platforms//cpu:x86_64", + ], + toolchain = ":esbuild_" + platform, + toolchain_type = ":toolchain_type", + ) + +def register_default_toolchains(): + for _, platform in _default_toolchains: + native.register_toolchains(_package_path + ":esbuild_{}_toolchain".format(platform)) diff --git a/ts/esbuild/upstream.bzl b/ts/esbuild/upstream.bzl index 80d37b888..190addaee 100644 --- a/ts/esbuild/upstream.bzl +++ b/ts/esbuild/upstream.bzl @@ -5,6 +5,7 @@ esbuild rule load("@build_bazel_rules_nodejs//:providers.bzl", "JSEcmaScriptModuleInfo", "JSModuleInfo", "NpmPackageInfo", "node_modules_aspect") load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "MODULE_MAPPINGS_ASPECT_RESULTS_NAME", "module_mappings_aspect") load(":helpers.bzl", "filter_files", "generate_path_mapping", "resolve_js_input", "write_jsconfig_file") +load(":toolchain.bzl", "TOOLCHAIN") def _esbuild_impl(ctx): # For each dep, JSEcmaScriptModuleInfo is used if found, then JSModuleInfo and finally @@ -128,7 +129,7 @@ def _esbuild_impl(ctx): ctx.actions.run( inputs = inputs, outputs = outputs, - executable = ctx.executable.tool, + executable = ctx.toolchains[TOOLCHAIN].binary, arguments = [args], progress_message = "%s Javascript %s [esbuild]" % ("Bundling" if not ctx.attr.output_dir else "Splitting", entry_point.short_path), execution_requirements = execution_requirements, @@ -268,19 +269,15 @@ edge16, node10, default esnext) See https://esbuild.github.io/api/#target for more details """, ), - "tool": attr.label( - allow_single_file = True, - mandatory = True, - executable = True, - cfg = "exec", - doc = "An executable for the esbuild binary", - ), }, implementation = _esbuild_impl, doc = """Runs the esbuild bundler under Bazel For further information about esbuild, see https://esbuild.github.io/ """, + toolchains = [ + TOOLCHAIN, + ], ) def esbuild_macro(name, output_dir = False, output_css = False, **kwargs): From 9ce129efbcac0dc46cf15c3441e083fe60a95135 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 7 Apr 2021 18:28:53 +1000 Subject: [PATCH 12/16] update rules_nodejs for public toolchain fix --- repos.bzl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repos.bzl b/repos.bzl index 6c0cb45fa..c191178d1 100644 --- a/repos.bzl +++ b/repos.bzl @@ -87,8 +87,8 @@ def register_repos(): http_archive( name = "build_bazel_rules_nodejs", - sha256 = "55a25a762fcf9c9b88ab54436581e671bc9f4f523cb5a1bd32459ebec7be68a8", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.2.2/rules_nodejs-3.2.2.tar.gz"], + sha256 = "1af6e031b007c7a1e7d14654da837c33cbd22e325ccfd06747421e5fd6a115ea", + urls = ["https://github.com/ankitects/rules_nodejs/releases/download/anki-2021-04-07/release.tar.gz"], ) # sass From 28f830730ee5bed2eb9081b4717a86ccfd5a9677 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 7 Apr 2021 21:50:17 +1000 Subject: [PATCH 13/16] fix '\\' being converted to single backslash Appears the default changed in 3.x https://github.com/mathjax/MathJax/issues/2532 https://forums.ankiweb.net/t/double-backslashes-in-text-of-notes-are-turned-into-single-backslashes-on-display/9048 --- qt/aqt/data/web/js/mathjax.js | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/data/web/js/mathjax.js b/qt/aqt/data/web/js/mathjax.js index f5d99fb84..2d864715f 100644 --- a/qt/aqt/data/web/js/mathjax.js +++ b/qt/aqt/data/web/js/mathjax.js @@ -3,6 +3,7 @@ window.MathJax = { displayMath: [["\\[", "\\]"]], processRefs: false, processEnvironments: false, + processEscapes: false, packages: { "[+]": ["noerrors", "mhchem"], }, From 1ee4385fcf6f38ea7a16e8396ea54e694ba36279 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 8 Apr 2021 09:35:19 +1000 Subject: [PATCH 14/16] i18n tts error message --- ftl/qt/errors.ftl | 1 + qt/aqt/tts.py | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/ftl/qt/errors.ftl b/ftl/qt/errors.ftl index 8f7c86805..c3bca0475 100644 --- a/ftl/qt/errors.ftl +++ b/ftl/qt/errors.ftl @@ -40,3 +40,4 @@ errors-unable-open-collection = Anki was unable to open your collection file. If problems persist after restarting your computer, please use the Open Backup button in the profile manager. Debug info: +errors-windows-tts-runtime-error = The TTS service failed. Please ensure Windows updates are installed, try restarting your computer, and try using a different voice. diff --git a/qt/aqt/tts.py b/qt/aqt/tts.py index 2b71a79f5..2088f639e 100644 --- a/qt/aqt/tts.py +++ b/qt/aqt/tts.py @@ -41,7 +41,7 @@ from anki.sound import AVTag, TTSTag from anki.utils import checksum, isWin, tmpdir from aqt import gui_hooks from aqt.sound import OnDoneCallback, SimpleProcessPlayer -from aqt.utils import tooltip +from aqt.utils import tooltip, tr @dataclass @@ -569,10 +569,7 @@ if isWin: try: ret.result() except RuntimeError: - # fixme: i18n if this turns out to happen frequently - tooltip( - "TTS failed to play. Please check available languages in system settings." - ) + tooltip(tr.errors_windows_tts_runtime_error()) return # inject file into the top of the audio queue From 605ec7898fd47a67fbaae4be981b9f0051e19e77 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 8 Apr 2021 11:56:09 +1000 Subject: [PATCH 15/16] switch to new upstream rules_nodejs release --- repos.bzl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repos.bzl b/repos.bzl index c191178d1..2daa42ba0 100644 --- a/repos.bzl +++ b/repos.bzl @@ -87,8 +87,8 @@ def register_repos(): http_archive( name = "build_bazel_rules_nodejs", - sha256 = "1af6e031b007c7a1e7d14654da837c33cbd22e325ccfd06747421e5fd6a115ea", - urls = ["https://github.com/ankitects/rules_nodejs/releases/download/anki-2021-04-07/release.tar.gz"], + sha256 = "f533eeefc8fe1ddfe93652ec50f82373d0c431f7faabd5e6323f6903195ef227", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.3.0/rules_nodejs-3.3.0.tar.gz"], ) # sass From 1cc63f9267eb3facc514fab827d4146aaf14d3bb Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 9 Apr 2021 12:48:24 +1000 Subject: [PATCH 16/16] update to latest rules_rust incremental compilation --- .bazelrc | 10 +++------- docs/development.md | 2 +- repos.bzl | 6 +++--- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.bazelrc b/.bazelrc index 2e841bfbf..50abd5e9e 100644 --- a/.bazelrc +++ b/.bazelrc @@ -28,15 +28,11 @@ build --incompatible_default_to_explicit_init_py build:ci --show_timestamps --isatty=0 --color=yes --show_progress_rate_limit=5 -# incrementally compile Anki crates in fastbuild mode, but not release mode. -# The worker must be separately enabled; see docs/development.md -build -c fastbuild --@rules_rust//worker:include_regex=anki.*|rsbridge -build:opt -c opt --@rules_rust//worker:include_regex=no-crates-please -build --worker_max_instances=Rustc=HOST_CPUS*0.5 +# disable incremental compilation in release mode +build:opt -c opt --@rules_rust//:experimental_incremental_prefixes= # the TypeScript workers on Windows choke when deps are changed while they're -# still running, so shut them down at the end of the build. Also fixes issues -# with the optional Rust worker. +# still running, so shut them down at the end of the build. build:windows --worker_quit_after_build try-import %workspace%/user.bazelrc diff --git a/docs/development.md b/docs/development.md index c9a53ef38..f70ad95ff 100644 --- a/docs/development.md +++ b/docs/development.md @@ -153,7 +153,7 @@ following in your user.bazelrc file to enable incremental compilation when using ./run. ``` -build --@rules_rust//worker:cache_root=/path/to/folder/to/store/temp/files +build --@rules_rust//:experimental_incremental_base=/home/myuser/bazel/incremental ``` The worker support is experimental, so you may need to remove it in future diff --git a/repos.bzl b/repos.bzl index 2daa42ba0..2d9666768 100644 --- a/repos.bzl +++ b/repos.bzl @@ -33,11 +33,11 @@ def register_repos(): maybe( http_archive, name = "rules_rust", - strip_prefix = "rules_rust-anki-2021-03-30", + strip_prefix = "rules_rust-anki-2021-04-09", urls = [ - "https://github.com/ankitects/rules_rust/archive/anki-2021-03-30.tar.gz", + "https://github.com/ankitects/rules_rust/archive/anki-2021-04-09.tar.gz", ], - sha256 = "ad6286615fd21f71db4490207aa8d5ecdf5f526643cd65d682458d92aa84ff85", + sha256 = "2821b22e065c1b4dc73610b1d6ccbed7ed4d755b316e7e0641cd079b7abe4900", ) # python