diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 083e82d3e..c443d2878 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -75,6 +75,7 @@ Meredith Derecho Daniel Wallgren Kerrick Staley Maksim Abramchuk +Benjamin Kulnik ******************** diff --git a/Cargo.lock b/Cargo.lock index a1a6adf9b..353df4aa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "adler" -version = "0.2.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" @@ -32,9 +32,9 @@ dependencies = [ [[package]] name = "ammonia" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89eac85170f4b3fb3dc5e442c1cfb036cb8eecf9dbbd431a161ffad15d90ea3b" +checksum = "1ee7d6eb157f337c5cedc95ddf17f0cbc36d36eb7763c8e0d1c1aeb3722f6279" dependencies = [ "html5ever", "lazy_static", @@ -62,6 +62,7 @@ dependencies = [ "flate2", "fluent", "fluent-syntax", + "fnv", "futures", "hex", "htmlescape", @@ -73,7 +74,7 @@ dependencies = [ "num-integer", "num_enum", "once_cell", - "pin-project 1.0.5", + "pin-project", "proc-macro-nested", "prost", "prost-build", @@ -94,6 +95,7 @@ dependencies = [ "slog-async", "slog-envlogger", "slog-term", + "strum", "tempfile", "tokio", "unic-langid", @@ -196,14 +198,14 @@ dependencies = [ "flate2", "futures-core", "memchr", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", ] [[package]] name = "async-trait" -version = "0.1.42" +version = "0.1.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +checksum = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf" dependencies = [ "proc-macro2", "quote", @@ -261,9 +263,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bitvec" -version = "0.19.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" dependencies = [ "funty", "radium", @@ -299,9 +301,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" [[package]] name = "byteorder" @@ -333,9 +335,9 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" [[package]] name = "cfg-if" @@ -417,9 +419,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" dependencies = [ "autocfg", "cfg-if 1.0.0", @@ -510,9 +512,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e" +checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" dependencies = [ "atty", "humantime", @@ -636,9 +638,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", "percent-encoding", @@ -678,9 +680,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" +checksum = "7f55667319111d593ba876406af7c409c0ebb44dc4be6132a783ccf163ea14c1" dependencies = [ "futures-channel", "futures-core", @@ -693,9 +695,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" +checksum = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939" dependencies = [ "futures-core", "futures-sink", @@ -703,15 +705,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" +checksum = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94" [[package]] name = "futures-executor" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +checksum = "891a4b7b96d84d5940084b2a37632dd65deeae662c114ceaa2c879629c9c0ad1" dependencies = [ "futures-core", "futures-task", @@ -720,15 +722,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" +checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" [[package]] name = "futures-macro" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" +checksum = "ea405816a5139fb39af82c2beb921d52143f556038378d6db21183a5c37fbfb7" dependencies = [ "proc-macro-hack", "proc-macro2", @@ -738,24 +740,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" +checksum = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3" [[package]] name = "futures-task" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" -dependencies = [ - "once_cell", -] +checksum = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80" [[package]] name = "futures-util" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +checksum = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1" dependencies = [ "futures-channel", "futures-core", @@ -764,7 +763,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "pin-utils", "proc-macro-hack", "proc-macro-nested", @@ -887,9 +886,9 @@ dependencies = [ [[package]] name = "hex" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "html5ever" @@ -972,7 +971,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project 1.0.5", + "pin-project", "socket2", "tokio", "tower-service", @@ -1022,9 +1021,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de910d521f7cc3135c4de8db1cb910e0b5ed1dc6f57c381cd07e8e661ce10094" +checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" dependencies = [ "matches", "unicode-bidi", @@ -1033,9 +1032,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" dependencies = [ "autocfg", "hashbrown", @@ -1043,10 +1042,24 @@ dependencies = [ [[package]] name = "indoc" -version = "1.0.3" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136" +checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8" dependencies = [ + "indoc-impl", + "proc-macro-hack", +] + +[[package]] +name = "indoc-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", "unindent", ] @@ -1133,9 +1146,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "js-sys" -version = "0.3.47" +version = "0.3.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65" +checksum = "dc9f84f9b115ce7843d60706df1422a916680bfdfcbdb0447c5614ff9d7e4d78" dependencies = [ "wasm-bindgen", ] @@ -1158,22 +1171,22 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lexical-core" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" dependencies = [ "arrayvec 0.5.2", "bitflags", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "ryu", "static_assertions", ] [[package]] name = "libc" -version = "0.2.85" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" +checksum = "03b07a082330a35e43f63177cc01689da34fbffa0105e1246cf0311472cac73a" [[package]] name = "libsqlite3-sys" @@ -1275,9 +1288,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", "autocfg", @@ -1363,11 +1376,12 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "nom" -version = "6.1.0" +version = "6.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6f70b46d6325aa300f1c7bb3d470127dfc27806d8ea6bf294ee0ce643ce2b1" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" dependencies = [ "bitvec", + "funty", "lexical-core", "memchr", "version_check", @@ -1442,9 +1456,9 @@ checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" [[package]] name = "once_cell" -version = "1.5.2" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] name = "openssl" @@ -1492,23 +1506,36 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.1.57", + "redox_syscall 0.2.5", "smallvec", "winapi 0.3.9", ] [[package]] name = "paste" -version = "1.0.4" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] [[package]] name = "percent-encoding" @@ -1564,33 +1591,13 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" -dependencies = [ - "pin-project-internal 0.4.27", -] - [[package]] name = "pin-project" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63" dependencies = [ - "pin-project-internal 1.0.5", -] - -[[package]] -name = "pin-project-internal" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "pin-project-internal", ] [[package]] @@ -1606,15 +1613,15 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" +checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" [[package]] name = "pin-utils" @@ -1741,9 +1748,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00ca634cf3acd58a599b535ed6cb188223298977d471d146121792bfa23b754c" +checksum = "4837b8e8e18a102c23f79d1e9a110b597ea3b684c95e874eb1ad88f8683109c3" dependencies = [ "cfg-if 1.0.0", "ctor", @@ -1758,9 +1765,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483ac516dbda6789a5b4be0271e7a31b9ad4ec8c0a5955050e8076f72bdbef8f" +checksum = "a47f2c300ceec3e58064fd5f8f5b61230f2ffd64bde4970c81fdd0563a2db1bb" dependencies = [ "pyo3-macros-backend", "quote", @@ -1769,9 +1776,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15230cabcda008f03565ed8bac40f094cbb5ee1b46e6551f1ec3a0e922cf7df9" +checksum = "87b097e5d84fcbe3e167f400fbedd657820a375b034c78bd852050749a575d66" dependencies = [ "proc-macro2", "quote", @@ -1780,9 +1787,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ "proc-macro2", ] @@ -1815,7 +1822,7 @@ checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" dependencies = [ "libc", "rand_chacha 0.3.0", - "rand_core 0.6.1", + "rand_core 0.6.2", "rand_hc 0.3.0", ] @@ -1836,7 +1843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" dependencies = [ "ppv-lite86", - "rand_core 0.6.1", + "rand_core 0.6.2", ] [[package]] @@ -1850,9 +1857,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" dependencies = [ "getrandom 0.2.2", ] @@ -1872,7 +1879,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" dependencies = [ - "rand_core 0.6.1", + "rand_core 0.6.2", ] [[package]] @@ -1892,9 +1899,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" dependencies = [ "bitflags", ] @@ -1982,7 +1989,7 @@ dependencies = [ "mime_guess", "native-tls", "percent-encoding", - "pin-project-lite 0.1.11", + "pin-project-lite 0.1.12", "rustls", "serde", "serde_json", @@ -2108,9 +2115,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.0.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" +checksum = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d" dependencies = [ "bitflags", "core-foundation", @@ -2121,9 +2128,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" +checksum = "dee48cdde5ed250b0d3252818f646e174ab414036edb884dde62d80a3ac6082d" dependencies = [ "core-foundation-sys", "libc", @@ -2131,9 +2138,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.123" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +checksum = "bd761ff957cb2a45fbb9ab3da6512de9de55872866160b23c25f1a841e99d29f" dependencies = [ "serde_derive", ] @@ -2152,9 +2159,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.123" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" +checksum = "1800f7693e94e186f5e25a28291ae1570da908aff7d97a095dec1e56ff99069b" dependencies = [ "proc-macro2", "quote", @@ -2163,9 +2170,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.62" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ "itoa", "ryu", @@ -2362,6 +2369,27 @@ dependencies = [ "quote", ] +[[package]] +name = "strum" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.4.0" @@ -2370,9 +2398,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" [[package]] name = "syn" -version = "1.0.60" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +checksum = "8fd9bc7ccc2688b3344c2f48b9b546648b25ce0b20fc717ee7fa7981a8ca9717" dependencies = [ "proc-macro2", "quote", @@ -2399,9 +2427,9 @@ checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" [[package]] name = "tap" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" @@ -2412,7 +2440,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "rand 0.8.3", - "redox_syscall 0.2.4", + "redox_syscall 0.2.5", "remove_dir_all", "winapi 0.3.9", ] @@ -2449,18 +2477,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" dependencies = [ "proc-macro2", "quote", @@ -2521,7 +2549,7 @@ dependencies = [ "memchr", "mio", "num_cpus", - "pin-project-lite 0.1.11", + "pin-project-lite 0.1.12", "slab", ] @@ -2579,7 +2607,7 @@ dependencies = [ "futures-core", "futures-sink", "log", - "pin-project-lite 0.1.11", + "pin-project-lite 0.1.12", "tokio", ] @@ -2600,13 +2628,13 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.23" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d40a22fd029e33300d8d89a5cc8ffce18bb7c587662f54629e94c9de5487f3" +checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "tracing-core", ] @@ -2621,11 +2649,11 @@ dependencies = [ [[package]] name = "tracing-futures" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" dependencies = [ - "pin-project 0.4.27", + "pin-project", "tracing", ] @@ -2713,9 +2741,9 @@ dependencies = [ [[package]] name = "unicode-normalization" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" dependencies = [ "tinyvec", ] @@ -2752,9 +2780,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" dependencies = [ "form_urlencoded", "idna", @@ -2814,9 +2842,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.70" +version = "0.2.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be" +checksum = "7ee1280240b7c461d6a0071313e08f34a60b0365f14260362e5a2b17d1d31aa7" dependencies = [ "cfg-if 1.0.0", "serde", @@ -2826,9 +2854,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.70" +version = "0.2.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7" +checksum = "5b7d8b6942b8bb3a9b0e73fc79b98095a27de6fa247615e59d096754a3bc2aa8" dependencies = [ "bumpalo", "lazy_static", @@ -2841,9 +2869,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de431a2910c86679c34283a33f66f4e4abd7e0aec27b6669060148872aadf94" +checksum = "8e67a5806118af01f0d9045915676b22aaebecf4178ae7021bc171dab0b897ab" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -2853,9 +2881,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.70" +version = "0.2.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c" +checksum = "e5ac38da8ef716661f0f36c0d8320b89028efe10c7c0afde65baffb496ce0d3b" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2863,9 +2891,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.70" +version = "0.2.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385" +checksum = "cc053ec74d454df287b9374ee8abb36ffd5acb95ba87da3ba5b7d3fe20eb401e" dependencies = [ "proc-macro2", "quote", @@ -2876,15 +2904,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.70" +version = "0.2.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64" +checksum = "7d6f8ec44822dd71f5f221a5847fb34acd9060535c1211b70a05844c0f6383b1" [[package]] name = "web-sys" -version = "0.3.47" +version = "0.3.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3" +checksum = "ec600b26223b2948cedfde2a0aa6756dcf1fef616f43d7b3097aaf53a6c4d92b" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/cargo/BUILD.reqwest.bazel b/cargo/BUILD.reqwest.bazel index 48bf56b9a..9a5344f2f 100644 --- a/cargo/BUILD.reqwest.bazel +++ b/cargo/BUILD.reqwest.bazel @@ -112,10 +112,10 @@ rust_library( "@raze__http__0_2_3//:http", "@raze__hyper_timeout__0_3_1//:hyper_timeout", "@raze__mime_guess__2_0_3//:mime_guess", - "@raze__serde__1_0_123//:serde", - "@raze__serde_json__1_0_62//:serde_json", + "@raze__serde__1_0_124//:serde", + "@raze__serde_json__1_0_64//:serde_json", "@raze__serde_urlencoded__0_6_1//:serde_urlencoded", - "@raze__url__2_2_0//:url", + "@raze__url__2_2_1//:url", ] + selects.with_or({ # cfg(not(target_arch = "wasm32")) ( @@ -128,8 +128,8 @@ rust_library( ): [ "@raze__base64__0_13_0//:base64", "@raze__encoding_rs__0_8_28//:encoding_rs", - "@raze__futures_core__0_3_12//:futures_core", - "@raze__futures_util__0_3_12//:futures_util", + "@raze__futures_core__0_3_13//:futures_core", + "@raze__futures_util__0_3_13//:futures_util", "@raze__http_body__0_3_1//:http_body", "@raze__hyper__0_13_10//:hyper", "@raze__ipnet__2_3_0//:ipnet", @@ -137,7 +137,7 @@ rust_library( "@raze__log__0_4_14//:log", "@raze__mime__0_3_16//:mime", "@raze__percent_encoding__2_1_0//:percent_encoding", - "@raze__pin_project_lite__0_1_11//:pin_project_lite", + "@raze__pin_project_lite__0_1_12//:pin_project_lite", "@raze__tokio__0_2_25//:tokio", "@raze__tokio_socks__0_3_0//:tokio_socks", ], diff --git a/cargo/crates.bzl b/cargo/crates.bzl index 7f60f12a8..7a302d058 100644 --- a/cargo/crates.bzl +++ b/cargo/crates.bzl @@ -23,12 +23,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__adler__0_2_3", - url = "https://crates.io/api/v1/crates/adler/0.2.3/download", + name = "raze__adler__1_0_2", + url = "https://crates.io/api/v1/crates/adler/1.0.2/download", type = "tar.gz", - sha256 = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e", - strip_prefix = "adler-0.2.3", - build_file = Label("//cargo/remote:BUILD.adler-0.2.3.bazel"), + sha256 = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe", + strip_prefix = "adler-1.0.2", + build_file = Label("//cargo/remote:BUILD.adler-1.0.2.bazel"), ) maybe( @@ -53,12 +53,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__ammonia__3_1_0", - url = "https://crates.io/api/v1/crates/ammonia/3.1.0/download", + name = "raze__ammonia__3_1_1", + url = "https://crates.io/api/v1/crates/ammonia/3.1.1/download", type = "tar.gz", - sha256 = "89eac85170f4b3fb3dc5e442c1cfb036cb8eecf9dbbd431a161ffad15d90ea3b", - strip_prefix = "ammonia-3.1.0", - build_file = Label("//cargo/remote:BUILD.ammonia-3.1.0.bazel"), + sha256 = "1ee7d6eb157f337c5cedc95ddf17f0cbc36d36eb7763c8e0d1c1aeb3722f6279", + strip_prefix = "ammonia-3.1.1", + build_file = Label("//cargo/remote:BUILD.ammonia-3.1.1.bazel"), ) maybe( @@ -163,12 +163,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__async_trait__0_1_42", - url = "https://crates.io/api/v1/crates/async-trait/0.1.42/download", + name = "raze__async_trait__0_1_48", + url = "https://crates.io/api/v1/crates/async-trait/0.1.48/download", type = "tar.gz", - sha256 = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d", - strip_prefix = "async-trait-0.1.42", - build_file = Label("//cargo/remote:BUILD.async-trait-0.1.42.bazel"), + sha256 = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf", + strip_prefix = "async-trait-0.1.48", + build_file = Label("//cargo/remote:BUILD.async-trait-0.1.48.bazel"), ) maybe( @@ -233,12 +233,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__bitvec__0_19_4", - url = "https://crates.io/api/v1/crates/bitvec/0.19.4/download", + name = "raze__bitvec__0_19_5", + url = "https://crates.io/api/v1/crates/bitvec/0.19.5/download", type = "tar.gz", - sha256 = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81", - strip_prefix = "bitvec-0.19.4", - build_file = Label("//cargo/remote:BUILD.bitvec-0.19.4.bazel"), + sha256 = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321", + strip_prefix = "bitvec-0.19.5", + build_file = Label("//cargo/remote:BUILD.bitvec-0.19.5.bazel"), ) maybe( @@ -263,12 +263,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__bumpalo__3_6_0", - url = "https://crates.io/api/v1/crates/bumpalo/3.6.0/download", + name = "raze__bumpalo__3_6_1", + url = "https://crates.io/api/v1/crates/bumpalo/3.6.1/download", type = "tar.gz", - sha256 = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9", - strip_prefix = "bumpalo-3.6.0", - build_file = Label("//cargo/remote:BUILD.bumpalo-3.6.0.bazel"), + sha256 = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe", + strip_prefix = "bumpalo-3.6.1", + build_file = Label("//cargo/remote:BUILD.bumpalo-3.6.1.bazel"), ) maybe( @@ -313,12 +313,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__cc__1_0_66", - url = "https://crates.io/api/v1/crates/cc/1.0.66/download", + name = "raze__cc__1_0_67", + url = "https://crates.io/api/v1/crates/cc/1.0.67/download", type = "tar.gz", - sha256 = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48", - strip_prefix = "cc-1.0.66", - build_file = Label("//cargo/remote:BUILD.cc-1.0.66.bazel"), + sha256 = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd", + strip_prefix = "cc-1.0.67", + build_file = Label("//cargo/remote:BUILD.cc-1.0.67.bazel"), ) maybe( @@ -413,12 +413,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__crossbeam_utils__0_8_1", - url = "https://crates.io/api/v1/crates/crossbeam-utils/0.8.1/download", + name = "raze__crossbeam_utils__0_8_3", + url = "https://crates.io/api/v1/crates/crossbeam-utils/0.8.3/download", type = "tar.gz", - sha256 = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d", - strip_prefix = "crossbeam-utils-0.8.1", - build_file = Label("//cargo/remote:BUILD.crossbeam-utils-0.8.1.bazel"), + sha256 = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49", + strip_prefix = "crossbeam-utils-0.8.3", + build_file = Label("//cargo/remote:BUILD.crossbeam-utils-0.8.3.bazel"), ) maybe( @@ -513,12 +513,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__env_logger__0_8_2", - url = "https://crates.io/api/v1/crates/env_logger/0.8.2/download", + name = "raze__env_logger__0_8_3", + url = "https://crates.io/api/v1/crates/env_logger/0.8.3/download", type = "tar.gz", - sha256 = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e", - strip_prefix = "env_logger-0.8.2", - build_file = Label("//cargo/remote:BUILD.env_logger-0.8.2.bazel"), + sha256 = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f", + strip_prefix = "env_logger-0.8.3", + build_file = Label("//cargo/remote:BUILD.env_logger-0.8.3.bazel"), ) maybe( @@ -653,12 +653,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__form_urlencoded__1_0_0", - url = "https://crates.io/api/v1/crates/form_urlencoded/1.0.0/download", + name = "raze__form_urlencoded__1_0_1", + url = "https://crates.io/api/v1/crates/form_urlencoded/1.0.1/download", type = "tar.gz", - sha256 = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00", - strip_prefix = "form_urlencoded-1.0.0", - build_file = Label("//cargo/remote:BUILD.form_urlencoded-1.0.0.bazel"), + sha256 = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191", + strip_prefix = "form_urlencoded-1.0.1", + build_file = Label("//cargo/remote:BUILD.form_urlencoded-1.0.1.bazel"), ) maybe( @@ -703,92 +703,92 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__futures__0_3_12", - url = "https://crates.io/api/v1/crates/futures/0.3.12/download", + name = "raze__futures__0_3_13", + url = "https://crates.io/api/v1/crates/futures/0.3.13/download", type = "tar.gz", - sha256 = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150", - strip_prefix = "futures-0.3.12", - build_file = Label("//cargo/remote:BUILD.futures-0.3.12.bazel"), + sha256 = "7f55667319111d593ba876406af7c409c0ebb44dc4be6132a783ccf163ea14c1", + strip_prefix = "futures-0.3.13", + build_file = Label("//cargo/remote:BUILD.futures-0.3.13.bazel"), ) maybe( http_archive, - name = "raze__futures_channel__0_3_12", - url = "https://crates.io/api/v1/crates/futures-channel/0.3.12/download", + name = "raze__futures_channel__0_3_13", + url = "https://crates.io/api/v1/crates/futures-channel/0.3.13/download", type = "tar.gz", - sha256 = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846", - strip_prefix = "futures-channel-0.3.12", - build_file = Label("//cargo/remote:BUILD.futures-channel-0.3.12.bazel"), + sha256 = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939", + strip_prefix = "futures-channel-0.3.13", + build_file = Label("//cargo/remote:BUILD.futures-channel-0.3.13.bazel"), ) maybe( http_archive, - name = "raze__futures_core__0_3_12", - url = "https://crates.io/api/v1/crates/futures-core/0.3.12/download", + name = "raze__futures_core__0_3_13", + url = "https://crates.io/api/v1/crates/futures-core/0.3.13/download", type = "tar.gz", - sha256 = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65", - strip_prefix = "futures-core-0.3.12", - build_file = Label("//cargo/remote:BUILD.futures-core-0.3.12.bazel"), + sha256 = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94", + strip_prefix = "futures-core-0.3.13", + build_file = Label("//cargo/remote:BUILD.futures-core-0.3.13.bazel"), ) maybe( http_archive, - name = "raze__futures_executor__0_3_12", - url = "https://crates.io/api/v1/crates/futures-executor/0.3.12/download", + name = "raze__futures_executor__0_3_13", + url = "https://crates.io/api/v1/crates/futures-executor/0.3.13/download", type = "tar.gz", - sha256 = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9", - strip_prefix = "futures-executor-0.3.12", - build_file = Label("//cargo/remote:BUILD.futures-executor-0.3.12.bazel"), + sha256 = "891a4b7b96d84d5940084b2a37632dd65deeae662c114ceaa2c879629c9c0ad1", + strip_prefix = "futures-executor-0.3.13", + build_file = Label("//cargo/remote:BUILD.futures-executor-0.3.13.bazel"), ) maybe( http_archive, - name = "raze__futures_io__0_3_12", - url = "https://crates.io/api/v1/crates/futures-io/0.3.12/download", + name = "raze__futures_io__0_3_13", + url = "https://crates.io/api/v1/crates/futures-io/0.3.13/download", type = "tar.gz", - sha256 = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500", - strip_prefix = "futures-io-0.3.12", - build_file = Label("//cargo/remote:BUILD.futures-io-0.3.12.bazel"), + sha256 = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59", + strip_prefix = "futures-io-0.3.13", + build_file = Label("//cargo/remote:BUILD.futures-io-0.3.13.bazel"), ) maybe( http_archive, - name = "raze__futures_macro__0_3_12", - url = "https://crates.io/api/v1/crates/futures-macro/0.3.12/download", + name = "raze__futures_macro__0_3_13", + url = "https://crates.io/api/v1/crates/futures-macro/0.3.13/download", type = "tar.gz", - sha256 = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd", - strip_prefix = "futures-macro-0.3.12", - build_file = Label("//cargo/remote:BUILD.futures-macro-0.3.12.bazel"), + sha256 = "ea405816a5139fb39af82c2beb921d52143f556038378d6db21183a5c37fbfb7", + strip_prefix = "futures-macro-0.3.13", + build_file = Label("//cargo/remote:BUILD.futures-macro-0.3.13.bazel"), ) maybe( http_archive, - name = "raze__futures_sink__0_3_12", - url = "https://crates.io/api/v1/crates/futures-sink/0.3.12/download", + name = "raze__futures_sink__0_3_13", + url = "https://crates.io/api/v1/crates/futures-sink/0.3.13/download", type = "tar.gz", - sha256 = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6", - strip_prefix = "futures-sink-0.3.12", - build_file = Label("//cargo/remote:BUILD.futures-sink-0.3.12.bazel"), + sha256 = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3", + strip_prefix = "futures-sink-0.3.13", + build_file = Label("//cargo/remote:BUILD.futures-sink-0.3.13.bazel"), ) maybe( http_archive, - name = "raze__futures_task__0_3_12", - url = "https://crates.io/api/v1/crates/futures-task/0.3.12/download", + name = "raze__futures_task__0_3_13", + url = "https://crates.io/api/v1/crates/futures-task/0.3.13/download", type = "tar.gz", - sha256 = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86", - strip_prefix = "futures-task-0.3.12", - build_file = Label("//cargo/remote:BUILD.futures-task-0.3.12.bazel"), + sha256 = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80", + strip_prefix = "futures-task-0.3.13", + build_file = Label("//cargo/remote:BUILD.futures-task-0.3.13.bazel"), ) maybe( http_archive, - name = "raze__futures_util__0_3_12", - url = "https://crates.io/api/v1/crates/futures-util/0.3.12/download", + name = "raze__futures_util__0_3_13", + url = "https://crates.io/api/v1/crates/futures-util/0.3.13/download", type = "tar.gz", - sha256 = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b", - strip_prefix = "futures-util-0.3.12", - build_file = Label("//cargo/remote:BUILD.futures-util-0.3.12.bazel"), + sha256 = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1", + strip_prefix = "futures-util-0.3.13", + build_file = Label("//cargo/remote:BUILD.futures-util-0.3.13.bazel"), ) maybe( @@ -903,12 +903,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__hex__0_4_2", - url = "https://crates.io/api/v1/crates/hex/0.4.2/download", + name = "raze__hex__0_4_3", + url = "https://crates.io/api/v1/crates/hex/0.4.3/download", type = "tar.gz", - sha256 = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35", - strip_prefix = "hex-0.4.2", - build_file = Label("//cargo/remote:BUILD.hex-0.4.2.bazel"), + sha256 = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70", + strip_prefix = "hex-0.4.3", + build_file = Label("//cargo/remote:BUILD.hex-0.4.3.bazel"), ) maybe( @@ -1033,32 +1033,42 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__idna__0_2_1", - url = "https://crates.io/api/v1/crates/idna/0.2.1/download", + name = "raze__idna__0_2_2", + url = "https://crates.io/api/v1/crates/idna/0.2.2/download", type = "tar.gz", - sha256 = "de910d521f7cc3135c4de8db1cb910e0b5ed1dc6f57c381cd07e8e661ce10094", - strip_prefix = "idna-0.2.1", - build_file = Label("//cargo/remote:BUILD.idna-0.2.1.bazel"), + sha256 = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21", + strip_prefix = "idna-0.2.2", + build_file = Label("//cargo/remote:BUILD.idna-0.2.2.bazel"), ) maybe( http_archive, - name = "raze__indexmap__1_6_1", - url = "https://crates.io/api/v1/crates/indexmap/1.6.1/download", + name = "raze__indexmap__1_6_2", + url = "https://crates.io/api/v1/crates/indexmap/1.6.2/download", type = "tar.gz", - sha256 = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b", - strip_prefix = "indexmap-1.6.1", - build_file = Label("//cargo/remote:BUILD.indexmap-1.6.1.bazel"), + sha256 = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3", + strip_prefix = "indexmap-1.6.2", + build_file = Label("//cargo/remote:BUILD.indexmap-1.6.2.bazel"), ) maybe( http_archive, - name = "raze__indoc__1_0_3", - url = "https://crates.io/api/v1/crates/indoc/1.0.3/download", + name = "raze__indoc__0_3_6", + url = "https://crates.io/api/v1/crates/indoc/0.3.6/download", type = "tar.gz", - sha256 = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136", - strip_prefix = "indoc-1.0.3", - build_file = Label("//cargo/remote:BUILD.indoc-1.0.3.bazel"), + sha256 = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8", + strip_prefix = "indoc-0.3.6", + build_file = Label("//cargo/remote:BUILD.indoc-0.3.6.bazel"), + ) + + maybe( + http_archive, + name = "raze__indoc_impl__0_3_6", + url = "https://crates.io/api/v1/crates/indoc-impl/0.3.6/download", + type = "tar.gz", + sha256 = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0", + strip_prefix = "indoc-impl-0.3.6", + build_file = Label("//cargo/remote:BUILD.indoc-impl-0.3.6.bazel"), ) maybe( @@ -1153,12 +1163,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__js_sys__0_3_47", - url = "https://crates.io/api/v1/crates/js-sys/0.3.47/download", + name = "raze__js_sys__0_3_48", + url = "https://crates.io/api/v1/crates/js-sys/0.3.48/download", type = "tar.gz", - sha256 = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65", - strip_prefix = "js-sys-0.3.47", - build_file = Label("//cargo/remote:BUILD.js-sys-0.3.47.bazel"), + sha256 = "dc9f84f9b115ce7843d60706df1422a916680bfdfcbdb0447c5614ff9d7e4d78", + strip_prefix = "js-sys-0.3.48", + build_file = Label("//cargo/remote:BUILD.js-sys-0.3.48.bazel"), ) maybe( @@ -1183,22 +1193,22 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__lexical_core__0_7_4", - url = "https://crates.io/api/v1/crates/lexical-core/0.7.4/download", + name = "raze__lexical_core__0_7_5", + url = "https://crates.io/api/v1/crates/lexical-core/0.7.5/download", type = "tar.gz", - sha256 = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616", - strip_prefix = "lexical-core-0.7.4", - build_file = Label("//cargo/remote:BUILD.lexical-core-0.7.4.bazel"), + sha256 = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374", + strip_prefix = "lexical-core-0.7.5", + build_file = Label("//cargo/remote:BUILD.lexical-core-0.7.5.bazel"), ) maybe( http_archive, - name = "raze__libc__0_2_85", - url = "https://crates.io/api/v1/crates/libc/0.2.85/download", + name = "raze__libc__0_2_88", + url = "https://crates.io/api/v1/crates/libc/0.2.88/download", type = "tar.gz", - sha256 = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3", - strip_prefix = "libc-0.2.85", - build_file = Label("//cargo/remote:BUILD.libc-0.2.85.bazel"), + sha256 = "03b07a082330a35e43f63177cc01689da34fbffa0105e1246cf0311472cac73a", + strip_prefix = "libc-0.2.88", + build_file = Label("//cargo/remote:BUILD.libc-0.2.88.bazel"), ) maybe( @@ -1313,12 +1323,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__miniz_oxide__0_4_3", - url = "https://crates.io/api/v1/crates/miniz_oxide/0.4.3/download", + name = "raze__miniz_oxide__0_4_4", + url = "https://crates.io/api/v1/crates/miniz_oxide/0.4.4/download", type = "tar.gz", - sha256 = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d", - strip_prefix = "miniz_oxide-0.4.3", - build_file = Label("//cargo/remote:BUILD.miniz_oxide-0.4.3.bazel"), + sha256 = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b", + strip_prefix = "miniz_oxide-0.4.4", + build_file = Label("//cargo/remote:BUILD.miniz_oxide-0.4.4.bazel"), ) maybe( @@ -1393,12 +1403,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__nom__6_1_0", - url = "https://crates.io/api/v1/crates/nom/6.1.0/download", + name = "raze__nom__6_1_2", + url = "https://crates.io/api/v1/crates/nom/6.1.2/download", type = "tar.gz", - sha256 = "ab6f70b46d6325aa300f1c7bb3d470127dfc27806d8ea6bf294ee0ce643ce2b1", - strip_prefix = "nom-6.1.0", - build_file = Label("//cargo/remote:BUILD.nom-6.1.0.bazel"), + sha256 = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2", + strip_prefix = "nom-6.1.2", + build_file = Label("//cargo/remote:BUILD.nom-6.1.2.bazel"), ) maybe( @@ -1473,12 +1483,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__once_cell__1_5_2", - url = "https://crates.io/api/v1/crates/once_cell/1.5.2/download", + name = "raze__once_cell__1_7_2", + url = "https://crates.io/api/v1/crates/once_cell/1.7.2/download", type = "tar.gz", - sha256 = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0", - strip_prefix = "once_cell-1.5.2", - build_file = Label("//cargo/remote:BUILD.once_cell-1.5.2.bazel"), + sha256 = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3", + strip_prefix = "once_cell-1.7.2", + build_file = Label("//cargo/remote:BUILD.once_cell-1.7.2.bazel"), ) maybe( @@ -1523,22 +1533,32 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__parking_lot_core__0_8_2", - url = "https://crates.io/api/v1/crates/parking_lot_core/0.8.2/download", + name = "raze__parking_lot_core__0_8_3", + url = "https://crates.io/api/v1/crates/parking_lot_core/0.8.3/download", type = "tar.gz", - sha256 = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272", - strip_prefix = "parking_lot_core-0.8.2", - build_file = Label("//cargo/remote:BUILD.parking_lot_core-0.8.2.bazel"), + sha256 = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018", + strip_prefix = "parking_lot_core-0.8.3", + build_file = Label("//cargo/remote:BUILD.parking_lot_core-0.8.3.bazel"), ) maybe( http_archive, - name = "raze__paste__1_0_4", - url = "https://crates.io/api/v1/crates/paste/1.0.4/download", + name = "raze__paste__0_1_18", + url = "https://crates.io/api/v1/crates/paste/0.1.18/download", type = "tar.gz", - sha256 = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1", - strip_prefix = "paste-1.0.4", - build_file = Label("//cargo/remote:BUILD.paste-1.0.4.bazel"), + sha256 = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880", + strip_prefix = "paste-0.1.18", + build_file = Label("//cargo/remote:BUILD.paste-0.1.18.bazel"), + ) + + maybe( + http_archive, + name = "raze__paste_impl__0_1_18", + url = "https://crates.io/api/v1/crates/paste-impl/0.1.18/download", + type = "tar.gz", + sha256 = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6", + strip_prefix = "paste-impl-0.1.18", + build_file = Label("//cargo/remote:BUILD.paste-impl-0.1.18.bazel"), ) maybe( @@ -1601,16 +1621,6 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.phf_shared-0.8.0.bazel"), ) - maybe( - http_archive, - name = "raze__pin_project__0_4_27", - url = "https://crates.io/api/v1/crates/pin-project/0.4.27/download", - type = "tar.gz", - sha256 = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15", - strip_prefix = "pin-project-0.4.27", - build_file = Label("//cargo/remote:BUILD.pin-project-0.4.27.bazel"), - ) - maybe( http_archive, name = "raze__pin_project__1_0_5", @@ -1621,16 +1631,6 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.pin-project-1.0.5.bazel"), ) - maybe( - http_archive, - name = "raze__pin_project_internal__0_4_27", - url = "https://crates.io/api/v1/crates/pin-project-internal/0.4.27/download", - type = "tar.gz", - sha256 = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895", - strip_prefix = "pin-project-internal-0.4.27", - build_file = Label("//cargo/remote:BUILD.pin-project-internal-0.4.27.bazel"), - ) - maybe( http_archive, name = "raze__pin_project_internal__1_0_5", @@ -1643,22 +1643,22 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__pin_project_lite__0_1_11", - url = "https://crates.io/api/v1/crates/pin-project-lite/0.1.11/download", + name = "raze__pin_project_lite__0_1_12", + url = "https://crates.io/api/v1/crates/pin-project-lite/0.1.12/download", type = "tar.gz", - sha256 = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b", - strip_prefix = "pin-project-lite-0.1.11", - build_file = Label("//cargo/remote:BUILD.pin-project-lite-0.1.11.bazel"), + sha256 = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777", + strip_prefix = "pin-project-lite-0.1.12", + build_file = Label("//cargo/remote:BUILD.pin-project-lite-0.1.12.bazel"), ) maybe( http_archive, - name = "raze__pin_project_lite__0_2_4", - url = "https://crates.io/api/v1/crates/pin-project-lite/0.2.4/download", + name = "raze__pin_project_lite__0_2_6", + url = "https://crates.io/api/v1/crates/pin-project-lite/0.2.6/download", type = "tar.gz", - sha256 = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827", - strip_prefix = "pin-project-lite-0.2.4", - build_file = Label("//cargo/remote:BUILD.pin-project-lite-0.2.4.bazel"), + sha256 = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905", + strip_prefix = "pin-project-lite-0.2.6", + build_file = Label("//cargo/remote:BUILD.pin-project-lite-0.2.6.bazel"), ) maybe( @@ -1803,42 +1803,42 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__pyo3__0_13_1", - url = "https://crates.io/api/v1/crates/pyo3/0.13.1/download", + name = "raze__pyo3__0_13_2", + url = "https://crates.io/api/v1/crates/pyo3/0.13.2/download", type = "tar.gz", - sha256 = "00ca634cf3acd58a599b535ed6cb188223298977d471d146121792bfa23b754c", - strip_prefix = "pyo3-0.13.1", - build_file = Label("//cargo/remote:BUILD.pyo3-0.13.1.bazel"), + sha256 = "4837b8e8e18a102c23f79d1e9a110b597ea3b684c95e874eb1ad88f8683109c3", + strip_prefix = "pyo3-0.13.2", + build_file = Label("//cargo/remote:BUILD.pyo3-0.13.2.bazel"), ) maybe( http_archive, - name = "raze__pyo3_macros__0_13_1", - url = "https://crates.io/api/v1/crates/pyo3-macros/0.13.1/download", + name = "raze__pyo3_macros__0_13_2", + url = "https://crates.io/api/v1/crates/pyo3-macros/0.13.2/download", type = "tar.gz", - sha256 = "483ac516dbda6789a5b4be0271e7a31b9ad4ec8c0a5955050e8076f72bdbef8f", - strip_prefix = "pyo3-macros-0.13.1", - build_file = Label("//cargo/remote:BUILD.pyo3-macros-0.13.1.bazel"), + sha256 = "a47f2c300ceec3e58064fd5f8f5b61230f2ffd64bde4970c81fdd0563a2db1bb", + strip_prefix = "pyo3-macros-0.13.2", + build_file = Label("//cargo/remote:BUILD.pyo3-macros-0.13.2.bazel"), ) maybe( http_archive, - name = "raze__pyo3_macros_backend__0_13_1", - url = "https://crates.io/api/v1/crates/pyo3-macros-backend/0.13.1/download", + name = "raze__pyo3_macros_backend__0_13_2", + url = "https://crates.io/api/v1/crates/pyo3-macros-backend/0.13.2/download", type = "tar.gz", - sha256 = "15230cabcda008f03565ed8bac40f094cbb5ee1b46e6551f1ec3a0e922cf7df9", - strip_prefix = "pyo3-macros-backend-0.13.1", - build_file = Label("//cargo/remote:BUILD.pyo3-macros-backend-0.13.1.bazel"), + sha256 = "87b097e5d84fcbe3e167f400fbedd657820a375b034c78bd852050749a575d66", + strip_prefix = "pyo3-macros-backend-0.13.2", + build_file = Label("//cargo/remote:BUILD.pyo3-macros-backend-0.13.2.bazel"), ) maybe( http_archive, - name = "raze__quote__1_0_8", - url = "https://crates.io/api/v1/crates/quote/1.0.8/download", + name = "raze__quote__1_0_9", + url = "https://crates.io/api/v1/crates/quote/1.0.9/download", type = "tar.gz", - sha256 = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df", - strip_prefix = "quote-1.0.8", - build_file = Label("//cargo/remote:BUILD.quote-1.0.8.bazel"), + sha256 = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7", + strip_prefix = "quote-1.0.9", + build_file = Label("//cargo/remote:BUILD.quote-1.0.9.bazel"), ) maybe( @@ -1903,12 +1903,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__rand_core__0_6_1", - url = "https://crates.io/api/v1/crates/rand_core/0.6.1/download", + name = "raze__rand_core__0_6_2", + url = "https://crates.io/api/v1/crates/rand_core/0.6.2/download", type = "tar.gz", - sha256 = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5", - strip_prefix = "rand_core-0.6.1", - build_file = Label("//cargo/remote:BUILD.rand_core-0.6.1.bazel"), + sha256 = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7", + strip_prefix = "rand_core-0.6.2", + build_file = Label("//cargo/remote:BUILD.rand_core-0.6.2.bazel"), ) maybe( @@ -1953,12 +1953,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__redox_syscall__0_2_4", - url = "https://crates.io/api/v1/crates/redox_syscall/0.2.4/download", + name = "raze__redox_syscall__0_2_5", + url = "https://crates.io/api/v1/crates/redox_syscall/0.2.5/download", type = "tar.gz", - sha256 = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570", - strip_prefix = "redox_syscall-0.2.4", - build_file = Label("//cargo/remote:BUILD.redox_syscall-0.2.4.bazel"), + sha256 = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9", + strip_prefix = "redox_syscall-0.2.5", + build_file = Label("//cargo/remote:BUILD.redox_syscall-0.2.5.bazel"), ) maybe( @@ -2133,32 +2133,32 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__security_framework__2_0_0", - url = "https://crates.io/api/v1/crates/security-framework/2.0.0/download", + name = "raze__security_framework__2_1_2", + url = "https://crates.io/api/v1/crates/security-framework/2.1.2/download", type = "tar.gz", - sha256 = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69", - strip_prefix = "security-framework-2.0.0", - build_file = Label("//cargo/remote:BUILD.security-framework-2.0.0.bazel"), + sha256 = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d", + strip_prefix = "security-framework-2.1.2", + build_file = Label("//cargo/remote:BUILD.security-framework-2.1.2.bazel"), ) maybe( http_archive, - name = "raze__security_framework_sys__2_0_0", - url = "https://crates.io/api/v1/crates/security-framework-sys/2.0.0/download", + name = "raze__security_framework_sys__2_1_1", + url = "https://crates.io/api/v1/crates/security-framework-sys/2.1.1/download", type = "tar.gz", - sha256 = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b", - strip_prefix = "security-framework-sys-2.0.0", - build_file = Label("//cargo/remote:BUILD.security-framework-sys-2.0.0.bazel"), + sha256 = "dee48cdde5ed250b0d3252818f646e174ab414036edb884dde62d80a3ac6082d", + strip_prefix = "security-framework-sys-2.1.1", + build_file = Label("//cargo/remote:BUILD.security-framework-sys-2.1.1.bazel"), ) maybe( http_archive, - name = "raze__serde__1_0_123", - url = "https://crates.io/api/v1/crates/serde/1.0.123/download", + name = "raze__serde__1_0_124", + url = "https://crates.io/api/v1/crates/serde/1.0.124/download", type = "tar.gz", - sha256 = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae", - strip_prefix = "serde-1.0.123", - build_file = Label("//cargo/remote:BUILD.serde-1.0.123.bazel"), + sha256 = "bd761ff957cb2a45fbb9ab3da6512de9de55872866160b23c25f1a841e99d29f", + strip_prefix = "serde-1.0.124", + build_file = Label("//cargo/remote:BUILD.serde-1.0.124.bazel"), ) maybe( @@ -2173,22 +2173,22 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__serde_derive__1_0_123", - url = "https://crates.io/api/v1/crates/serde_derive/1.0.123/download", + name = "raze__serde_derive__1_0_124", + url = "https://crates.io/api/v1/crates/serde_derive/1.0.124/download", type = "tar.gz", - sha256 = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31", - strip_prefix = "serde_derive-1.0.123", - build_file = Label("//cargo/remote:BUILD.serde_derive-1.0.123.bazel"), + sha256 = "1800f7693e94e186f5e25a28291ae1570da908aff7d97a095dec1e56ff99069b", + strip_prefix = "serde_derive-1.0.124", + build_file = Label("//cargo/remote:BUILD.serde_derive-1.0.124.bazel"), ) maybe( http_archive, - name = "raze__serde_json__1_0_62", - url = "https://crates.io/api/v1/crates/serde_json/1.0.62/download", + name = "raze__serde_json__1_0_64", + url = "https://crates.io/api/v1/crates/serde_json/1.0.64/download", type = "tar.gz", - sha256 = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486", - strip_prefix = "serde_json-1.0.62", - build_file = Label("//cargo/remote:BUILD.serde_json-1.0.62.bazel"), + sha256 = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79", + strip_prefix = "serde_json-1.0.64", + build_file = Label("//cargo/remote:BUILD.serde_json-1.0.64.bazel"), ) maybe( @@ -2391,6 +2391,26 @@ def raze_fetch_remote_crates(): build_file = Label("//cargo/remote:BUILD.string_cache_codegen-0.5.1.bazel"), ) + maybe( + http_archive, + name = "raze__strum__0_20_0", + url = "https://crates.io/api/v1/crates/strum/0.20.0/download", + type = "tar.gz", + sha256 = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c", + strip_prefix = "strum-0.20.0", + build_file = Label("//cargo/remote:BUILD.strum-0.20.0.bazel"), + ) + + maybe( + http_archive, + name = "raze__strum_macros__0_20_1", + url = "https://crates.io/api/v1/crates/strum_macros/0.20.1/download", + type = "tar.gz", + sha256 = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149", + strip_prefix = "strum_macros-0.20.1", + build_file = Label("//cargo/remote:BUILD.strum_macros-0.20.1.bazel"), + ) + maybe( http_archive, name = "raze__subtle__2_4_0", @@ -2403,12 +2423,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__syn__1_0_60", - url = "https://crates.io/api/v1/crates/syn/1.0.60/download", + name = "raze__syn__1_0_63", + url = "https://crates.io/api/v1/crates/syn/1.0.63/download", type = "tar.gz", - sha256 = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081", - strip_prefix = "syn-1.0.60", - build_file = Label("//cargo/remote:BUILD.syn-1.0.60.bazel"), + sha256 = "8fd9bc7ccc2688b3344c2f48b9b546648b25ce0b20fc717ee7fa7981a8ca9717", + strip_prefix = "syn-1.0.63", + build_file = Label("//cargo/remote:BUILD.syn-1.0.63.bazel"), ) maybe( @@ -2433,12 +2453,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__tap__1_0_0", - url = "https://crates.io/api/v1/crates/tap/1.0.0/download", + name = "raze__tap__1_0_1", + url = "https://crates.io/api/v1/crates/tap/1.0.1/download", type = "tar.gz", - sha256 = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e", - strip_prefix = "tap-1.0.0", - build_file = Label("//cargo/remote:BUILD.tap-1.0.0.bazel"), + sha256 = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369", + strip_prefix = "tap-1.0.1", + build_file = Label("//cargo/remote:BUILD.tap-1.0.1.bazel"), ) maybe( @@ -2483,22 +2503,22 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__thiserror__1_0_23", - url = "https://crates.io/api/v1/crates/thiserror/1.0.23/download", + name = "raze__thiserror__1_0_24", + url = "https://crates.io/api/v1/crates/thiserror/1.0.24/download", type = "tar.gz", - sha256 = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146", - strip_prefix = "thiserror-1.0.23", - build_file = Label("//cargo/remote:BUILD.thiserror-1.0.23.bazel"), + sha256 = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e", + strip_prefix = "thiserror-1.0.24", + build_file = Label("//cargo/remote:BUILD.thiserror-1.0.24.bazel"), ) maybe( http_archive, - name = "raze__thiserror_impl__1_0_23", - url = "https://crates.io/api/v1/crates/thiserror-impl/1.0.23/download", + name = "raze__thiserror_impl__1_0_24", + url = "https://crates.io/api/v1/crates/thiserror-impl/1.0.24/download", type = "tar.gz", - sha256 = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1", - strip_prefix = "thiserror-impl-1.0.23", - build_file = Label("//cargo/remote:BUILD.thiserror-impl-1.0.23.bazel"), + sha256 = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0", + strip_prefix = "thiserror-impl-1.0.24", + build_file = Label("//cargo/remote:BUILD.thiserror-impl-1.0.24.bazel"), ) maybe( @@ -2633,12 +2653,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__tracing__0_1_23", - url = "https://crates.io/api/v1/crates/tracing/0.1.23/download", + name = "raze__tracing__0_1_25", + url = "https://crates.io/api/v1/crates/tracing/0.1.25/download", type = "tar.gz", - sha256 = "f7d40a22fd029e33300d8d89a5cc8ffce18bb7c587662f54629e94c9de5487f3", - strip_prefix = "tracing-0.1.23", - build_file = Label("//cargo/remote:BUILD.tracing-0.1.23.bazel"), + sha256 = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f", + strip_prefix = "tracing-0.1.25", + build_file = Label("//cargo/remote:BUILD.tracing-0.1.25.bazel"), ) maybe( @@ -2653,12 +2673,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__tracing_futures__0_2_4", - url = "https://crates.io/api/v1/crates/tracing-futures/0.2.4/download", + name = "raze__tracing_futures__0_2_5", + url = "https://crates.io/api/v1/crates/tracing-futures/0.2.5/download", type = "tar.gz", - sha256 = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c", - strip_prefix = "tracing-futures-0.2.4", - build_file = Label("//cargo/remote:BUILD.tracing-futures-0.2.4.bazel"), + sha256 = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2", + strip_prefix = "tracing-futures-0.2.5", + build_file = Label("//cargo/remote:BUILD.tracing-futures-0.2.5.bazel"), ) maybe( @@ -2753,12 +2773,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__unicode_normalization__0_1_16", - url = "https://crates.io/api/v1/crates/unicode-normalization/0.1.16/download", + name = "raze__unicode_normalization__0_1_17", + url = "https://crates.io/api/v1/crates/unicode-normalization/0.1.17/download", type = "tar.gz", - sha256 = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606", - strip_prefix = "unicode-normalization-0.1.16", - build_file = Label("//cargo/remote:BUILD.unicode-normalization-0.1.16.bazel"), + sha256 = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef", + strip_prefix = "unicode-normalization-0.1.17", + build_file = Label("//cargo/remote:BUILD.unicode-normalization-0.1.17.bazel"), ) maybe( @@ -2813,12 +2833,12 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__url__2_2_0", - url = "https://crates.io/api/v1/crates/url/2.2.0/download", + name = "raze__url__2_2_1", + url = "https://crates.io/api/v1/crates/url/2.2.1/download", type = "tar.gz", - sha256 = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e", - strip_prefix = "url-2.2.0", - build_file = Label("//cargo/remote:BUILD.url-2.2.0.bazel"), + sha256 = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b", + strip_prefix = "url-2.2.1", + build_file = Label("//cargo/remote:BUILD.url-2.2.1.bazel"), ) maybe( @@ -2893,72 +2913,72 @@ def raze_fetch_remote_crates(): maybe( http_archive, - name = "raze__wasm_bindgen__0_2_70", - url = "https://crates.io/api/v1/crates/wasm-bindgen/0.2.70/download", + name = "raze__wasm_bindgen__0_2_71", + url = "https://crates.io/api/v1/crates/wasm-bindgen/0.2.71/download", type = "tar.gz", - sha256 = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be", - strip_prefix = "wasm-bindgen-0.2.70", - build_file = Label("//cargo/remote:BUILD.wasm-bindgen-0.2.70.bazel"), + sha256 = "7ee1280240b7c461d6a0071313e08f34a60b0365f14260362e5a2b17d1d31aa7", + strip_prefix = "wasm-bindgen-0.2.71", + build_file = Label("//cargo/remote:BUILD.wasm-bindgen-0.2.71.bazel"), ) maybe( http_archive, - name = "raze__wasm_bindgen_backend__0_2_70", - url = "https://crates.io/api/v1/crates/wasm-bindgen-backend/0.2.70/download", + name = "raze__wasm_bindgen_backend__0_2_71", + url = "https://crates.io/api/v1/crates/wasm-bindgen-backend/0.2.71/download", type = "tar.gz", - sha256 = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7", - strip_prefix = "wasm-bindgen-backend-0.2.70", - build_file = Label("//cargo/remote:BUILD.wasm-bindgen-backend-0.2.70.bazel"), + sha256 = "5b7d8b6942b8bb3a9b0e73fc79b98095a27de6fa247615e59d096754a3bc2aa8", + strip_prefix = "wasm-bindgen-backend-0.2.71", + build_file = Label("//cargo/remote:BUILD.wasm-bindgen-backend-0.2.71.bazel"), ) maybe( http_archive, - name = "raze__wasm_bindgen_futures__0_4_20", - url = "https://crates.io/api/v1/crates/wasm-bindgen-futures/0.4.20/download", + name = "raze__wasm_bindgen_futures__0_4_21", + url = "https://crates.io/api/v1/crates/wasm-bindgen-futures/0.4.21/download", type = "tar.gz", - sha256 = "3de431a2910c86679c34283a33f66f4e4abd7e0aec27b6669060148872aadf94", - strip_prefix = "wasm-bindgen-futures-0.4.20", - build_file = Label("//cargo/remote:BUILD.wasm-bindgen-futures-0.4.20.bazel"), + sha256 = "8e67a5806118af01f0d9045915676b22aaebecf4178ae7021bc171dab0b897ab", + strip_prefix = "wasm-bindgen-futures-0.4.21", + build_file = Label("//cargo/remote:BUILD.wasm-bindgen-futures-0.4.21.bazel"), ) maybe( http_archive, - name = "raze__wasm_bindgen_macro__0_2_70", - url = "https://crates.io/api/v1/crates/wasm-bindgen-macro/0.2.70/download", + name = "raze__wasm_bindgen_macro__0_2_71", + url = "https://crates.io/api/v1/crates/wasm-bindgen-macro/0.2.71/download", type = "tar.gz", - sha256 = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c", - strip_prefix = "wasm-bindgen-macro-0.2.70", - build_file = Label("//cargo/remote:BUILD.wasm-bindgen-macro-0.2.70.bazel"), + sha256 = "e5ac38da8ef716661f0f36c0d8320b89028efe10c7c0afde65baffb496ce0d3b", + strip_prefix = "wasm-bindgen-macro-0.2.71", + build_file = Label("//cargo/remote:BUILD.wasm-bindgen-macro-0.2.71.bazel"), ) maybe( http_archive, - name = "raze__wasm_bindgen_macro_support__0_2_70", - url = "https://crates.io/api/v1/crates/wasm-bindgen-macro-support/0.2.70/download", + name = "raze__wasm_bindgen_macro_support__0_2_71", + url = "https://crates.io/api/v1/crates/wasm-bindgen-macro-support/0.2.71/download", type = "tar.gz", - sha256 = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385", - strip_prefix = "wasm-bindgen-macro-support-0.2.70", - build_file = Label("//cargo/remote:BUILD.wasm-bindgen-macro-support-0.2.70.bazel"), + sha256 = "cc053ec74d454df287b9374ee8abb36ffd5acb95ba87da3ba5b7d3fe20eb401e", + strip_prefix = "wasm-bindgen-macro-support-0.2.71", + build_file = Label("//cargo/remote:BUILD.wasm-bindgen-macro-support-0.2.71.bazel"), ) maybe( http_archive, - name = "raze__wasm_bindgen_shared__0_2_70", - url = "https://crates.io/api/v1/crates/wasm-bindgen-shared/0.2.70/download", + name = "raze__wasm_bindgen_shared__0_2_71", + url = "https://crates.io/api/v1/crates/wasm-bindgen-shared/0.2.71/download", type = "tar.gz", - sha256 = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64", - strip_prefix = "wasm-bindgen-shared-0.2.70", - build_file = Label("//cargo/remote:BUILD.wasm-bindgen-shared-0.2.70.bazel"), + sha256 = "7d6f8ec44822dd71f5f221a5847fb34acd9060535c1211b70a05844c0f6383b1", + strip_prefix = "wasm-bindgen-shared-0.2.71", + build_file = Label("//cargo/remote:BUILD.wasm-bindgen-shared-0.2.71.bazel"), ) maybe( http_archive, - name = "raze__web_sys__0_3_47", - url = "https://crates.io/api/v1/crates/web-sys/0.3.47/download", + name = "raze__web_sys__0_3_48", + url = "https://crates.io/api/v1/crates/web-sys/0.3.48/download", type = "tar.gz", - sha256 = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3", - strip_prefix = "web-sys-0.3.47", - build_file = Label("//cargo/remote:BUILD.web-sys-0.3.47.bazel"), + sha256 = "ec600b26223b2948cedfde2a0aa6756dcf1fef616f43d7b3097aaf53a6c4d92b", + strip_prefix = "web-sys-0.3.48", + build_file = Label("//cargo/remote:BUILD.web-sys-0.3.48.bazel"), ) maybe( diff --git a/cargo/licenses.json b/cargo/licenses.json index fcb3e2e07..ca3f4327f 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -10,7 +10,7 @@ }, { "name": "adler", - "version": "0.2.3", + "version": "1.0.2", "authors": "Jonas Schievink ", "repository": "https://github.com/jonas-schievink/adler.git", "license": "0BSD OR Apache-2.0 OR MIT", @@ -37,7 +37,7 @@ }, { "name": "ammonia", - "version": "3.1.0", + "version": "3.1.1", "authors": "Michael Howell ", "repository": "https://github.com/rust-ammonia/ammonia", "license": "Apache-2.0 OR MIT", @@ -154,7 +154,7 @@ }, { "name": "async-trait", - "version": "0.1.42", + "version": "0.1.48", "authors": "David Tolnay ", "repository": "https://github.com/dtolnay/async-trait", "license": "Apache-2.0 OR MIT", @@ -217,7 +217,7 @@ }, { "name": "bitvec", - "version": "0.19.4", + "version": "0.19.5", "authors": "myrrlyn ", "repository": "https://github.com/myrrlyn/bitvec", "license": "MIT", @@ -244,7 +244,7 @@ }, { "name": "bumpalo", - "version": "3.6.0", + "version": "3.6.1", "authors": "Nick Fitzgerald ", "repository": "https://github.com/fitzgen/bumpalo", "license": "Apache-2.0 OR MIT", @@ -289,7 +289,7 @@ }, { "name": "cc", - "version": "1.0.66", + "version": "1.0.67", "authors": "Alex Crichton ", "repository": "https://github.com/alexcrichton/cc-rs", "license": "Apache-2.0 OR MIT", @@ -379,7 +379,7 @@ }, { "name": "crossbeam-utils", - "version": "0.8.1", + "version": "0.8.3", "authors": "The Crossbeam Project Developers", "repository": "https://github.com/crossbeam-rs/crossbeam", "license": "Apache-2.0 OR MIT", @@ -469,7 +469,7 @@ }, { "name": "env_logger", - "version": "0.8.2", + "version": "0.8.3", "authors": "The Rust Project Developers", "repository": "https://github.com/env-logger-rs/env_logger/", "license": "Apache-2.0 OR MIT", @@ -595,7 +595,7 @@ }, { "name": "form_urlencoded", - "version": "1.0.0", + "version": "1.0.1", "authors": "The rust-url developers", "repository": "https://github.com/servo/rust-url", "license": "Apache-2.0 OR MIT", @@ -640,7 +640,7 @@ }, { "name": "futures", - "version": "0.3.12", + "version": "0.3.13", "authors": "Alex Crichton ", "repository": "https://github.com/rust-lang/futures-rs", "license": "Apache-2.0 OR MIT", @@ -649,7 +649,7 @@ }, { "name": "futures-channel", - "version": "0.3.12", + "version": "0.3.13", "authors": "Alex Crichton ", "repository": "https://github.com/rust-lang/futures-rs", "license": "Apache-2.0 OR MIT", @@ -658,7 +658,7 @@ }, { "name": "futures-core", - "version": "0.3.12", + "version": "0.3.13", "authors": "Alex Crichton ", "repository": "https://github.com/rust-lang/futures-rs", "license": "Apache-2.0 OR MIT", @@ -667,7 +667,7 @@ }, { "name": "futures-executor", - "version": "0.3.12", + "version": "0.3.13", "authors": "Alex Crichton ", "repository": "https://github.com/rust-lang/futures-rs", "license": "Apache-2.0 OR MIT", @@ -676,7 +676,7 @@ }, { "name": "futures-io", - "version": "0.3.12", + "version": "0.3.13", "authors": "Alex Crichton ", "repository": "https://github.com/rust-lang/futures-rs", "license": "Apache-2.0 OR MIT", @@ -685,7 +685,7 @@ }, { "name": "futures-macro", - "version": "0.3.12", + "version": "0.3.13", "authors": "Taylor Cramer |Taiki Endo ", "repository": "https://github.com/rust-lang/futures-rs", "license": "Apache-2.0 OR MIT", @@ -694,7 +694,7 @@ }, { "name": "futures-sink", - "version": "0.3.12", + "version": "0.3.13", "authors": "Alex Crichton ", "repository": "https://github.com/rust-lang/futures-rs", "license": "Apache-2.0 OR MIT", @@ -703,7 +703,7 @@ }, { "name": "futures-task", - "version": "0.3.12", + "version": "0.3.13", "authors": "Alex Crichton ", "repository": "https://github.com/rust-lang/futures-rs", "license": "Apache-2.0 OR MIT", @@ -712,7 +712,7 @@ }, { "name": "futures-util", - "version": "0.3.12", + "version": "0.3.13", "authors": "Alex Crichton ", "repository": "https://github.com/rust-lang/futures-rs", "license": "Apache-2.0 OR MIT", @@ -820,7 +820,7 @@ }, { "name": "hex", - "version": "0.4.2", + "version": "0.4.3", "authors": "KokaKiwi ", "repository": "https://github.com/KokaKiwi/rust-hex", "license": "Apache-2.0 OR MIT", @@ -937,7 +937,7 @@ }, { "name": "idna", - "version": "0.2.1", + "version": "0.2.2", "authors": "The rust-url developers", "repository": "https://github.com/servo/rust-url/", "license": "Apache-2.0 OR MIT", @@ -946,7 +946,7 @@ }, { "name": "indexmap", - "version": "1.6.1", + "version": "1.6.2", "authors": "bluss|Josh Stone ", "repository": "https://github.com/bluss/indexmap", "license": "Apache-2.0 OR MIT", @@ -955,7 +955,16 @@ }, { "name": "indoc", - "version": "1.0.3", + "version": "0.3.6", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/indoc", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Indented document literals" + }, + { + "name": "indoc-impl", + "version": "0.3.6", "authors": "David Tolnay ", "repository": "https://github.com/dtolnay/indoc", "license": "Apache-2.0 OR MIT", @@ -1045,7 +1054,7 @@ }, { "name": "js-sys", - "version": "0.3.47", + "version": "0.3.48", "authors": "The wasm-bindgen Developers", "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys", "license": "Apache-2.0 OR MIT", @@ -1072,7 +1081,7 @@ }, { "name": "lexical-core", - "version": "0.7.4", + "version": "0.7.5", "authors": "Alex Huszagh ", "repository": "https://github.com/Alexhuszagh/rust-lexical/tree/master/lexical-core", "license": "Apache-2.0 OR MIT", @@ -1081,7 +1090,7 @@ }, { "name": "libc", - "version": "0.2.85", + "version": "0.2.88", "authors": "The Rust Project Developers", "repository": "https://github.com/rust-lang/libc", "license": "Apache-2.0 OR MIT", @@ -1189,7 +1198,7 @@ }, { "name": "miniz_oxide", - "version": "0.4.3", + "version": "0.4.4", "authors": "Frommi |oyvindln ", "repository": "https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide", "license": "Apache-2.0 OR MIT OR Zlib", @@ -1261,7 +1270,7 @@ }, { "name": "nom", - "version": "6.1.0", + "version": "6.1.2", "authors": "contact@geoffroycouprie.com", "repository": "https://github.com/Geal/nom", "license": "MIT", @@ -1333,7 +1342,7 @@ }, { "name": "once_cell", - "version": "1.5.2", + "version": "1.7.2", "authors": "Aleksey Kladov ", "repository": "https://github.com/matklad/once_cell", "license": "Apache-2.0 OR MIT", @@ -1378,7 +1387,7 @@ }, { "name": "parking_lot_core", - "version": "0.8.2", + "version": "0.8.3", "authors": "Amanieu d'Antras ", "repository": "https://github.com/Amanieu/parking_lot", "license": "Apache-2.0 OR MIT", @@ -1387,13 +1396,22 @@ }, { "name": "paste", - "version": "1.0.4", + "version": "0.1.18", "authors": "David Tolnay ", "repository": "https://github.com/dtolnay/paste", "license": "Apache-2.0 OR MIT", "license_file": null, "description": "Macros for all your token pasting needs" }, + { + "name": "paste-impl", + "version": "0.1.18", + "authors": "David Tolnay ", + "repository": "https://github.com/dtolnay/paste", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Implementation detail of the `paste` crate" + }, { "name": "percent-encoding", "version": "2.1.0", @@ -1448,15 +1466,6 @@ "license_file": null, "description": "Support code shared by PHF libraries" }, - { - "name": "pin-project", - "version": "0.4.27", - "authors": "Taiki Endo ", - "repository": "https://github.com/taiki-e/pin-project", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "A crate for safe and ergonomic pin-projection." - }, { "name": "pin-project", "version": "1.0.5", @@ -1466,15 +1475,6 @@ "license_file": null, "description": "A crate for safe and ergonomic pin-projection." }, - { - "name": "pin-project-internal", - "version": "0.4.27", - "authors": "Taiki Endo ", - "repository": "https://github.com/taiki-e/pin-project", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "An internal crate to support pin_project - do not use directly" - }, { "name": "pin-project-internal", "version": "1.0.5", @@ -1486,7 +1486,7 @@ }, { "name": "pin-project-lite", - "version": "0.1.11", + "version": "0.1.12", "authors": "Taiki Endo ", "repository": "https://github.com/taiki-e/pin-project-lite", "license": "Apache-2.0 OR MIT", @@ -1495,7 +1495,7 @@ }, { "name": "pin-project-lite", - "version": "0.2.4", + "version": "0.2.6", "authors": "Taiki Endo ", "repository": "https://github.com/taiki-e/pin-project-lite", "license": "Apache-2.0 OR MIT", @@ -1630,7 +1630,7 @@ }, { "name": "pyo3", - "version": "0.13.1", + "version": "0.13.2", "authors": "PyO3 Project and Contributors ", "repository": "https://github.com/pyo3/pyo3", "license": "Apache-2.0", @@ -1639,7 +1639,7 @@ }, { "name": "pyo3-macros", - "version": "0.13.1", + "version": "0.13.2", "authors": "PyO3 Project and Contributors ", "repository": "https://github.com/pyo3/pyo3", "license": "Apache-2.0", @@ -1648,7 +1648,7 @@ }, { "name": "pyo3-macros-backend", - "version": "0.13.1", + "version": "0.13.2", "authors": "PyO3 Project and Contributors ", "repository": "https://github.com/pyo3/pyo3", "license": "Apache-2.0", @@ -1657,7 +1657,7 @@ }, { "name": "quote", - "version": "1.0.8", + "version": "1.0.9", "authors": "David Tolnay ", "repository": "https://github.com/dtolnay/quote", "license": "Apache-2.0 OR MIT", @@ -1720,7 +1720,7 @@ }, { "name": "rand_core", - "version": "0.6.1", + "version": "0.6.2", "authors": "The Rand Project Developers|The Rust Project Developers", "repository": "https://github.com/rust-random/rand", "license": "Apache-2.0 OR MIT", @@ -1765,7 +1765,7 @@ }, { "name": "redox_syscall", - "version": "0.2.4", + "version": "0.2.5", "authors": "Jeremy Soller ", "repository": "https://gitlab.redox-os.org/redox-os/syscall", "license": "MIT", @@ -1936,7 +1936,7 @@ }, { "name": "security-framework", - "version": "2.0.0", + "version": "2.1.2", "authors": "Steven Fackler |Kornel ", "repository": "https://github.com/kornelski/rust-security-framework", "license": "Apache-2.0 OR MIT", @@ -1945,7 +1945,7 @@ }, { "name": "security-framework-sys", - "version": "2.0.0", + "version": "2.1.1", "authors": "Steven Fackler |Kornel ", "repository": "https://github.com/kornelski/rust-security-framework", "license": "Apache-2.0 OR MIT", @@ -1954,7 +1954,7 @@ }, { "name": "serde", - "version": "1.0.123", + "version": "1.0.124", "authors": "Erick Tryzelaar |David Tolnay ", "repository": "https://github.com/serde-rs/serde", "license": "Apache-2.0 OR MIT", @@ -1972,7 +1972,7 @@ }, { "name": "serde_derive", - "version": "1.0.123", + "version": "1.0.124", "authors": "Erick Tryzelaar |David Tolnay ", "repository": "https://github.com/serde-rs/serde", "license": "Apache-2.0 OR MIT", @@ -1981,7 +1981,7 @@ }, { "name": "serde_json", - "version": "1.0.62", + "version": "1.0.64", "authors": "Erick Tryzelaar |David Tolnay ", "repository": "https://github.com/serde-rs/json", "license": "Apache-2.0 OR MIT", @@ -2168,6 +2168,24 @@ "license_file": null, "description": "A codegen library for string-cache, developed as part of the Servo project." }, + { + "name": "strum", + "version": "0.20.0", + "authors": "Peter Glotfelty ", + "repository": "https://github.com/Peternator7/strum", + "license": "MIT", + "license_file": null, + "description": "Helpful macros for working with enums and strings" + }, + { + "name": "strum_macros", + "version": "0.20.1", + "authors": "Peter Glotfelty ", + "repository": "https://github.com/Peternator7/strum", + "license": "MIT", + "license_file": null, + "description": "Helpful macros for working with enums and strings" + }, { "name": "subtle", "version": "2.4.0", @@ -2179,7 +2197,7 @@ }, { "name": "syn", - "version": "1.0.60", + "version": "1.0.63", "authors": "David Tolnay ", "repository": "https://github.com/dtolnay/syn", "license": "Apache-2.0 OR MIT", @@ -2206,7 +2224,7 @@ }, { "name": "tap", - "version": "1.0.0", + "version": "1.0.1", "authors": "Elliott Linder |myrrlyn ", "repository": "https://github.com/myrrlyn/tap", "license": "MIT", @@ -2251,7 +2269,7 @@ }, { "name": "thiserror", - "version": "1.0.23", + "version": "1.0.24", "authors": "David Tolnay ", "repository": "https://github.com/dtolnay/thiserror", "license": "Apache-2.0 OR MIT", @@ -2260,7 +2278,7 @@ }, { "name": "thiserror-impl", - "version": "1.0.23", + "version": "1.0.24", "authors": "David Tolnay ", "repository": "https://github.com/dtolnay/thiserror", "license": "Apache-2.0 OR MIT", @@ -2386,7 +2404,7 @@ }, { "name": "tracing", - "version": "0.1.23", + "version": "0.1.25", "authors": "Eliza Weisman |Tokio Contributors ", "repository": "https://github.com/tokio-rs/tracing", "license": "MIT", @@ -2404,7 +2422,7 @@ }, { "name": "tracing-futures", - "version": "0.2.4", + "version": "0.2.5", "authors": "Eliza Weisman |Tokio Contributors ", "repository": "https://github.com/tokio-rs/tracing", "license": "MIT", @@ -2494,7 +2512,7 @@ }, { "name": "unicode-normalization", - "version": "0.1.16", + "version": "0.1.17", "authors": "kwantam |Manish Goregaokar ", "repository": "https://github.com/unicode-rs/unicode-normalization", "license": "Apache-2.0 OR MIT", @@ -2548,7 +2566,7 @@ }, { "name": "url", - "version": "2.2.0", + "version": "2.2.1", "authors": "The rust-url developers", "repository": "https://github.com/servo/rust-url", "license": "Apache-2.0 OR MIT", @@ -2620,7 +2638,7 @@ }, { "name": "wasm-bindgen", - "version": "0.2.70", + "version": "0.2.71", "authors": "The wasm-bindgen Developers", "repository": "https://github.com/rustwasm/wasm-bindgen", "license": "Apache-2.0 OR MIT", @@ -2629,7 +2647,7 @@ }, { "name": "wasm-bindgen-backend", - "version": "0.2.70", + "version": "0.2.71", "authors": "The wasm-bindgen Developers", "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/backend", "license": "Apache-2.0 OR MIT", @@ -2638,7 +2656,7 @@ }, { "name": "wasm-bindgen-futures", - "version": "0.4.20", + "version": "0.4.21", "authors": "The wasm-bindgen Developers", "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures", "license": "Apache-2.0 OR MIT", @@ -2647,7 +2665,7 @@ }, { "name": "wasm-bindgen-macro", - "version": "0.2.70", + "version": "0.2.71", "authors": "The wasm-bindgen Developers", "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro", "license": "Apache-2.0 OR MIT", @@ -2656,7 +2674,7 @@ }, { "name": "wasm-bindgen-macro-support", - "version": "0.2.70", + "version": "0.2.71", "authors": "The wasm-bindgen Developers", "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro-support", "license": "Apache-2.0 OR MIT", @@ -2665,7 +2683,7 @@ }, { "name": "wasm-bindgen-shared", - "version": "0.2.70", + "version": "0.2.71", "authors": "The wasm-bindgen Developers", "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/shared", "license": "Apache-2.0 OR MIT", @@ -2674,7 +2692,7 @@ }, { "name": "web-sys", - "version": "0.3.47", + "version": "0.3.48", "authors": "The wasm-bindgen Developers", "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/web-sys", "license": "Apache-2.0 OR MIT", diff --git a/cargo/remote/BUILD.adler-0.2.3.bazel b/cargo/remote/BUILD.adler-1.0.2.bazel similarity index 97% rename from cargo/remote/BUILD.adler-0.2.3.bazel rename to cargo/remote/BUILD.adler-1.0.2.bazel index c84194641..69b273992 100644 --- a/cargo/remote/BUILD.adler-0.2.3.bazel +++ b/cargo/remote/BUILD.adler-1.0.2.bazel @@ -48,7 +48,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.3", + version = "1.0.2", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.ammonia-3.1.0.bazel b/cargo/remote/BUILD.ammonia-3.1.1.bazel similarity index 96% rename from cargo/remote/BUILD.ammonia-3.1.0.bazel rename to cargo/remote/BUILD.ammonia-3.1.1.bazel index ca1d28cff..0d1967c5a 100644 --- a/cargo/remote/BUILD.ammonia-3.1.0.bazel +++ b/cargo/remote/BUILD.ammonia-3.1.1.bazel @@ -48,7 +48,7 @@ rust_library( "cargo-raze", "manual", ], - version = "3.1.0", + version = "3.1.1", # buildifier: leave-alone deps = [ "@raze__html5ever__0_25_1//:html5ever", @@ -57,7 +57,7 @@ rust_library( "@raze__markup5ever_rcdom__0_1_0//:markup5ever_rcdom", "@raze__matches__0_1_8//:matches", "@raze__tendril__0_4_2//:tendril", - "@raze__url__2_2_0//:url", + "@raze__url__2_2_1//:url", ], ) diff --git a/cargo/remote/BUILD.askama_derive-0.10.5.bazel b/cargo/remote/BUILD.askama_derive-0.10.5.bazel index 4ddb63a2e..d1b8dc2a5 100644 --- a/cargo/remote/BUILD.askama_derive-0.10.5.bazel +++ b/cargo/remote/BUILD.askama_derive-0.10.5.bazel @@ -51,6 +51,6 @@ rust_library( deps = [ "@raze__askama_shared__0_11_1//:askama_shared", "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__syn__1_0_60//:syn", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.askama_shared-0.11.1.bazel b/cargo/remote/BUILD.askama_shared-0.11.1.bazel index 51b2c9d34..fd8640554 100644 --- a/cargo/remote/BUILD.askama_shared-0.11.1.bazel +++ b/cargo/remote/BUILD.askama_shared-0.11.1.bazel @@ -57,13 +57,13 @@ rust_library( deps = [ "@raze__askama_escape__0_10_1//:askama_escape", "@raze__humansize__1_1_0//:humansize", - "@raze__nom__6_1_0//:nom", + "@raze__nom__6_1_2//:nom", "@raze__num_traits__0_2_14//:num_traits", "@raze__percent_encoding__2_1_0//:percent_encoding", "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__serde__1_0_123//:serde", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__serde__1_0_124//:serde", + "@raze__syn__1_0_63//:syn", "@raze__toml__0_5_8//:toml", ], ) diff --git a/cargo/remote/BUILD.async-compression-0.3.7.bazel b/cargo/remote/BUILD.async-compression-0.3.7.bazel index 231e4bb65..ab324093d 100644 --- a/cargo/remote/BUILD.async-compression-0.3.7.bazel +++ b/cargo/remote/BUILD.async-compression-0.3.7.bazel @@ -63,9 +63,9 @@ rust_library( deps = [ "@raze__bytes__0_5_6//:bytes", "@raze__flate2__1_0_20//:flate2", - "@raze__futures_core__0_3_12//:futures_core", + "@raze__futures_core__0_3_13//:futures_core", "@raze__memchr__2_3_4//:memchr", - "@raze__pin_project_lite__0_2_4//:pin_project_lite", + "@raze__pin_project_lite__0_2_6//:pin_project_lite", ], ) diff --git a/cargo/remote/BUILD.pin-project-internal-0.4.27.bazel b/cargo/remote/BUILD.async-trait-0.1.48.bazel similarity index 78% rename from cargo/remote/BUILD.pin-project-internal-0.4.27.bazel rename to cargo/remote/BUILD.async-trait-0.1.48.bazel index 9441e8988..9340810f2 100644 --- a/cargo/remote/BUILD.pin-project-internal-0.4.27.bazel +++ b/cargo/remote/BUILD.async-trait-0.1.48.bazel @@ -25,7 +25,7 @@ package(default_visibility = [ ]) licenses([ - "notice", # Apache-2.0 from expression "Apache-2.0 OR MIT" + "notice", # MIT from expression "MIT OR Apache-2.0" ]) # Generated Targets @@ -36,7 +36,7 @@ load( ) cargo_build_script( - name = "pin_project_internal_build_script", + name = "async_trait_build_script", srcs = glob(["**/*.rs"]), build_script_env = { }, @@ -52,14 +52,14 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "0.4.27", + version = "0.1.48", visibility = ["//visibility:private"], deps = [ ], ) rust_library( - name = "pin_project_internal", + name = "async_trait", srcs = glob(["**/*.rs"]), crate_features = [ ], @@ -74,12 +74,16 @@ rust_library( "cargo-raze", "manual", ], - version = "0.4.27", + version = "0.1.48", # buildifier: leave-alone deps = [ - ":pin_project_internal_build_script", + ":async_trait_build_script", "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) + +# Unsupported target "compiletest" with type "test" omitted + +# Unsupported target "test" with type "test" omitted diff --git a/cargo/remote/BUILD.atty-0.2.14.bazel b/cargo/remote/BUILD.atty-0.2.14.bazel index 27f45b192..13c91d5c7 100644 --- a/cargo/remote/BUILD.atty-0.2.14.bazel +++ b/cargo/remote/BUILD.atty-0.2.14.bazel @@ -62,7 +62,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }) + selects.with_or({ diff --git a/cargo/remote/BUILD.backtrace-0.3.56.bazel b/cargo/remote/BUILD.backtrace-0.3.56.bazel index f8a99c327..2c3114a65 100644 --- a/cargo/remote/BUILD.backtrace-0.3.56.bazel +++ b/cargo/remote/BUILD.backtrace-0.3.56.bazel @@ -65,8 +65,8 @@ rust_library( deps = [ "@raze__addr2line__0_14_1//:addr2line", "@raze__cfg_if__1_0_0//:cfg_if", - "@raze__libc__0_2_85//:libc", - "@raze__miniz_oxide__0_4_3//:miniz_oxide", + "@raze__libc__0_2_88//:libc", + "@raze__miniz_oxide__0_4_4//:miniz_oxide", "@raze__object__0_23_0//:object", "@raze__rustc_demangle__0_1_18//:rustc_demangle", ] + selects.with_or({ diff --git a/cargo/remote/BUILD.bitvec-0.19.4.bazel b/cargo/remote/BUILD.bitvec-0.19.5.bazel similarity index 95% rename from cargo/remote/BUILD.bitvec-0.19.4.bazel rename to cargo/remote/BUILD.bitvec-0.19.5.bazel index 740e1495f..6e5e49d07 100644 --- a/cargo/remote/BUILD.bitvec-0.19.4.bazel +++ b/cargo/remote/BUILD.bitvec-0.19.5.bazel @@ -54,12 +54,12 @@ rust_library( "cargo-raze", "manual", ], - version = "0.19.4", + version = "0.19.5", # buildifier: leave-alone deps = [ "@raze__funty__1_1_0//:funty", "@raze__radium__0_5_3//:radium", - "@raze__tap__1_0_0//:tap", + "@raze__tap__1_0_1//:tap", "@raze__wyz__0_2_0//:wyz", ], ) diff --git a/cargo/remote/BUILD.blake3-0.3.7.bazel b/cargo/remote/BUILD.blake3-0.3.7.bazel index 3fe58c2bf..bc8421926 100644 --- a/cargo/remote/BUILD.blake3-0.3.7.bazel +++ b/cargo/remote/BUILD.blake3-0.3.7.bazel @@ -57,7 +57,7 @@ cargo_build_script( version = "0.3.7", visibility = ["//visibility:private"], deps = [ - "@raze__cc__1_0_66//:cc", + "@raze__cc__1_0_67//:cc", ], ) diff --git a/cargo/remote/BUILD.bumpalo-3.6.0.bazel b/cargo/remote/BUILD.bumpalo-3.6.1.bazel similarity index 98% rename from cargo/remote/BUILD.bumpalo-3.6.0.bazel rename to cargo/remote/BUILD.bumpalo-3.6.1.bazel index 44139091b..89af028a1 100644 --- a/cargo/remote/BUILD.bumpalo-3.6.0.bazel +++ b/cargo/remote/BUILD.bumpalo-3.6.1.bazel @@ -49,7 +49,7 @@ rust_library( "cargo-raze", "manual", ], - version = "3.6.0", + version = "3.6.1", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.cc-1.0.66.bazel b/cargo/remote/BUILD.cc-1.0.67.bazel similarity index 97% rename from cargo/remote/BUILD.cc-1.0.66.bazel rename to cargo/remote/BUILD.cc-1.0.67.bazel index 17d6e6292..5d43872f4 100644 --- a/cargo/remote/BUILD.cc-1.0.66.bazel +++ b/cargo/remote/BUILD.cc-1.0.67.bazel @@ -47,7 +47,7 @@ rust_binary( "cargo-raze", "manual", ], - version = "1.0.66", + version = "1.0.67", # buildifier: leave-alone deps = [ # Binaries get an implicit dependency on their crate's lib @@ -71,7 +71,7 @@ rust_library( "cargo-raze", "manual", ], - version = "1.0.66", + version = "1.0.67", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.chrono-0.4.19.bazel b/cargo/remote/BUILD.chrono-0.4.19.bazel index aa64505b8..8d204827a 100644 --- a/cargo/remote/BUILD.chrono-0.4.19.bazel +++ b/cargo/remote/BUILD.chrono-0.4.19.bazel @@ -62,7 +62,7 @@ rust_library( version = "0.4.19", # buildifier: leave-alone deps = [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", "@raze__num_integer__0_1_44//:num_integer", "@raze__num_traits__0_2_14//:num_traits", "@raze__time__0_1_43//:time", diff --git a/cargo/remote/BUILD.coarsetime-0.1.18.bazel b/cargo/remote/BUILD.coarsetime-0.1.18.bazel index 2b7296434..f046786a1 100644 --- a/cargo/remote/BUILD.coarsetime-0.1.18.bazel +++ b/cargo/remote/BUILD.coarsetime-0.1.18.bazel @@ -51,7 +51,7 @@ rust_library( version = "0.1.18", # buildifier: leave-alone deps = [ - "@raze__once_cell__1_5_2//:once_cell", + "@raze__once_cell__1_7_2//:once_cell", ] + selects.with_or({ # cfg(not(target_os = "wasi")) ( @@ -62,7 +62,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-pc-windows-msvc", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }), diff --git a/cargo/remote/BUILD.core-foundation-0.9.1.bazel b/cargo/remote/BUILD.core-foundation-0.9.1.bazel index 407994e7e..4b77758c1 100644 --- a/cargo/remote/BUILD.core-foundation-0.9.1.bazel +++ b/cargo/remote/BUILD.core-foundation-0.9.1.bazel @@ -50,7 +50,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__core_foundation_sys__0_8_2//:core_foundation_sys", - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], ) diff --git a/cargo/remote/BUILD.crossbeam-channel-0.5.0.bazel b/cargo/remote/BUILD.crossbeam-channel-0.5.0.bazel index 0c8a08e95..0ad625aca 100644 --- a/cargo/remote/BUILD.crossbeam-channel-0.5.0.bazel +++ b/cargo/remote/BUILD.crossbeam-channel-0.5.0.bazel @@ -61,7 +61,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__cfg_if__1_0_0//:cfg_if", - "@raze__crossbeam_utils__0_8_1//:crossbeam_utils", + "@raze__crossbeam_utils__0_8_3//:crossbeam_utils", ], ) diff --git a/cargo/remote/BUILD.crossbeam-utils-0.8.1.bazel b/cargo/remote/BUILD.crossbeam-utils-0.8.3.bazel similarity index 98% rename from cargo/remote/BUILD.crossbeam-utils-0.8.1.bazel rename to cargo/remote/BUILD.crossbeam-utils-0.8.3.bazel index 45422717c..0a1e440ba 100644 --- a/cargo/remote/BUILD.crossbeam-utils-0.8.1.bazel +++ b/cargo/remote/BUILD.crossbeam-utils-0.8.3.bazel @@ -55,7 +55,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "0.8.1", + version = "0.8.3", visibility = ["//visibility:private"], deps = [ "@raze__autocfg__1_0_1//:autocfg", @@ -83,7 +83,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.8.1", + version = "0.8.3", # buildifier: leave-alone deps = [ ":crossbeam_utils_build_script", diff --git a/cargo/remote/BUILD.ctor-0.1.19.bazel b/cargo/remote/BUILD.ctor-0.1.19.bazel index 58cfa526e..2612c69a6 100644 --- a/cargo/remote/BUILD.ctor-0.1.19.bazel +++ b/cargo/remote/BUILD.ctor-0.1.19.bazel @@ -51,7 +51,7 @@ rust_library( version = "0.1.19", # buildifier: leave-alone deps = [ - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.derivative-2.2.0.bazel b/cargo/remote/BUILD.derivative-2.2.0.bazel index 9a753c788..0f0286a4e 100644 --- a/cargo/remote/BUILD.derivative-2.2.0.bazel +++ b/cargo/remote/BUILD.derivative-2.2.0.bazel @@ -51,8 +51,8 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.dirs-sys-0.3.5.bazel b/cargo/remote/BUILD.dirs-sys-0.3.5.bazel index e1fa5e589..25552b20d 100644 --- a/cargo/remote/BUILD.dirs-sys-0.3.5.bazel +++ b/cargo/remote/BUILD.dirs-sys-0.3.5.bazel @@ -60,7 +60,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }) + selects.with_or({ diff --git a/cargo/remote/BUILD.env_logger-0.8.2.bazel b/cargo/remote/BUILD.env_logger-0.8.3.bazel similarity index 73% rename from cargo/remote/BUILD.env_logger-0.8.2.bazel rename to cargo/remote/BUILD.env_logger-0.8.3.bazel index 29177b578..494df7c8c 100644 --- a/cargo/remote/BUILD.env_logger-0.8.2.bazel +++ b/cargo/remote/BUILD.env_logger-0.8.3.bazel @@ -30,22 +30,6 @@ licenses([ # Generated Targets -# Unsupported target "custom_default_format" with type "example" omitted - -# Unsupported target "custom_format" with type "example" omitted - -# Unsupported target "custom_logger" with type "example" omitted - -# Unsupported target "default" with type "example" omitted - -# Unsupported target "direct_logger" with type "example" omitted - -# Unsupported target "filters_from_code" with type "example" omitted - -# Unsupported target "in_tests" with type "example" omitted - -# Unsupported target "syslog_friendly_format" with type "example" omitted - rust_library( name = "env_logger", srcs = glob(["**/*.rs"]), @@ -67,7 +51,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.8.2", + version = "0.8.3", # buildifier: leave-alone deps = [ "@raze__atty__0_2_14//:atty", diff --git a/cargo/remote/BUILD.failure_derive-0.1.8.bazel b/cargo/remote/BUILD.failure_derive-0.1.8.bazel index 3c82f3016..51b44dddf 100644 --- a/cargo/remote/BUILD.failure_derive-0.1.8.bazel +++ b/cargo/remote/BUILD.failure_derive-0.1.8.bazel @@ -79,8 +79,8 @@ rust_library( deps = [ ":failure_derive_build_script", "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", "@raze__synstructure__0_12_4//:synstructure", ], ) diff --git a/cargo/remote/BUILD.flate2-1.0.20.bazel b/cargo/remote/BUILD.flate2-1.0.20.bazel index d70e50cf5..45cd063e9 100644 --- a/cargo/remote/BUILD.flate2-1.0.20.bazel +++ b/cargo/remote/BUILD.flate2-1.0.20.bazel @@ -98,8 +98,8 @@ rust_library( deps = [ "@raze__cfg_if__1_0_0//:cfg_if", "@raze__crc32fast__1_2_1//:crc32fast", - "@raze__libc__0_2_85//:libc", - "@raze__miniz_oxide__0_4_3//:miniz_oxide", + "@raze__libc__0_2_88//:libc", + "@raze__miniz_oxide__0_4_4//:miniz_oxide", ], ) diff --git a/cargo/remote/BUILD.form_urlencoded-1.0.0.bazel b/cargo/remote/BUILD.form_urlencoded-1.0.1.bazel similarity index 96% rename from cargo/remote/BUILD.form_urlencoded-1.0.0.bazel rename to cargo/remote/BUILD.form_urlencoded-1.0.1.bazel index e87880d8d..54482d722 100644 --- a/cargo/remote/BUILD.form_urlencoded-1.0.0.bazel +++ b/cargo/remote/BUILD.form_urlencoded-1.0.1.bazel @@ -38,7 +38,7 @@ rust_library( crate_root = "src/lib.rs", crate_type = "lib", data = [], - edition = "2015", + edition = "2018", rustc_flags = [ "--cap-lints=allow", ], @@ -46,7 +46,7 @@ rust_library( "cargo-raze", "manual", ], - version = "1.0.0", + version = "1.0.1", # buildifier: leave-alone deps = [ "@raze__matches__0_1_8//:matches", diff --git a/cargo/remote/BUILD.futures-0.3.12.bazel b/cargo/remote/BUILD.futures-0.3.13.bazel similarity index 90% rename from cargo/remote/BUILD.futures-0.3.12.bazel rename to cargo/remote/BUILD.futures-0.3.13.bazel index 332b58bce..45c797842 100644 --- a/cargo/remote/BUILD.futures-0.3.12.bazel +++ b/cargo/remote/BUILD.futures-0.3.13.bazel @@ -52,16 +52,16 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.12", + version = "0.3.13", # buildifier: leave-alone deps = [ - "@raze__futures_channel__0_3_12//:futures_channel", - "@raze__futures_core__0_3_12//:futures_core", - "@raze__futures_executor__0_3_12//:futures_executor", - "@raze__futures_io__0_3_12//:futures_io", - "@raze__futures_sink__0_3_12//:futures_sink", - "@raze__futures_task__0_3_12//:futures_task", - "@raze__futures_util__0_3_12//:futures_util", + "@raze__futures_channel__0_3_13//:futures_channel", + "@raze__futures_core__0_3_13//:futures_core", + "@raze__futures_executor__0_3_13//:futures_executor", + "@raze__futures_io__0_3_13//:futures_io", + "@raze__futures_sink__0_3_13//:futures_sink", + "@raze__futures_task__0_3_13//:futures_task", + "@raze__futures_util__0_3_13//:futures_util", ], ) @@ -75,6 +75,8 @@ rust_library( # Unsupported target "atomic_waker" with type "test" omitted +# Unsupported target "auto_traits" with type "test" omitted + # Unsupported target "basic_combinators" with type "test" omitted # Unsupported target "buffer_unordered" with type "test" omitted diff --git a/cargo/remote/BUILD.futures-channel-0.3.12.bazel b/cargo/remote/BUILD.futures-channel-0.3.13.bazel similarity index 91% rename from cargo/remote/BUILD.futures-channel-0.3.12.bazel rename to cargo/remote/BUILD.futures-channel-0.3.13.bazel index 0f91e0ae3..aac8bf32c 100644 --- a/cargo/remote/BUILD.futures-channel-0.3.12.bazel +++ b/cargo/remote/BUILD.futures-channel-0.3.13.bazel @@ -53,11 +53,11 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.12", + version = "0.3.13", # buildifier: leave-alone deps = [ - "@raze__futures_core__0_3_12//:futures_core", - "@raze__futures_sink__0_3_12//:futures_sink", + "@raze__futures_core__0_3_13//:futures_core", + "@raze__futures_sink__0_3_13//:futures_sink", ], ) diff --git a/cargo/remote/BUILD.futures-core-0.3.12.bazel b/cargo/remote/BUILD.futures-core-0.3.13.bazel similarity index 97% rename from cargo/remote/BUILD.futures-core-0.3.12.bazel rename to cargo/remote/BUILD.futures-core-0.3.13.bazel index d7c3d48e9..05eb6f799 100644 --- a/cargo/remote/BUILD.futures-core-0.3.12.bazel +++ b/cargo/remote/BUILD.futures-core-0.3.13.bazel @@ -49,7 +49,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.12", + version = "0.3.13", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.futures-executor-0.3.12.bazel b/cargo/remote/BUILD.futures-executor-0.3.13.bazel similarity index 86% rename from cargo/remote/BUILD.futures-executor-0.3.12.bazel rename to cargo/remote/BUILD.futures-executor-0.3.13.bazel index f56ef2222..14cf36255 100644 --- a/cargo/remote/BUILD.futures-executor-0.3.12.bazel +++ b/cargo/remote/BUILD.futures-executor-0.3.13.bazel @@ -49,12 +49,12 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.12", + version = "0.3.13", # buildifier: leave-alone deps = [ - "@raze__futures_core__0_3_12//:futures_core", - "@raze__futures_task__0_3_12//:futures_task", - "@raze__futures_util__0_3_12//:futures_util", + "@raze__futures_core__0_3_13//:futures_core", + "@raze__futures_task__0_3_13//:futures_task", + "@raze__futures_util__0_3_13//:futures_util", ], ) diff --git a/cargo/remote/BUILD.futures-io-0.3.12.bazel b/cargo/remote/BUILD.futures-io-0.3.13.bazel similarity index 97% rename from cargo/remote/BUILD.futures-io-0.3.12.bazel rename to cargo/remote/BUILD.futures-io-0.3.13.bazel index 1497d3265..4c1a25483 100644 --- a/cargo/remote/BUILD.futures-io-0.3.12.bazel +++ b/cargo/remote/BUILD.futures-io-0.3.13.bazel @@ -47,7 +47,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.12", + version = "0.3.13", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.futures-macro-0.3.12.bazel b/cargo/remote/BUILD.futures-macro-0.3.13.bazel similarity index 92% rename from cargo/remote/BUILD.futures-macro-0.3.12.bazel rename to cargo/remote/BUILD.futures-macro-0.3.13.bazel index 09b08ab59..a3fe0cfc4 100644 --- a/cargo/remote/BUILD.futures-macro-0.3.12.bazel +++ b/cargo/remote/BUILD.futures-macro-0.3.13.bazel @@ -49,11 +49,11 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.12", + version = "0.3.13", # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.futures-sink-0.3.12.bazel b/cargo/remote/BUILD.futures-sink-0.3.13.bazel similarity index 97% rename from cargo/remote/BUILD.futures-sink-0.3.12.bazel rename to cargo/remote/BUILD.futures-sink-0.3.13.bazel index 504144575..0de484f3f 100644 --- a/cargo/remote/BUILD.futures-sink-0.3.12.bazel +++ b/cargo/remote/BUILD.futures-sink-0.3.13.bazel @@ -49,7 +49,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.12", + version = "0.3.13", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.futures-task-0.3.12.bazel b/cargo/remote/BUILD.futures-task-0.3.13.bazel similarity index 91% rename from cargo/remote/BUILD.futures-task-0.3.12.bazel rename to cargo/remote/BUILD.futures-task-0.3.13.bazel index ae5334d48..26ae75447 100644 --- a/cargo/remote/BUILD.futures-task-0.3.12.bazel +++ b/cargo/remote/BUILD.futures-task-0.3.13.bazel @@ -35,7 +35,6 @@ rust_library( srcs = glob(["**/*.rs"]), crate_features = [ "alloc", - "once_cell", "std", ], crate_root = "src/lib.rs", @@ -49,9 +48,8 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.12", + version = "0.3.13", # buildifier: leave-alone deps = [ - "@raze__once_cell__1_5_2//:once_cell", ], ) diff --git a/cargo/remote/BUILD.futures-util-0.3.12.bazel b/cargo/remote/BUILD.futures-util-0.3.13.bazel similarity index 80% rename from cargo/remote/BUILD.futures-util-0.3.12.bazel rename to cargo/remote/BUILD.futures-util-0.3.13.bazel index 62f5c217d..c9a1e6e81 100644 --- a/cargo/remote/BUILD.futures-util-0.3.12.bazel +++ b/cargo/remote/BUILD.futures-util-0.3.13.bazel @@ -58,7 +58,7 @@ rust_library( data = [], edition = "2018", proc_macro_deps = [ - "@raze__futures_macro__0_3_12//:futures_macro", + "@raze__futures_macro__0_3_13//:futures_macro", "@raze__proc_macro_hack__0_5_19//:proc_macro_hack", ], rustc_flags = [ @@ -68,16 +68,16 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.12", + version = "0.3.13", # buildifier: leave-alone deps = [ - "@raze__futures_channel__0_3_12//:futures_channel", - "@raze__futures_core__0_3_12//:futures_core", - "@raze__futures_io__0_3_12//:futures_io", - "@raze__futures_sink__0_3_12//:futures_sink", - "@raze__futures_task__0_3_12//:futures_task", + "@raze__futures_channel__0_3_13//:futures_channel", + "@raze__futures_core__0_3_13//:futures_core", + "@raze__futures_io__0_3_13//:futures_io", + "@raze__futures_sink__0_3_13//:futures_sink", + "@raze__futures_task__0_3_13//:futures_task", "@raze__memchr__2_3_4//:memchr", - "@raze__pin_project_lite__0_2_4//:pin_project_lite", + "@raze__pin_project_lite__0_2_6//:pin_project_lite", "@raze__pin_utils__0_1_0//:pin_utils", "@raze__proc_macro_nested__0_1_6//:proc_macro_nested", "@raze__slab__0_4_2//:slab", diff --git a/cargo/remote/BUILD.getrandom-0.1.16.bazel b/cargo/remote/BUILD.getrandom-0.1.16.bazel index 6a37451dc..3c47c7617 100644 --- a/cargo/remote/BUILD.getrandom-0.1.16.bazel +++ b/cargo/remote/BUILD.getrandom-0.1.16.bazel @@ -105,7 +105,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }), diff --git a/cargo/remote/BUILD.getrandom-0.2.2.bazel b/cargo/remote/BUILD.getrandom-0.2.2.bazel index f01a759b8..7a9c8475a 100644 --- a/cargo/remote/BUILD.getrandom-0.2.2.bazel +++ b/cargo/remote/BUILD.getrandom-0.2.2.bazel @@ -105,7 +105,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }), diff --git a/cargo/remote/BUILD.ghost-0.1.2.bazel b/cargo/remote/BUILD.ghost-0.1.2.bazel index 4bbf4e050..002faf26b 100644 --- a/cargo/remote/BUILD.ghost-0.1.2.bazel +++ b/cargo/remote/BUILD.ghost-0.1.2.bazel @@ -50,7 +50,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.h2-0.2.7.bazel b/cargo/remote/BUILD.h2-0.2.7.bazel index d2e779a52..9d0f5454f 100644 --- a/cargo/remote/BUILD.h2-0.2.7.bazel +++ b/cargo/remote/BUILD.h2-0.2.7.bazel @@ -57,15 +57,15 @@ rust_library( deps = [ "@raze__bytes__0_5_6//:bytes", "@raze__fnv__1_0_7//:fnv", - "@raze__futures_core__0_3_12//:futures_core", - "@raze__futures_sink__0_3_12//:futures_sink", - "@raze__futures_util__0_3_12//:futures_util", + "@raze__futures_core__0_3_13//:futures_core", + "@raze__futures_sink__0_3_13//:futures_sink", + "@raze__futures_util__0_3_13//:futures_util", "@raze__http__0_2_3//:http", - "@raze__indexmap__1_6_1//:indexmap", + "@raze__indexmap__1_6_2//:indexmap", "@raze__slab__0_4_2//:slab", "@raze__tokio__0_2_25//:tokio", "@raze__tokio_util__0_3_1//:tokio_util", - "@raze__tracing__0_1_23//:tracing", - "@raze__tracing_futures__0_2_4//:tracing_futures", + "@raze__tracing__0_1_25//:tracing", + "@raze__tracing_futures__0_2_5//:tracing_futures", ], ) diff --git a/cargo/remote/BUILD.hermit-abi-0.1.18.bazel b/cargo/remote/BUILD.hermit-abi-0.1.18.bazel index 8df49d30b..bb0effbf8 100644 --- a/cargo/remote/BUILD.hermit-abi-0.1.18.bazel +++ b/cargo/remote/BUILD.hermit-abi-0.1.18.bazel @@ -50,6 +50,6 @@ rust_library( version = "0.1.18", # buildifier: leave-alone deps = [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], ) diff --git a/cargo/remote/BUILD.hex-0.4.2.bazel b/cargo/remote/BUILD.hex-0.4.3.bazel similarity index 96% rename from cargo/remote/BUILD.hex-0.4.2.bazel rename to cargo/remote/BUILD.hex-0.4.3.bazel index 6e1791aea..2f786e6b7 100644 --- a/cargo/remote/BUILD.hex-0.4.2.bazel +++ b/cargo/remote/BUILD.hex-0.4.3.bazel @@ -36,6 +36,7 @@ rust_library( name = "hex", srcs = glob(["**/*.rs"]), crate_features = [ + "alloc", "default", "std", ], @@ -50,7 +51,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.4.2", + version = "0.4.3", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.html5ever-0.25.1.bazel b/cargo/remote/BUILD.html5ever-0.25.1.bazel index 2c92b60b9..81d6014c4 100644 --- a/cargo/remote/BUILD.html5ever-0.25.1.bazel +++ b/cargo/remote/BUILD.html5ever-0.25.1.bazel @@ -56,8 +56,8 @@ cargo_build_script( visibility = ["//visibility:private"], deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.hyper-0.13.10.bazel b/cargo/remote/BUILD.hyper-0.13.10.bazel index 5c37b74c5..dda5d13bc 100644 --- a/cargo/remote/BUILD.hyper-0.13.10.bazel +++ b/cargo/remote/BUILD.hyper-0.13.10.bazel @@ -96,9 +96,9 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__bytes__0_5_6//:bytes", - "@raze__futures_channel__0_3_12//:futures_channel", - "@raze__futures_core__0_3_12//:futures_core", - "@raze__futures_util__0_3_12//:futures_util", + "@raze__futures_channel__0_3_13//:futures_channel", + "@raze__futures_core__0_3_13//:futures_core", + "@raze__futures_util__0_3_13//:futures_util", "@raze__h2__0_2_7//:h2", "@raze__http__0_2_3//:http", "@raze__http_body__0_3_1//:http_body", @@ -109,7 +109,7 @@ rust_library( "@raze__socket2__0_3_19//:socket2", "@raze__tokio__0_2_25//:tokio", "@raze__tower_service__0_3_1//:tower_service", - "@raze__tracing__0_1_23//:tracing", + "@raze__tracing__0_1_25//:tracing", "@raze__want__0_3_0//:want", ] + selects.with_or({ # cfg(any(target_os = "linux", target_os = "macos")) diff --git a/cargo/remote/BUILD.hyper-rustls-0.21.0.bazel b/cargo/remote/BUILD.hyper-rustls-0.21.0.bazel index 7040a0e4f..1e274617b 100644 --- a/cargo/remote/BUILD.hyper-rustls-0.21.0.bazel +++ b/cargo/remote/BUILD.hyper-rustls-0.21.0.bazel @@ -54,7 +54,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__bytes__0_5_6//:bytes", - "@raze__futures_util__0_3_12//:futures_util", + "@raze__futures_util__0_3_13//:futures_util", "@raze__hyper__0_13_10//:hyper", "@raze__log__0_4_14//:log", "@raze__rustls__0_18_1//:rustls", diff --git a/cargo/remote/BUILD.idna-0.2.1.bazel b/cargo/remote/BUILD.idna-0.2.2.bazel similarity index 93% rename from cargo/remote/BUILD.idna-0.2.1.bazel rename to cargo/remote/BUILD.idna-0.2.2.bazel index 2bef76519..576b1694e 100644 --- a/cargo/remote/BUILD.idna-0.2.1.bazel +++ b/cargo/remote/BUILD.idna-0.2.2.bazel @@ -48,12 +48,12 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.1", + version = "0.2.2", # buildifier: leave-alone deps = [ "@raze__matches__0_1_8//:matches", "@raze__unicode_bidi__0_3_4//:unicode_bidi", - "@raze__unicode_normalization__0_1_16//:unicode_normalization", + "@raze__unicode_normalization__0_1_17//:unicode_normalization", ], ) diff --git a/cargo/remote/BUILD.indexmap-1.6.1.bazel b/cargo/remote/BUILD.indexmap-1.6.2.bazel similarity index 97% rename from cargo/remote/BUILD.indexmap-1.6.1.bazel rename to cargo/remote/BUILD.indexmap-1.6.2.bazel index 7de84d18b..2d77ba2d1 100644 --- a/cargo/remote/BUILD.indexmap-1.6.1.bazel +++ b/cargo/remote/BUILD.indexmap-1.6.2.bazel @@ -52,7 +52,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "1.6.1", + version = "1.6.2", visibility = ["//visibility:private"], deps = [ "@raze__autocfg__1_0_1//:autocfg", @@ -79,7 +79,7 @@ rust_library( "cargo-raze", "manual", ], - version = "1.6.1", + version = "1.6.2", # buildifier: leave-alone deps = [ ":indexmap_build_script", diff --git a/cargo/remote/BUILD.indoc-0.3.6.bazel b/cargo/remote/BUILD.indoc-0.3.6.bazel new file mode 100644 index 000000000..0f9297e44 --- /dev/null +++ b/cargo/remote/BUILD.indoc-0.3.6.bazel @@ -0,0 +1,61 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_binary", + "rust_library", + "rust_test", +) + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets + +rust_library( + name = "indoc", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "lib", + data = [], + edition = "2018", + proc_macro_deps = [ + "@raze__indoc_impl__0_3_6//:indoc_impl", + "@raze__proc_macro_hack__0_5_19//:proc_macro_hack", + ], + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.3.6", + # buildifier: leave-alone + deps = [ + ], +) + +# Unsupported target "compiletest" with type "test" omitted + +# Unsupported target "run-pass" with type "test" omitted diff --git a/cargo/remote/BUILD.indoc-1.0.3.bazel b/cargo/remote/BUILD.indoc-1.0.3.bazel deleted file mode 100644 index 0d104434c..000000000 --- a/cargo/remote/BUILD.indoc-1.0.3.bazel +++ /dev/null @@ -1,64 +0,0 @@ -""" -@generated -cargo-raze crate build file. - -DO NOT EDIT! Replaced on runs of cargo-raze -""" - -# buildifier: disable=load -load( - "@io_bazel_rules_rust//rust:rust.bzl", - "rust_binary", - "rust_library", - "rust_test", -) - -# buildifier: disable=load -load("@bazel_skylib//lib:selects.bzl", "selects") - -package(default_visibility = [ - # Public for visibility by "@raze__crate__version//" targets. - # - # Prefer access through "//cargo", which limits external - # visibility to explicit Cargo.toml dependencies. - "//visibility:public", -]) - -licenses([ - "notice", # MIT from expression "MIT OR Apache-2.0" -]) - -# Generated Targets - -rust_library( - name = "indoc", - srcs = glob(["**/*.rs"]), - crate_features = [ - ], - crate_root = "src/lib.rs", - crate_type = "proc-macro", - data = [], - edition = "2018", - rustc_flags = [ - "--cap-lints=allow", - ], - tags = [ - "cargo-raze", - "manual", - ], - version = "1.0.3", - # buildifier: leave-alone - deps = [ - "@raze__unindent__0_1_7//:unindent", - ], -) - -# Unsupported target "compiletest" with type "test" omitted - -# Unsupported target "test_formatdoc" with type "test" omitted - -# Unsupported target "test_indoc" with type "test" omitted - -# Unsupported target "test_unindent" with type "test" omitted - -# Unsupported target "test_writedoc" with type "test" omitted diff --git a/cargo/remote/BUILD.indoc-impl-0.3.6.bazel b/cargo/remote/BUILD.indoc-impl-0.3.6.bazel new file mode 100644 index 000000000..6959c505d --- /dev/null +++ b/cargo/remote/BUILD.indoc-impl-0.3.6.bazel @@ -0,0 +1,60 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_binary", + "rust_library", + "rust_test", +) + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets + +rust_library( + name = "indoc_impl", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "proc-macro", + data = [], + edition = "2018", + proc_macro_deps = [ + "@raze__proc_macro_hack__0_5_19//:proc_macro_hack", + ], + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.3.6", + # buildifier: leave-alone + deps = [ + "@raze__proc_macro2__1_0_24//:proc_macro2", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", + "@raze__unindent__0_1_7//:unindent", + ], +) diff --git a/cargo/remote/BUILD.inventory-impl-0.1.10.bazel b/cargo/remote/BUILD.inventory-impl-0.1.10.bazel index c2d1be447..b60c035cd 100644 --- a/cargo/remote/BUILD.inventory-impl-0.1.10.bazel +++ b/cargo/remote/BUILD.inventory-impl-0.1.10.bazel @@ -50,7 +50,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.iovec-0.1.4.bazel b/cargo/remote/BUILD.iovec-0.1.4.bazel index 4bf22f16e..e827d81c2 100644 --- a/cargo/remote/BUILD.iovec-0.1.4.bazel +++ b/cargo/remote/BUILD.iovec-0.1.4.bazel @@ -60,7 +60,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }), diff --git a/cargo/remote/BUILD.js-sys-0.3.47.bazel b/cargo/remote/BUILD.js-sys-0.3.48.bazel similarity index 93% rename from cargo/remote/BUILD.js-sys-0.3.47.bazel rename to cargo/remote/BUILD.js-sys-0.3.48.bazel index ccc7e3455..b4b4edb2e 100644 --- a/cargo/remote/BUILD.js-sys-0.3.47.bazel +++ b/cargo/remote/BUILD.js-sys-0.3.48.bazel @@ -46,10 +46,10 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.47", + version = "0.3.48", # buildifier: leave-alone deps = [ - "@raze__wasm_bindgen__0_2_70//:wasm_bindgen", + "@raze__wasm_bindgen__0_2_71//:wasm_bindgen", ], ) diff --git a/cargo/remote/BUILD.lexical-core-0.7.4.bazel b/cargo/remote/BUILD.lexical-core-0.7.5.bazel similarity index 95% rename from cargo/remote/BUILD.lexical-core-0.7.4.bazel rename to cargo/remote/BUILD.lexical-core-0.7.5.bazel index 7e676b99f..95c9ccd21 100644 --- a/cargo/remote/BUILD.lexical-core-0.7.4.bazel +++ b/cargo/remote/BUILD.lexical-core-0.7.5.bazel @@ -59,7 +59,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "0.7.4", + version = "0.7.5", visibility = ["//visibility:private"], deps = [ ], @@ -88,13 +88,13 @@ rust_library( "cargo-raze", "manual", ], - version = "0.7.4", + version = "0.7.5", # buildifier: leave-alone deps = [ ":lexical_core_build_script", "@raze__arrayvec__0_5_2//:arrayvec", "@raze__bitflags__1_2_1//:bitflags", - "@raze__cfg_if__0_1_10//:cfg_if", + "@raze__cfg_if__1_0_0//:cfg_if", "@raze__ryu__1_0_5//:ryu", "@raze__static_assertions__1_1_0//:static_assertions", ], diff --git a/cargo/remote/BUILD.libc-0.2.85.bazel b/cargo/remote/BUILD.libc-0.2.88.bazel similarity index 97% rename from cargo/remote/BUILD.libc-0.2.85.bazel rename to cargo/remote/BUILD.libc-0.2.88.bazel index 2f1ebf051..e6756eec3 100644 --- a/cargo/remote/BUILD.libc-0.2.85.bazel +++ b/cargo/remote/BUILD.libc-0.2.88.bazel @@ -55,7 +55,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "0.2.85", + version = "0.2.88", visibility = ["//visibility:private"], deps = [ ], @@ -80,7 +80,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.85", + version = "0.2.88", # buildifier: leave-alone deps = [ ":libc_build_script", diff --git a/cargo/remote/BUILD.libsqlite3-sys-0.20.1.bazel b/cargo/remote/BUILD.libsqlite3-sys-0.20.1.bazel index c69ff7e39..73f44f5da 100644 --- a/cargo/remote/BUILD.libsqlite3-sys-0.20.1.bazel +++ b/cargo/remote/BUILD.libsqlite3-sys-0.20.1.bazel @@ -64,7 +64,7 @@ cargo_build_script( version = "0.20.1", visibility = ["//visibility:private"], deps = [ - "@raze__cc__1_0_66//:cc", + "@raze__cc__1_0_67//:cc", "@raze__pkg_config__0_3_19//:pkg_config", ] + selects.with_or({ # cfg(target_env = "msvc") diff --git a/cargo/remote/BUILD.markup5ever-0.10.0.bazel b/cargo/remote/BUILD.markup5ever-0.10.0.bazel index 0cdc866d3..b0d71d04e 100644 --- a/cargo/remote/BUILD.markup5ever-0.10.0.bazel +++ b/cargo/remote/BUILD.markup5ever-0.10.0.bazel @@ -46,7 +46,7 @@ cargo_build_script( data = glob(["**"]), edition = "2018", proc_macro_deps = [ - "@raze__serde_derive__1_0_123//:serde_derive", + "@raze__serde_derive__1_0_124//:serde_derive", ], rustc_flags = [ "--cap-lints=allow", @@ -59,8 +59,8 @@ cargo_build_script( visibility = ["//visibility:private"], deps = [ "@raze__phf_codegen__0_8_0//:phf_codegen", - "@raze__serde__1_0_123//:serde", - "@raze__serde_json__1_0_62//:serde_json", + "@raze__serde__1_0_124//:serde", + "@raze__serde_json__1_0_64//:serde_json", "@raze__string_cache_codegen__0_5_1//:string_cache_codegen", ], ) diff --git a/cargo/remote/BUILD.miniz_oxide-0.4.3.bazel b/cargo/remote/BUILD.miniz_oxide-0.4.4.bazel similarity index 95% rename from cargo/remote/BUILD.miniz_oxide-0.4.3.bazel rename to cargo/remote/BUILD.miniz_oxide-0.4.4.bazel index 98839c25e..f029bbc63 100644 --- a/cargo/remote/BUILD.miniz_oxide-0.4.3.bazel +++ b/cargo/remote/BUILD.miniz_oxide-0.4.4.bazel @@ -52,7 +52,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "0.4.3", + version = "0.4.4", visibility = ["//visibility:private"], deps = [ "@raze__autocfg__1_0_1//:autocfg", @@ -75,10 +75,10 @@ rust_library( "cargo-raze", "manual", ], - version = "0.4.3", + version = "0.4.4", # buildifier: leave-alone deps = [ ":miniz_oxide_build_script", - "@raze__adler__0_2_3//:adler", + "@raze__adler__1_0_2//:adler", ], ) diff --git a/cargo/remote/BUILD.mio-0.6.23.bazel b/cargo/remote/BUILD.mio-0.6.23.bazel index 085f5ea0a..7f97a7e76 100644 --- a/cargo/remote/BUILD.mio-0.6.23.bazel +++ b/cargo/remote/BUILD.mio-0.6.23.bazel @@ -67,7 +67,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }) + selects.with_or({ diff --git a/cargo/remote/BUILD.native-tls-0.2.7.bazel b/cargo/remote/BUILD.native-tls-0.2.7.bazel index 54c6ec7ad..6a1937b99 100644 --- a/cargo/remote/BUILD.native-tls-0.2.7.bazel +++ b/cargo/remote/BUILD.native-tls-0.2.7.bazel @@ -62,7 +62,7 @@ cargo_build_script( "@io_bazel_rules_rust//rust/platform:x86_64-apple-darwin", "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", ): [ - "@raze__security_framework_sys__2_0_0//:security_framework_sys", + "@raze__security_framework_sys__2_1_1//:security_framework_sys", ], "//conditions:default": [], }) + selects.with_or({ @@ -118,9 +118,9 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", ): [ "@raze__lazy_static__1_4_0//:lazy_static", - "@raze__libc__0_2_85//:libc", - "@raze__security_framework__2_0_0//:security_framework", - "@raze__security_framework_sys__2_0_0//:security_framework_sys", + "@raze__libc__0_2_88//:libc", + "@raze__security_framework__2_1_2//:security_framework", + "@raze__security_framework_sys__2_1_1//:security_framework_sys", "@raze__tempfile__3_2_0//:tempfile", ], "//conditions:default": [], diff --git a/cargo/remote/BUILD.net2-0.2.37.bazel b/cargo/remote/BUILD.net2-0.2.37.bazel index 5620e111f..61ee6371d 100644 --- a/cargo/remote/BUILD.net2-0.2.37.bazel +++ b/cargo/remote/BUILD.net2-0.2.37.bazel @@ -63,7 +63,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }) + selects.with_or({ diff --git a/cargo/remote/BUILD.nom-6.1.0.bazel b/cargo/remote/BUILD.nom-6.1.2.bazel similarity index 94% rename from cargo/remote/BUILD.nom-6.1.0.bazel rename to cargo/remote/BUILD.nom-6.1.2.bazel index c1c18d44b..55675bc96 100644 --- a/cargo/remote/BUILD.nom-6.1.0.bazel +++ b/cargo/remote/BUILD.nom-6.1.2.bazel @@ -44,6 +44,7 @@ cargo_build_script( "alloc", "bitvec", "default", + "funty", "lexical", "lexical-core", "std", @@ -58,7 +59,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "6.1.0", + version = "6.1.2", visibility = ["//visibility:private"], deps = [ "@raze__version_check__0_9_2//:version_check", @@ -92,6 +93,7 @@ rust_library( "alloc", "bitvec", "default", + "funty", "lexical", "lexical-core", "std", @@ -107,12 +109,13 @@ rust_library( "cargo-raze", "manual", ], - version = "6.1.0", + version = "6.1.2", # buildifier: leave-alone deps = [ ":nom_build_script", - "@raze__bitvec__0_19_4//:bitvec", - "@raze__lexical_core__0_7_4//:lexical_core", + "@raze__bitvec__0_19_5//:bitvec", + "@raze__funty__1_1_0//:funty", + "@raze__lexical_core__0_7_5//:lexical_core", "@raze__memchr__2_3_4//:memchr", ], ) diff --git a/cargo/remote/BUILD.num_cpus-1.13.0.bazel b/cargo/remote/BUILD.num_cpus-1.13.0.bazel index 4c0712fa7..eef520870 100644 --- a/cargo/remote/BUILD.num_cpus-1.13.0.bazel +++ b/cargo/remote/BUILD.num_cpus-1.13.0.bazel @@ -51,6 +51,6 @@ rust_library( version = "1.13.0", # buildifier: leave-alone deps = [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], ) diff --git a/cargo/remote/BUILD.num_enum_derive-0.5.1.bazel b/cargo/remote/BUILD.num_enum_derive-0.5.1.bazel index c063f933c..40b5f74df 100644 --- a/cargo/remote/BUILD.num_enum_derive-0.5.1.bazel +++ b/cargo/remote/BUILD.num_enum_derive-0.5.1.bazel @@ -53,7 +53,7 @@ rust_library( deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", "@raze__proc_macro_crate__0_1_5//:proc_macro_crate", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.once_cell-1.5.2.bazel b/cargo/remote/BUILD.once_cell-1.7.2.bazel similarity index 97% rename from cargo/remote/BUILD.once_cell-1.5.2.bazel rename to cargo/remote/BUILD.once_cell-1.7.2.bazel index 6478fcdae..89cc451a3 100644 --- a/cargo/remote/BUILD.once_cell-1.5.2.bazel +++ b/cargo/remote/BUILD.once_cell-1.7.2.bazel @@ -50,6 +50,7 @@ rust_library( crate_features = [ "alloc", "default", + "race", "std", ], crate_root = "src/lib.rs", @@ -63,7 +64,7 @@ rust_library( "cargo-raze", "manual", ], - version = "1.5.2", + version = "1.7.2", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.openssl-0.10.32.bazel b/cargo/remote/BUILD.openssl-0.10.32.bazel index b9f308397..3d0e3e1a7 100644 --- a/cargo/remote/BUILD.openssl-0.10.32.bazel +++ b/cargo/remote/BUILD.openssl-0.10.32.bazel @@ -85,7 +85,7 @@ rust_library( "@raze__cfg_if__1_0_0//:cfg_if", "@raze__foreign_types__0_3_2//:foreign_types", "@raze__lazy_static__1_4_0//:lazy_static", - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", "@raze__openssl_sys__0_9_60//:openssl_sys", ], ) diff --git a/cargo/remote/BUILD.openssl-sys-0.9.60.bazel b/cargo/remote/BUILD.openssl-sys-0.9.60.bazel index d00d4cea5..c0cc140e6 100644 --- a/cargo/remote/BUILD.openssl-sys-0.9.60.bazel +++ b/cargo/remote/BUILD.openssl-sys-0.9.60.bazel @@ -56,7 +56,7 @@ cargo_build_script( visibility = ["//visibility:private"], deps = [ "@raze__autocfg__1_0_1//:autocfg", - "@raze__cc__1_0_66//:cc", + "@raze__cc__1_0_67//:cc", "@raze__pkg_config__0_3_19//:pkg_config", ] + selects.with_or({ # cfg(target_env = "msvc") @@ -91,7 +91,7 @@ rust_library( # buildifier: leave-alone deps = [ ":openssl_sys_build_script", - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ] + selects.with_or({ # cfg(target_env = "msvc") ( diff --git a/cargo/remote/BUILD.parking_lot-0.11.1.bazel b/cargo/remote/BUILD.parking_lot-0.11.1.bazel index 89ea3c351..70a14bdf7 100644 --- a/cargo/remote/BUILD.parking_lot-0.11.1.bazel +++ b/cargo/remote/BUILD.parking_lot-0.11.1.bazel @@ -52,7 +52,7 @@ rust_library( deps = [ "@raze__instant__0_1_9//:instant", "@raze__lock_api__0_4_2//:lock_api", - "@raze__parking_lot_core__0_8_2//:parking_lot_core", + "@raze__parking_lot_core__0_8_3//:parking_lot_core", ], ) diff --git a/cargo/remote/BUILD.parking_lot_core-0.8.2.bazel b/cargo/remote/BUILD.parking_lot_core-0.8.3.bazel similarity index 96% rename from cargo/remote/BUILD.parking_lot_core-0.8.2.bazel rename to cargo/remote/BUILD.parking_lot_core-0.8.3.bazel index 009cdef48..7cca48dec 100644 --- a/cargo/remote/BUILD.parking_lot_core-0.8.2.bazel +++ b/cargo/remote/BUILD.parking_lot_core-0.8.3.bazel @@ -48,7 +48,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.8.2", + version = "0.8.3", # buildifier: leave-alone deps = [ "@raze__cfg_if__1_0_0//:cfg_if", @@ -63,7 +63,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }) + selects.with_or({ diff --git a/cargo/remote/BUILD.async-trait-0.1.42.bazel b/cargo/remote/BUILD.paste-0.1.18.bazel similarity index 83% rename from cargo/remote/BUILD.async-trait-0.1.42.bazel rename to cargo/remote/BUILD.paste-0.1.18.bazel index 2bfe61d20..2b8da073f 100644 --- a/cargo/remote/BUILD.async-trait-0.1.42.bazel +++ b/cargo/remote/BUILD.paste-0.1.18.bazel @@ -31,14 +31,18 @@ licenses([ # Generated Targets rust_library( - name = "async_trait", + name = "paste", srcs = glob(["**/*.rs"]), crate_features = [ ], crate_root = "src/lib.rs", - crate_type = "proc-macro", + crate_type = "lib", data = [], edition = "2018", + proc_macro_deps = [ + "@raze__paste_impl__0_1_18//:paste_impl", + "@raze__proc_macro_hack__0_5_19//:proc_macro_hack", + ], rustc_flags = [ "--cap-lints=allow", ], @@ -46,12 +50,9 @@ rust_library( "cargo-raze", "manual", ], - version = "0.1.42", + version = "0.1.18", # buildifier: leave-alone deps = [ - "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", ], ) diff --git a/cargo/remote/BUILD.paste-1.0.4.bazel b/cargo/remote/BUILD.paste-1.0.4.bazel deleted file mode 100644 index 5b1d715a7..000000000 --- a/cargo/remote/BUILD.paste-1.0.4.bazel +++ /dev/null @@ -1,63 +0,0 @@ -""" -@generated -cargo-raze crate build file. - -DO NOT EDIT! Replaced on runs of cargo-raze -""" - -# buildifier: disable=load -load( - "@io_bazel_rules_rust//rust:rust.bzl", - "rust_binary", - "rust_library", - "rust_test", -) - -# buildifier: disable=load -load("@bazel_skylib//lib:selects.bzl", "selects") - -package(default_visibility = [ - # Public for visibility by "@raze__crate__version//" targets. - # - # Prefer access through "//cargo", which limits external - # visibility to explicit Cargo.toml dependencies. - "//visibility:public", -]) - -licenses([ - "notice", # MIT from expression "MIT OR Apache-2.0" -]) - -# Generated Targets - -rust_library( - name = "paste", - srcs = glob(["**/*.rs"]), - crate_features = [ - ], - crate_root = "src/lib.rs", - crate_type = "proc-macro", - data = [], - edition = "2018", - rustc_flags = [ - "--cap-lints=allow", - ], - tags = [ - "cargo-raze", - "manual", - ], - version = "1.0.4", - # buildifier: leave-alone - deps = [ - ], -) - -# Unsupported target "compiletest" with type "test" omitted - -# Unsupported target "test_attr" with type "test" omitted - -# Unsupported target "test_doc" with type "test" omitted - -# Unsupported target "test_expr" with type "test" omitted - -# Unsupported target "test_item" with type "test" omitted diff --git a/cargo/remote/BUILD.paste-impl-0.1.18.bazel b/cargo/remote/BUILD.paste-impl-0.1.18.bazel new file mode 100644 index 000000000..8391d05d8 --- /dev/null +++ b/cargo/remote/BUILD.paste-impl-0.1.18.bazel @@ -0,0 +1,56 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_binary", + "rust_library", + "rust_test", +) + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT OR Apache-2.0" +]) + +# Generated Targets + +rust_library( + name = "paste_impl", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "proc-macro", + data = [], + edition = "2018", + proc_macro_deps = [ + "@raze__proc_macro_hack__0_5_19//:proc_macro_hack", + ], + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.1.18", + # buildifier: leave-alone + deps = [ + ], +) diff --git a/cargo/remote/BUILD.petgraph-0.5.1.bazel b/cargo/remote/BUILD.petgraph-0.5.1.bazel index a73220d88..8aea62d46 100644 --- a/cargo/remote/BUILD.petgraph-0.5.1.bazel +++ b/cargo/remote/BUILD.petgraph-0.5.1.bazel @@ -62,7 +62,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__fixedbitset__0_2_0//:fixedbitset", - "@raze__indexmap__1_6_1//:indexmap", + "@raze__indexmap__1_6_2//:indexmap", ], ) diff --git a/cargo/remote/BUILD.pin-project-0.4.27.bazel b/cargo/remote/BUILD.pin-project-0.4.27.bazel deleted file mode 100644 index e67b0cdc3..000000000 --- a/cargo/remote/BUILD.pin-project-0.4.27.bazel +++ /dev/null @@ -1,104 +0,0 @@ -""" -@generated -cargo-raze crate build file. - -DO NOT EDIT! Replaced on runs of cargo-raze -""" - -# buildifier: disable=load -load( - "@io_bazel_rules_rust//rust:rust.bzl", - "rust_binary", - "rust_library", - "rust_test", -) - -# buildifier: disable=load -load("@bazel_skylib//lib:selects.bzl", "selects") - -package(default_visibility = [ - # Public for visibility by "@raze__crate__version//" targets. - # - # Prefer access through "//cargo", which limits external - # visibility to explicit Cargo.toml dependencies. - "//visibility:public", -]) - -licenses([ - "notice", # Apache-2.0 from expression "Apache-2.0 OR MIT" -]) - -# Generated Targets - -# Unsupported target "enum-default" with type "example" omitted - -# Unsupported target "enum-default-expanded" with type "example" omitted - -# Unsupported target "not_unpin" with type "example" omitted - -# Unsupported target "not_unpin-expanded" with type "example" omitted - -# Unsupported target "pinned_drop" with type "example" omitted - -# Unsupported target "pinned_drop-expanded" with type "example" omitted - -# Unsupported target "project_replace" with type "example" omitted - -# Unsupported target "project_replace-expanded" with type "example" omitted - -# Unsupported target "struct-default" with type "example" omitted - -# Unsupported target "struct-default-expanded" with type "example" omitted - -# Unsupported target "unsafe_unpin" with type "example" omitted - -# Unsupported target "unsafe_unpin-expanded" with type "example" omitted - -rust_library( - name = "pin_project", - srcs = glob(["**/*.rs"]), - crate_features = [ - ], - crate_root = "src/lib.rs", - crate_type = "lib", - data = [], - edition = "2018", - proc_macro_deps = [ - "@raze__pin_project_internal__0_4_27//:pin_project_internal", - ], - rustc_flags = [ - "--cap-lints=allow", - ], - tags = [ - "cargo-raze", - "manual", - ], - version = "0.4.27", - # buildifier: leave-alone - deps = [ - ], -) - -# Unsupported target "cfg" with type "test" omitted - -# Unsupported target "compiletest" with type "test" omitted - -# Unsupported target "drop_order" with type "test" omitted - -# Unsupported target "lint" with type "test" omitted - -# Unsupported target "pin_project" with type "test" omitted - -# Unsupported target "pinned_drop" with type "test" omitted - -# Unsupported target "project" with type "test" omitted - -# Unsupported target "project_ref" with type "test" omitted - -# Unsupported target "project_replace" with type "test" omitted - -# Unsupported target "repr_packed" with type "test" omitted - -# Unsupported target "sized" with type "test" omitted - -# Unsupported target "unsafe_unpin" with type "test" omitted diff --git a/cargo/remote/BUILD.pin-project-internal-1.0.5.bazel b/cargo/remote/BUILD.pin-project-internal-1.0.5.bazel index 081faad76..0c9322045 100644 --- a/cargo/remote/BUILD.pin-project-internal-1.0.5.bazel +++ b/cargo/remote/BUILD.pin-project-internal-1.0.5.bazel @@ -50,7 +50,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.pin-project-lite-0.1.11.bazel b/cargo/remote/BUILD.pin-project-lite-0.1.12.bazel similarity index 98% rename from cargo/remote/BUILD.pin-project-lite-0.1.11.bazel rename to cargo/remote/BUILD.pin-project-lite-0.1.12.bazel index 2a9333c4f..2cc236e31 100644 --- a/cargo/remote/BUILD.pin-project-lite-0.1.11.bazel +++ b/cargo/remote/BUILD.pin-project-lite-0.1.12.bazel @@ -46,7 +46,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.1.11", + version = "0.1.12", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.pin-project-lite-0.2.4.bazel b/cargo/remote/BUILD.pin-project-lite-0.2.6.bazel similarity index 94% rename from cargo/remote/BUILD.pin-project-lite-0.2.4.bazel rename to cargo/remote/BUILD.pin-project-lite-0.2.6.bazel index aa6586ee0..566e3e5fd 100644 --- a/cargo/remote/BUILD.pin-project-lite-0.2.4.bazel +++ b/cargo/remote/BUILD.pin-project-lite-0.2.6.bazel @@ -46,7 +46,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.4", + version = "0.2.6", # buildifier: leave-alone deps = [ ], @@ -56,6 +56,8 @@ rust_library( # Unsupported target "drop_order" with type "test" omitted +# Unsupported target "expandtest" with type "test" omitted + # Unsupported target "lint" with type "test" omitted # Unsupported target "proper_unpin" with type "test" omitted diff --git a/cargo/remote/BUILD.prost-derive-0.7.0.bazel b/cargo/remote/BUILD.prost-derive-0.7.0.bazel index e31bb9f9a..be1273cd4 100644 --- a/cargo/remote/BUILD.prost-derive-0.7.0.bazel +++ b/cargo/remote/BUILD.prost-derive-0.7.0.bazel @@ -52,7 +52,7 @@ rust_library( "@raze__anyhow__1_0_38//:anyhow", "@raze__itertools__0_9_0//:itertools", "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.pyo3-0.13.1.bazel b/cargo/remote/BUILD.pyo3-0.13.2.bazel similarity index 94% rename from cargo/remote/BUILD.pyo3-0.13.1.bazel rename to cargo/remote/BUILD.pyo3-0.13.2.bazel index a6b178b16..86bd5c842 100644 --- a/cargo/remote/BUILD.pyo3-0.13.1.bazel +++ b/cargo/remote/BUILD.pyo3-0.13.2.bazel @@ -63,7 +63,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "0.13.1", + version = "0.13.2", visibility = ["//visibility:private"], deps = [ ], @@ -104,9 +104,7 @@ rust_library( edition = "2018", proc_macro_deps = [ "@raze__ctor__0_1_19//:ctor", - "@raze__indoc__1_0_3//:indoc", - "@raze__paste__1_0_4//:paste", - "@raze__pyo3_macros__0_13_1//:pyo3_macros", + "@raze__pyo3_macros__0_13_2//:pyo3_macros", ], rustc_flags = [ "--cap-lints=allow", @@ -115,14 +113,16 @@ rust_library( "cargo-raze", "manual", ], - version = "0.13.1", + version = "0.13.2", # buildifier: leave-alone deps = [ ":pyo3_build_script", "@raze__cfg_if__1_0_0//:cfg_if", + "@raze__indoc__0_3_6//:indoc", "@raze__inventory__0_1_10//:inventory", - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", "@raze__parking_lot__0_11_1//:parking_lot", + "@raze__paste__0_1_18//:paste", "@raze__unindent__0_1_7//:unindent", ], ) @@ -173,6 +173,8 @@ rust_library( # Unsupported target "test_sequence" with type "test" omitted +# Unsupported target "test_serde" with type "test" omitted + # Unsupported target "test_string" with type "test" omitted # Unsupported target "test_text_signature" with type "test" omitted diff --git a/cargo/remote/BUILD.pyo3-macros-0.13.1.bazel b/cargo/remote/BUILD.pyo3-macros-0.13.2.bazel similarity index 86% rename from cargo/remote/BUILD.pyo3-macros-0.13.1.bazel rename to cargo/remote/BUILD.pyo3-macros-0.13.2.bazel index b8ecfa062..1b5b6222f 100644 --- a/cargo/remote/BUILD.pyo3-macros-0.13.1.bazel +++ b/cargo/remote/BUILD.pyo3-macros-0.13.2.bazel @@ -46,11 +46,11 @@ rust_library( "cargo-raze", "manual", ], - version = "0.13.1", + version = "0.13.2", # buildifier: leave-alone deps = [ - "@raze__pyo3_macros_backend__0_13_1//:pyo3_macros_backend", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__pyo3_macros_backend__0_13_2//:pyo3_macros_backend", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.pyo3-macros-backend-0.13.1.bazel b/cargo/remote/BUILD.pyo3-macros-backend-0.13.2.bazel similarity index 91% rename from cargo/remote/BUILD.pyo3-macros-backend-0.13.1.bazel rename to cargo/remote/BUILD.pyo3-macros-backend-0.13.2.bazel index 1772792d3..401694a0d 100644 --- a/cargo/remote/BUILD.pyo3-macros-backend-0.13.1.bazel +++ b/cargo/remote/BUILD.pyo3-macros-backend-0.13.2.bazel @@ -46,11 +46,11 @@ rust_library( "cargo-raze", "manual", ], - version = "0.13.1", + version = "0.13.2", # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.quote-1.0.8.bazel b/cargo/remote/BUILD.quote-1.0.9.bazel similarity index 98% rename from cargo/remote/BUILD.quote-1.0.8.bazel rename to cargo/remote/BUILD.quote-1.0.9.bazel index a3018c978..b97597b72 100644 --- a/cargo/remote/BUILD.quote-1.0.8.bazel +++ b/cargo/remote/BUILD.quote-1.0.9.bazel @@ -48,7 +48,7 @@ rust_library( "cargo-raze", "manual", ], - version = "1.0.8", + version = "1.0.9", # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", diff --git a/cargo/remote/BUILD.rand-0.7.3.bazel b/cargo/remote/BUILD.rand-0.7.3.bazel index f85b51f7f..d9666337b 100644 --- a/cargo/remote/BUILD.rand-0.7.3.bazel +++ b/cargo/remote/BUILD.rand-0.7.3.bazel @@ -85,7 +85,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }), diff --git a/cargo/remote/BUILD.rand-0.8.3.bazel b/cargo/remote/BUILD.rand-0.8.3.bazel index 2ea0366ff..4f510be35 100644 --- a/cargo/remote/BUILD.rand-0.8.3.bazel +++ b/cargo/remote/BUILD.rand-0.8.3.bazel @@ -60,7 +60,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__rand_chacha__0_3_0//:rand_chacha", - "@raze__rand_core__0_6_1//:rand_core", + "@raze__rand_core__0_6_2//:rand_core", ] + selects.with_or({ # cfg(unix) ( @@ -70,7 +70,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }), diff --git a/cargo/remote/BUILD.rand_chacha-0.3.0.bazel b/cargo/remote/BUILD.rand_chacha-0.3.0.bazel index dac6120eb..63b1402c9 100644 --- a/cargo/remote/BUILD.rand_chacha-0.3.0.bazel +++ b/cargo/remote/BUILD.rand_chacha-0.3.0.bazel @@ -51,6 +51,6 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__ppv_lite86__0_2_10//:ppv_lite86", - "@raze__rand_core__0_6_1//:rand_core", + "@raze__rand_core__0_6_2//:rand_core", ], ) diff --git a/cargo/remote/BUILD.rand_core-0.6.1.bazel b/cargo/remote/BUILD.rand_core-0.6.2.bazel similarity index 97% rename from cargo/remote/BUILD.rand_core-0.6.1.bazel rename to cargo/remote/BUILD.rand_core-0.6.2.bazel index 3c94e9bc4..ba9c758d0 100644 --- a/cargo/remote/BUILD.rand_core-0.6.1.bazel +++ b/cargo/remote/BUILD.rand_core-0.6.2.bazel @@ -49,7 +49,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.6.1", + version = "0.6.2", # buildifier: leave-alone deps = [ "@raze__getrandom__0_2_2//:getrandom", diff --git a/cargo/remote/BUILD.rand_hc-0.3.0.bazel b/cargo/remote/BUILD.rand_hc-0.3.0.bazel index e0e61d0f6..2425bf4b9 100644 --- a/cargo/remote/BUILD.rand_hc-0.3.0.bazel +++ b/cargo/remote/BUILD.rand_hc-0.3.0.bazel @@ -49,6 +49,6 @@ rust_library( version = "0.3.0", # buildifier: leave-alone deps = [ - "@raze__rand_core__0_6_1//:rand_core", + "@raze__rand_core__0_6_2//:rand_core", ], ) diff --git a/cargo/remote/BUILD.redox_syscall-0.2.4.bazel b/cargo/remote/BUILD.redox_syscall-0.2.5.bazel similarity index 98% rename from cargo/remote/BUILD.redox_syscall-0.2.4.bazel rename to cargo/remote/BUILD.redox_syscall-0.2.5.bazel index 42d4ca7d5..2ced2b5cc 100644 --- a/cargo/remote/BUILD.redox_syscall-0.2.4.bazel +++ b/cargo/remote/BUILD.redox_syscall-0.2.5.bazel @@ -55,7 +55,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.4", + version = "0.2.5", # buildifier: leave-alone deps = [ "@raze__bitflags__1_2_1//:bitflags", diff --git a/cargo/remote/BUILD.rental-impl-0.5.5.bazel b/cargo/remote/BUILD.rental-impl-0.5.5.bazel index daf3e2f1a..79e5ba755 100644 --- a/cargo/remote/BUILD.rental-impl-0.5.5.bazel +++ b/cargo/remote/BUILD.rental-impl-0.5.5.bazel @@ -50,7 +50,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.reqwest-0.10.8.bazel b/cargo/remote/BUILD.reqwest-0.10.8.bazel index 2dde7ed32..82c7dc4f2 100644 --- a/cargo/remote/BUILD.reqwest-0.10.8.bazel +++ b/cargo/remote/BUILD.reqwest-0.10.8.bazel @@ -86,10 +86,10 @@ rust_library( "@raze__http__0_2_3//:http", "@raze__hyper_timeout__0_3_1//:hyper_timeout", "@raze__mime_guess__2_0_3//:mime_guess", - "@raze__serde__1_0_123//:serde", - "@raze__serde_json__1_0_62//:serde_json", + "@raze__serde__1_0_124//:serde", + "@raze__serde_json__1_0_64//:serde_json", "@raze__serde_urlencoded__0_6_1//:serde_urlencoded", - "@raze__url__2_2_0//:url", + "@raze__url__2_2_1//:url", ] + selects.with_or({ # cfg(not(target_arch = "wasm32")) ( @@ -102,8 +102,8 @@ rust_library( ): [ "@raze__base64__0_13_0//:base64", "@raze__encoding_rs__0_8_28//:encoding_rs", - "@raze__futures_core__0_3_12//:futures_core", - "@raze__futures_util__0_3_12//:futures_util", + "@raze__futures_core__0_3_13//:futures_core", + "@raze__futures_util__0_3_13//:futures_util", "@raze__http_body__0_3_1//:http_body", "@raze__hyper__0_13_10//:hyper", "@raze__hyper_rustls__0_21_0//:hyper_rustls", @@ -114,7 +114,7 @@ rust_library( "@raze__mime__0_3_16//:mime", "@raze__native_tls__0_2_7//:native_tls", "@raze__percent_encoding__2_1_0//:percent_encoding", - "@raze__pin_project_lite__0_1_11//:pin_project_lite", + "@raze__pin_project_lite__0_1_12//:pin_project_lite", "@raze__rustls__0_18_1//:rustls", "@raze__tokio__0_2_25//:tokio", "@raze__tokio_rustls__0_14_1//:tokio_rustls", diff --git a/cargo/remote/BUILD.ring-0.16.20.bazel b/cargo/remote/BUILD.ring-0.16.20.bazel index 7463b0d9b..8c6a98473 100644 --- a/cargo/remote/BUILD.ring-0.16.20.bazel +++ b/cargo/remote/BUILD.ring-0.16.20.bazel @@ -59,7 +59,7 @@ cargo_build_script( version = "0.16.20", visibility = ["//visibility:private"], deps = [ - "@raze__cc__1_0_66//:cc", + "@raze__cc__1_0_67//:cc", ] + selects.with_or({ # cfg(any(target_arch = "x86", target_arch = "x86_64", all(any(target_arch = "aarch64", target_arch = "arm"), any(target_os = "android", target_os = "fuchsia", target_os = "linux")))) ( @@ -147,8 +147,8 @@ rust_library( "@io_bazel_rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", - "@raze__once_cell__1_5_2//:once_cell", + "@raze__libc__0_2_88//:libc", + "@raze__once_cell__1_7_2//:once_cell", ], "//conditions:default": [], }) + selects.with_or({ diff --git a/cargo/remote/BUILD.rust-argon2-0.8.3.bazel b/cargo/remote/BUILD.rust-argon2-0.8.3.bazel index 8df7e3df2..817c4a5c9 100644 --- a/cargo/remote/BUILD.rust-argon2-0.8.3.bazel +++ b/cargo/remote/BUILD.rust-argon2-0.8.3.bazel @@ -63,7 +63,7 @@ rust_library( "@raze__base64__0_13_0//:base64", "@raze__blake2b_simd__0_5_11//:blake2b_simd", "@raze__constant_time_eq__0_1_5//:constant_time_eq", - "@raze__crossbeam_utils__0_8_1//:crossbeam_utils", + "@raze__crossbeam_utils__0_8_3//:crossbeam_utils", ], ) diff --git a/cargo/remote/BUILD.security-framework-2.0.0.bazel b/cargo/remote/BUILD.security-framework-2.1.2.bazel similarity index 92% rename from cargo/remote/BUILD.security-framework-2.0.0.bazel rename to cargo/remote/BUILD.security-framework-2.1.2.bazel index 99d908a6a..ed2fec94c 100644 --- a/cargo/remote/BUILD.security-framework-2.0.0.bazel +++ b/cargo/remote/BUILD.security-framework-2.1.2.bazel @@ -54,13 +54,13 @@ rust_library( "cargo-raze", "manual", ], - version = "2.0.0", + version = "2.1.2", # buildifier: leave-alone deps = [ "@raze__bitflags__1_2_1//:bitflags", "@raze__core_foundation__0_9_1//:core_foundation", "@raze__core_foundation_sys__0_8_2//:core_foundation_sys", - "@raze__libc__0_2_85//:libc", - "@raze__security_framework_sys__2_0_0//:security_framework_sys", + "@raze__libc__0_2_88//:libc", + "@raze__security_framework_sys__2_1_1//:security_framework_sys", ], ) diff --git a/cargo/remote/BUILD.security-framework-sys-2.0.0.bazel b/cargo/remote/BUILD.security-framework-sys-2.1.1.bazel similarity index 94% rename from cargo/remote/BUILD.security-framework-sys-2.0.0.bazel rename to cargo/remote/BUILD.security-framework-sys-2.1.1.bazel index b4e23dc5a..649e12269 100644 --- a/cargo/remote/BUILD.security-framework-sys-2.0.0.bazel +++ b/cargo/remote/BUILD.security-framework-sys-2.1.1.bazel @@ -48,10 +48,10 @@ rust_library( "cargo-raze", "manual", ], - version = "2.0.0", + version = "2.1.1", # buildifier: leave-alone deps = [ "@raze__core_foundation_sys__0_8_2//:core_foundation_sys", - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], ) diff --git a/cargo/remote/BUILD.serde-1.0.123.bazel b/cargo/remote/BUILD.serde-1.0.124.bazel similarity index 94% rename from cargo/remote/BUILD.serde-1.0.123.bazel rename to cargo/remote/BUILD.serde-1.0.124.bazel index 592548aa5..45ba37041 100644 --- a/cargo/remote/BUILD.serde-1.0.123.bazel +++ b/cargo/remote/BUILD.serde-1.0.124.bazel @@ -56,7 +56,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "1.0.123", + version = "1.0.124", visibility = ["//visibility:private"], deps = [ ], @@ -76,7 +76,7 @@ rust_library( data = [], edition = "2015", proc_macro_deps = [ - "@raze__serde_derive__1_0_123//:serde_derive", + "@raze__serde_derive__1_0_124//:serde_derive", ], rustc_flags = [ "--cap-lints=allow", @@ -85,7 +85,7 @@ rust_library( "cargo-raze", "manual", ], - version = "1.0.123", + version = "1.0.124", # buildifier: leave-alone deps = [ ":serde_build_script", diff --git a/cargo/remote/BUILD.serde-aux-0.6.1.bazel b/cargo/remote/BUILD.serde-aux-0.6.1.bazel index 428e3e144..c57cc9ea7 100644 --- a/cargo/remote/BUILD.serde-aux-0.6.1.bazel +++ b/cargo/remote/BUILD.serde-aux-0.6.1.bazel @@ -42,7 +42,7 @@ rust_library( data = [], edition = "2015", proc_macro_deps = [ - "@raze__serde_derive__1_0_123//:serde_derive", + "@raze__serde_derive__1_0_124//:serde_derive", ], rustc_flags = [ "--cap-lints=allow", @@ -55,7 +55,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__chrono__0_4_19//:chrono", - "@raze__serde__1_0_123//:serde", - "@raze__serde_json__1_0_62//:serde_json", + "@raze__serde__1_0_124//:serde", + "@raze__serde_json__1_0_64//:serde_json", ], ) diff --git a/cargo/remote/BUILD.serde_derive-1.0.123.bazel b/cargo/remote/BUILD.serde_derive-1.0.124.bazel similarity index 93% rename from cargo/remote/BUILD.serde_derive-1.0.123.bazel rename to cargo/remote/BUILD.serde_derive-1.0.124.bazel index e401ff426..c777b1cbd 100644 --- a/cargo/remote/BUILD.serde_derive-1.0.123.bazel +++ b/cargo/remote/BUILD.serde_derive-1.0.124.bazel @@ -53,7 +53,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "1.0.123", + version = "1.0.124", visibility = ["//visibility:private"], deps = [ ], @@ -76,12 +76,12 @@ rust_library( "cargo-raze", "manual", ], - version = "1.0.123", + version = "1.0.124", # buildifier: leave-alone deps = [ ":serde_derive_build_script", "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.serde_json-1.0.62.bazel b/cargo/remote/BUILD.serde_json-1.0.64.bazel similarity index 95% rename from cargo/remote/BUILD.serde_json-1.0.62.bazel rename to cargo/remote/BUILD.serde_json-1.0.64.bazel index 093549615..c57b61f94 100644 --- a/cargo/remote/BUILD.serde_json-1.0.62.bazel +++ b/cargo/remote/BUILD.serde_json-1.0.64.bazel @@ -54,7 +54,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "1.0.62", + version = "1.0.64", visibility = ["//visibility:private"], deps = [ ], @@ -78,12 +78,12 @@ rust_library( "cargo-raze", "manual", ], - version = "1.0.62", + version = "1.0.64", # buildifier: leave-alone deps = [ ":serde_json_build_script", "@raze__itoa__0_4_7//:itoa", "@raze__ryu__1_0_5//:ryu", - "@raze__serde__1_0_123//:serde", + "@raze__serde__1_0_124//:serde", ], ) diff --git a/cargo/remote/BUILD.serde_repr-0.1.6.bazel b/cargo/remote/BUILD.serde_repr-0.1.6.bazel index aa549d228..7b9a7c5a3 100644 --- a/cargo/remote/BUILD.serde_repr-0.1.6.bazel +++ b/cargo/remote/BUILD.serde_repr-0.1.6.bazel @@ -50,8 +50,8 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.serde_tuple-0.5.0.bazel b/cargo/remote/BUILD.serde_tuple-0.5.0.bazel index a8123acdf..3f8d2de30 100644 --- a/cargo/remote/BUILD.serde_tuple-0.5.0.bazel +++ b/cargo/remote/BUILD.serde_tuple-0.5.0.bazel @@ -54,6 +54,6 @@ rust_library( version = "0.5.0", # buildifier: leave-alone deps = [ - "@raze__serde__1_0_123//:serde", + "@raze__serde__1_0_124//:serde", ], ) diff --git a/cargo/remote/BUILD.serde_tuple_macros-0.5.0.bazel b/cargo/remote/BUILD.serde_tuple_macros-0.5.0.bazel index c68a1f9d5..b6d826136 100644 --- a/cargo/remote/BUILD.serde_tuple_macros-0.5.0.bazel +++ b/cargo/remote/BUILD.serde_tuple_macros-0.5.0.bazel @@ -50,7 +50,7 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.serde_urlencoded-0.6.1.bazel b/cargo/remote/BUILD.serde_urlencoded-0.6.1.bazel index 1855d128c..31b5a6d2a 100644 --- a/cargo/remote/BUILD.serde_urlencoded-0.6.1.bazel +++ b/cargo/remote/BUILD.serde_urlencoded-0.6.1.bazel @@ -51,8 +51,8 @@ rust_library( deps = [ "@raze__dtoa__0_4_7//:dtoa", "@raze__itoa__0_4_7//:itoa", - "@raze__serde__1_0_123//:serde", - "@raze__url__2_2_0//:url", + "@raze__serde__1_0_124//:serde", + "@raze__url__2_2_1//:url", ], ) diff --git a/cargo/remote/BUILD.socket2-0.3.19.bazel b/cargo/remote/BUILD.socket2-0.3.19.bazel index c802d5ff0..df0dcc444 100644 --- a/cargo/remote/BUILD.socket2-0.3.19.bazel +++ b/cargo/remote/BUILD.socket2-0.3.19.bazel @@ -61,7 +61,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ "@raze__cfg_if__1_0_0//:cfg_if", - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }) + selects.with_or({ diff --git a/cargo/remote/BUILD.string_cache-0.8.1.bazel b/cargo/remote/BUILD.string_cache-0.8.1.bazel index e0757a8ad..acc63b8a3 100644 --- a/cargo/remote/BUILD.string_cache-0.8.1.bazel +++ b/cargo/remote/BUILD.string_cache-0.8.1.bazel @@ -58,7 +58,7 @@ rust_library( "@raze__new_debug_unreachable__1_0_4//:new_debug_unreachable", "@raze__phf_shared__0_8_0//:phf_shared", "@raze__precomputed_hash__0_1_1//:precomputed_hash", - "@raze__serde__1_0_123//:serde", + "@raze__serde__1_0_124//:serde", ], ) diff --git a/cargo/remote/BUILD.string_cache_codegen-0.5.1.bazel b/cargo/remote/BUILD.string_cache_codegen-0.5.1.bazel index 3249be290..1ad9a8959 100644 --- a/cargo/remote/BUILD.string_cache_codegen-0.5.1.bazel +++ b/cargo/remote/BUILD.string_cache_codegen-0.5.1.bazel @@ -52,6 +52,6 @@ rust_library( "@raze__phf_generator__0_8_0//:phf_generator", "@raze__phf_shared__0_8_0//:phf_shared", "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", + "@raze__quote__1_0_9//:quote", ], ) diff --git a/cargo/remote/BUILD.strum-0.20.0.bazel b/cargo/remote/BUILD.strum-0.20.0.bazel new file mode 100644 index 000000000..0409267e7 --- /dev/null +++ b/cargo/remote/BUILD.strum-0.20.0.bazel @@ -0,0 +1,58 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_binary", + "rust_library", + "rust_test", +) + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT" +]) + +# Generated Targets + +rust_library( + name = "strum", + srcs = glob(["**/*.rs"]), + crate_features = [ + "derive", + "strum_macros", + ], + crate_root = "src/lib.rs", + crate_type = "lib", + data = [], + edition = "2018", + proc_macro_deps = [ + "@raze__strum_macros__0_20_1//:strum_macros", + ], + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.20.0", + # buildifier: leave-alone + deps = [ + ], +) diff --git a/cargo/remote/BUILD.strum_macros-0.20.1.bazel b/cargo/remote/BUILD.strum_macros-0.20.1.bazel new file mode 100644 index 000000000..0fb89b5f8 --- /dev/null +++ b/cargo/remote/BUILD.strum_macros-0.20.1.bazel @@ -0,0 +1,57 @@ +""" +@generated +cargo-raze crate build file. + +DO NOT EDIT! Replaced on runs of cargo-raze +""" + +# buildifier: disable=load +load( + "@io_bazel_rules_rust//rust:rust.bzl", + "rust_binary", + "rust_library", + "rust_test", +) + +# buildifier: disable=load +load("@bazel_skylib//lib:selects.bzl", "selects") + +package(default_visibility = [ + # Public for visibility by "@raze__crate__version//" targets. + # + # Prefer access through "//cargo", which limits external + # visibility to explicit Cargo.toml dependencies. + "//visibility:public", +]) + +licenses([ + "notice", # MIT from expression "MIT" +]) + +# Generated Targets + +rust_library( + name = "strum_macros", + srcs = glob(["**/*.rs"]), + crate_features = [ + ], + crate_root = "src/lib.rs", + crate_type = "proc-macro", + data = [], + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-raze", + "manual", + ], + version = "0.20.1", + # buildifier: leave-alone + deps = [ + "@raze__heck__0_3_2//:heck", + "@raze__proc_macro2__1_0_24//:proc_macro2", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", + ], +) diff --git a/cargo/remote/BUILD.syn-1.0.60.bazel b/cargo/remote/BUILD.syn-1.0.63.bazel similarity index 97% rename from cargo/remote/BUILD.syn-1.0.60.bazel rename to cargo/remote/BUILD.syn-1.0.63.bazel index 0c89588e8..a3609c163 100644 --- a/cargo/remote/BUILD.syn-1.0.60.bazel +++ b/cargo/remote/BUILD.syn-1.0.63.bazel @@ -64,7 +64,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "1.0.60", + version = "1.0.63", visibility = ["//visibility:private"], deps = [ ], @@ -102,12 +102,12 @@ rust_library( "cargo-raze", "manual", ], - version = "1.0.60", + version = "1.0.63", # buildifier: leave-alone deps = [ ":syn_build_script", "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", + "@raze__quote__1_0_9//:quote", "@raze__unicode_xid__0_2_1//:unicode_xid", ], ) diff --git a/cargo/remote/BUILD.synstructure-0.12.4.bazel b/cargo/remote/BUILD.synstructure-0.12.4.bazel index dc462ba22..a6948af5a 100644 --- a/cargo/remote/BUILD.synstructure-0.12.4.bazel +++ b/cargo/remote/BUILD.synstructure-0.12.4.bazel @@ -52,8 +52,8 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", "@raze__unicode_xid__0_2_1//:unicode_xid", ], ) diff --git a/cargo/remote/BUILD.tap-1.0.0.bazel b/cargo/remote/BUILD.tap-1.0.1.bazel similarity index 97% rename from cargo/remote/BUILD.tap-1.0.0.bazel rename to cargo/remote/BUILD.tap-1.0.1.bazel index e25e709f2..f8fb46be4 100644 --- a/cargo/remote/BUILD.tap-1.0.0.bazel +++ b/cargo/remote/BUILD.tap-1.0.1.bazel @@ -46,7 +46,7 @@ rust_library( "cargo-raze", "manual", ], - version = "1.0.0", + version = "1.0.1", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.tempfile-3.2.0.bazel b/cargo/remote/BUILD.tempfile-3.2.0.bazel index 771af477b..6842d118c 100644 --- a/cargo/remote/BUILD.tempfile-3.2.0.bazel +++ b/cargo/remote/BUILD.tempfile-3.2.0.bazel @@ -63,7 +63,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }) + selects.with_or({ diff --git a/cargo/remote/BUILD.thiserror-1.0.23.bazel b/cargo/remote/BUILD.thiserror-1.0.24.bazel similarity index 95% rename from cargo/remote/BUILD.thiserror-1.0.23.bazel rename to cargo/remote/BUILD.thiserror-1.0.24.bazel index 689ce3b95..f52bd7542 100644 --- a/cargo/remote/BUILD.thiserror-1.0.23.bazel +++ b/cargo/remote/BUILD.thiserror-1.0.24.bazel @@ -40,7 +40,7 @@ rust_library( data = [], edition = "2018", proc_macro_deps = [ - "@raze__thiserror_impl__1_0_23//:thiserror_impl", + "@raze__thiserror_impl__1_0_24//:thiserror_impl", ], rustc_flags = [ "--cap-lints=allow", @@ -49,7 +49,7 @@ rust_library( "cargo-raze", "manual", ], - version = "1.0.23", + version = "1.0.24", # buildifier: leave-alone deps = [ ], diff --git a/cargo/remote/BUILD.thiserror-impl-1.0.23.bazel b/cargo/remote/BUILD.thiserror-impl-1.0.24.bazel similarity index 91% rename from cargo/remote/BUILD.thiserror-impl-1.0.23.bazel rename to cargo/remote/BUILD.thiserror-impl-1.0.24.bazel index ae4d6e568..da193929f 100644 --- a/cargo/remote/BUILD.thiserror-impl-1.0.23.bazel +++ b/cargo/remote/BUILD.thiserror-impl-1.0.24.bazel @@ -46,11 +46,11 @@ rust_library( "cargo-raze", "manual", ], - version = "1.0.23", + version = "1.0.24", # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", ], ) diff --git a/cargo/remote/BUILD.thread_local-1.1.3.bazel b/cargo/remote/BUILD.thread_local-1.1.3.bazel index 4ca0f559f..f5363f306 100644 --- a/cargo/remote/BUILD.thread_local-1.1.3.bazel +++ b/cargo/remote/BUILD.thread_local-1.1.3.bazel @@ -51,6 +51,6 @@ rust_library( version = "1.1.3", # buildifier: leave-alone deps = [ - "@raze__once_cell__1_5_2//:once_cell", + "@raze__once_cell__1_7_2//:once_cell", ], ) diff --git a/cargo/remote/BUILD.time-0.1.43.bazel b/cargo/remote/BUILD.time-0.1.43.bazel index fa0bdada4..863cce86c 100644 --- a/cargo/remote/BUILD.time-0.1.43.bazel +++ b/cargo/remote/BUILD.time-0.1.43.bazel @@ -51,7 +51,7 @@ rust_library( version = "0.1.43", # buildifier: leave-alone deps = [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ] + selects.with_or({ # cfg(windows) ( diff --git a/cargo/remote/BUILD.tokio-0.2.25.bazel b/cargo/remote/BUILD.tokio-0.2.25.bazel index 0628dbab5..a51647bb7 100644 --- a/cargo/remote/BUILD.tokio-0.2.25.bazel +++ b/cargo/remote/BUILD.tokio-0.2.25.bazel @@ -72,13 +72,13 @@ rust_library( deps = [ "@raze__bytes__0_5_6//:bytes", "@raze__fnv__1_0_7//:fnv", - "@raze__futures_core__0_3_12//:futures_core", + "@raze__futures_core__0_3_13//:futures_core", "@raze__iovec__0_1_4//:iovec", "@raze__lazy_static__1_4_0//:lazy_static", "@raze__memchr__2_3_4//:memchr", "@raze__mio__0_6_23//:mio", "@raze__num_cpus__1_13_0//:num_cpus", - "@raze__pin_project_lite__0_1_11//:pin_project_lite", + "@raze__pin_project_lite__0_1_12//:pin_project_lite", "@raze__slab__0_4_2//:slab", ] + selects.with_or({ # cfg(unix) diff --git a/cargo/remote/BUILD.tokio-rustls-0.14.1.bazel b/cargo/remote/BUILD.tokio-rustls-0.14.1.bazel index 555f58bd5..eac533793 100644 --- a/cargo/remote/BUILD.tokio-rustls-0.14.1.bazel +++ b/cargo/remote/BUILD.tokio-rustls-0.14.1.bazel @@ -49,7 +49,7 @@ rust_library( version = "0.14.1", # buildifier: leave-alone deps = [ - "@raze__futures_core__0_3_12//:futures_core", + "@raze__futures_core__0_3_13//:futures_core", "@raze__rustls__0_18_1//:rustls", "@raze__tokio__0_2_25//:tokio", "@raze__webpki__0_21_4//:webpki", diff --git a/cargo/remote/BUILD.tokio-socks-0.3.0.bazel b/cargo/remote/BUILD.tokio-socks-0.3.0.bazel index 9b3cedf93..02eabf0fe 100644 --- a/cargo/remote/BUILD.tokio-socks-0.3.0.bazel +++ b/cargo/remote/BUILD.tokio-socks-0.3.0.bazel @@ -55,8 +55,8 @@ rust_library( deps = [ "@raze__bytes__0_4_12//:bytes", "@raze__either__1_6_1//:either", - "@raze__futures__0_3_12//:futures", - "@raze__thiserror__1_0_23//:thiserror", + "@raze__futures__0_3_13//:futures", + "@raze__thiserror__1_0_24//:thiserror", "@raze__tokio__0_2_25//:tokio", ], ) diff --git a/cargo/remote/BUILD.tokio-util-0.3.1.bazel b/cargo/remote/BUILD.tokio-util-0.3.1.bazel index b75bd3097..3aadb6825 100644 --- a/cargo/remote/BUILD.tokio-util-0.3.1.bazel +++ b/cargo/remote/BUILD.tokio-util-0.3.1.bazel @@ -52,10 +52,10 @@ rust_library( # buildifier: leave-alone deps = [ "@raze__bytes__0_5_6//:bytes", - "@raze__futures_core__0_3_12//:futures_core", - "@raze__futures_sink__0_3_12//:futures_sink", + "@raze__futures_core__0_3_13//:futures_core", + "@raze__futures_sink__0_3_13//:futures_sink", "@raze__log__0_4_14//:log", - "@raze__pin_project_lite__0_1_11//:pin_project_lite", + "@raze__pin_project_lite__0_1_12//:pin_project_lite", "@raze__tokio__0_2_25//:tokio", ], ) diff --git a/cargo/remote/BUILD.toml-0.5.8.bazel b/cargo/remote/BUILD.toml-0.5.8.bazel index 837f1bf91..40d27402d 100644 --- a/cargo/remote/BUILD.toml-0.5.8.bazel +++ b/cargo/remote/BUILD.toml-0.5.8.bazel @@ -56,7 +56,7 @@ rust_library( version = "0.5.8", # buildifier: leave-alone deps = [ - "@raze__serde__1_0_123//:serde", + "@raze__serde__1_0_124//:serde", ], ) diff --git a/cargo/remote/BUILD.tracing-0.1.23.bazel b/cargo/remote/BUILD.tracing-0.1.25.bazel similarity index 96% rename from cargo/remote/BUILD.tracing-0.1.23.bazel rename to cargo/remote/BUILD.tracing-0.1.25.bazel index cd106c745..84c260ef6 100644 --- a/cargo/remote/BUILD.tracing-0.1.23.bazel +++ b/cargo/remote/BUILD.tracing-0.1.25.bazel @@ -52,12 +52,12 @@ rust_library( "cargo-raze", "manual", ], - version = "0.1.23", + version = "0.1.25", # buildifier: leave-alone deps = [ "@raze__cfg_if__1_0_0//:cfg_if", "@raze__log__0_4_14//:log", - "@raze__pin_project_lite__0_2_4//:pin_project_lite", + "@raze__pin_project_lite__0_2_6//:pin_project_lite", "@raze__tracing_core__0_1_17//:tracing_core", ], ) diff --git a/cargo/remote/BUILD.tracing-futures-0.2.4.bazel b/cargo/remote/BUILD.tracing-futures-0.2.5.bazel similarity index 90% rename from cargo/remote/BUILD.tracing-futures-0.2.4.bazel rename to cargo/remote/BUILD.tracing-futures-0.2.5.bazel index 901d7c564..8fd484365 100644 --- a/cargo/remote/BUILD.tracing-futures-0.2.4.bazel +++ b/cargo/remote/BUILD.tracing-futures-0.2.5.bazel @@ -48,11 +48,11 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.4", + version = "0.2.5", # buildifier: leave-alone deps = [ - "@raze__pin_project__0_4_27//:pin_project", - "@raze__tracing__0_1_23//:tracing", + "@raze__pin_project__1_0_5//:pin_project", + "@raze__tracing__0_1_25//:tracing", ], ) diff --git a/cargo/remote/BUILD.unic-langid-macros-impl-0.9.0.bazel b/cargo/remote/BUILD.unic-langid-macros-impl-0.9.0.bazel index 7f38ab555..b203d89b8 100644 --- a/cargo/remote/BUILD.unic-langid-macros-impl-0.9.0.bazel +++ b/cargo/remote/BUILD.unic-langid-macros-impl-0.9.0.bazel @@ -52,8 +52,8 @@ rust_library( version = "0.9.0", # buildifier: leave-alone deps = [ - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", "@raze__unic_langid_impl__0_9_0//:unic_langid_impl", ], ) diff --git a/cargo/remote/BUILD.unicode-normalization-0.1.16.bazel b/cargo/remote/BUILD.unicode-normalization-0.1.17.bazel similarity index 97% rename from cargo/remote/BUILD.unicode-normalization-0.1.16.bazel rename to cargo/remote/BUILD.unicode-normalization-0.1.17.bazel index 37490a6d2..1af4ca4ab 100644 --- a/cargo/remote/BUILD.unicode-normalization-0.1.16.bazel +++ b/cargo/remote/BUILD.unicode-normalization-0.1.17.bazel @@ -50,7 +50,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.1.16", + version = "0.1.17", # buildifier: leave-alone deps = [ "@raze__tinyvec__1_1_1//:tinyvec", diff --git a/cargo/remote/BUILD.url-2.2.0.bazel b/cargo/remote/BUILD.url-2.2.1.bazel similarity index 83% rename from cargo/remote/BUILD.url-2.2.0.bazel rename to cargo/remote/BUILD.url-2.2.1.bazel index acfd5e54e..874779e2c 100644 --- a/cargo/remote/BUILD.url-2.2.0.bazel +++ b/cargo/remote/BUILD.url-2.2.1.bazel @@ -48,12 +48,16 @@ rust_library( "cargo-raze", "manual", ], - version = "2.2.0", + version = "2.2.1", # buildifier: leave-alone deps = [ - "@raze__form_urlencoded__1_0_0//:form_urlencoded", - "@raze__idna__0_2_1//:idna", + "@raze__form_urlencoded__1_0_1//:form_urlencoded", + "@raze__idna__0_2_2//:idna", "@raze__matches__0_1_8//:matches", "@raze__percent_encoding__2_1_0//:percent_encoding", ], ) + +# Unsupported target "data" with type "test" omitted + +# Unsupported target "unit" with type "test" omitted diff --git a/cargo/remote/BUILD.utime-0.3.1.bazel b/cargo/remote/BUILD.utime-0.3.1.bazel index f4c2b59bb..48ba549bd 100644 --- a/cargo/remote/BUILD.utime-0.3.1.bazel +++ b/cargo/remote/BUILD.utime-0.3.1.bazel @@ -60,7 +60,7 @@ rust_library( "@io_bazel_rules_rust//rust/platform:x86_64-apple-ios", "@io_bazel_rules_rust//rust/platform:x86_64-unknown-linux-gnu", ): [ - "@raze__libc__0_2_85//:libc", + "@raze__libc__0_2_88//:libc", ], "//conditions:default": [], }) + selects.with_or({ diff --git a/cargo/remote/BUILD.wasm-bindgen-0.2.70.bazel b/cargo/remote/BUILD.wasm-bindgen-0.2.71.bazel similarity index 91% rename from cargo/remote/BUILD.wasm-bindgen-0.2.70.bazel rename to cargo/remote/BUILD.wasm-bindgen-0.2.71.bazel index 7632b7e84..6a3890704 100644 --- a/cargo/remote/BUILD.wasm-bindgen-0.2.70.bazel +++ b/cargo/remote/BUILD.wasm-bindgen-0.2.71.bazel @@ -58,7 +58,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "0.2.70", + version = "0.2.71", visibility = ["//visibility:private"], deps = [ ], @@ -80,7 +80,7 @@ rust_library( data = [], edition = "2018", proc_macro_deps = [ - "@raze__wasm_bindgen_macro__0_2_70//:wasm_bindgen_macro", + "@raze__wasm_bindgen_macro__0_2_71//:wasm_bindgen_macro", ], rustc_flags = [ "--cap-lints=allow", @@ -89,13 +89,13 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.70", + version = "0.2.71", # buildifier: leave-alone deps = [ ":wasm_bindgen_build_script", "@raze__cfg_if__1_0_0//:cfg_if", - "@raze__serde__1_0_123//:serde", - "@raze__serde_json__1_0_62//:serde_json", + "@raze__serde__1_0_124//:serde", + "@raze__serde_json__1_0_64//:serde_json", ], ) diff --git a/cargo/remote/BUILD.wasm-bindgen-backend-0.2.70.bazel b/cargo/remote/BUILD.wasm-bindgen-backend-0.2.71.bazel similarity index 85% rename from cargo/remote/BUILD.wasm-bindgen-backend-0.2.70.bazel rename to cargo/remote/BUILD.wasm-bindgen-backend-0.2.71.bazel index a2d7e88b3..b874f44a9 100644 --- a/cargo/remote/BUILD.wasm-bindgen-backend-0.2.70.bazel +++ b/cargo/remote/BUILD.wasm-bindgen-backend-0.2.71.bazel @@ -47,15 +47,15 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.70", + version = "0.2.71", # buildifier: leave-alone deps = [ - "@raze__bumpalo__3_6_0//:bumpalo", + "@raze__bumpalo__3_6_1//:bumpalo", "@raze__lazy_static__1_4_0//:lazy_static", "@raze__log__0_4_14//:log", "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", - "@raze__wasm_bindgen_shared__0_2_70//:wasm_bindgen_shared", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", + "@raze__wasm_bindgen_shared__0_2_71//:wasm_bindgen_shared", ], ) diff --git a/cargo/remote/BUILD.wasm-bindgen-futures-0.4.20.bazel b/cargo/remote/BUILD.wasm-bindgen-futures-0.4.21.bazel similarity index 90% rename from cargo/remote/BUILD.wasm-bindgen-futures-0.4.20.bazel rename to cargo/remote/BUILD.wasm-bindgen-futures-0.4.21.bazel index c0be10815..4c1b1221c 100644 --- a/cargo/remote/BUILD.wasm-bindgen-futures-0.4.20.bazel +++ b/cargo/remote/BUILD.wasm-bindgen-futures-0.4.21.bazel @@ -46,12 +46,12 @@ rust_library( "cargo-raze", "manual", ], - version = "0.4.20", + version = "0.4.21", # buildifier: leave-alone deps = [ "@raze__cfg_if__1_0_0//:cfg_if", - "@raze__js_sys__0_3_47//:js_sys", - "@raze__wasm_bindgen__0_2_70//:wasm_bindgen", + "@raze__js_sys__0_3_48//:js_sys", + "@raze__wasm_bindgen__0_2_71//:wasm_bindgen", ], ) diff --git a/cargo/remote/BUILD.wasm-bindgen-macro-0.2.70.bazel b/cargo/remote/BUILD.wasm-bindgen-macro-0.2.71.bazel similarity index 89% rename from cargo/remote/BUILD.wasm-bindgen-macro-0.2.70.bazel rename to cargo/remote/BUILD.wasm-bindgen-macro-0.2.71.bazel index b7b82c0ca..7b389a9a0 100644 --- a/cargo/remote/BUILD.wasm-bindgen-macro-0.2.70.bazel +++ b/cargo/remote/BUILD.wasm-bindgen-macro-0.2.71.bazel @@ -47,11 +47,11 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.70", + version = "0.2.71", # buildifier: leave-alone deps = [ - "@raze__quote__1_0_8//:quote", - "@raze__wasm_bindgen_macro_support__0_2_70//:wasm_bindgen_macro_support", + "@raze__quote__1_0_9//:quote", + "@raze__wasm_bindgen_macro_support__0_2_71//:wasm_bindgen_macro_support", ], ) diff --git a/cargo/remote/BUILD.wasm-bindgen-macro-support-0.2.70.bazel b/cargo/remote/BUILD.wasm-bindgen-macro-support-0.2.71.bazel similarity index 82% rename from cargo/remote/BUILD.wasm-bindgen-macro-support-0.2.70.bazel rename to cargo/remote/BUILD.wasm-bindgen-macro-support-0.2.71.bazel index 989742b9b..fbce4182b 100644 --- a/cargo/remote/BUILD.wasm-bindgen-macro-support-0.2.70.bazel +++ b/cargo/remote/BUILD.wasm-bindgen-macro-support-0.2.71.bazel @@ -47,13 +47,13 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.70", + version = "0.2.71", # buildifier: leave-alone deps = [ "@raze__proc_macro2__1_0_24//:proc_macro2", - "@raze__quote__1_0_8//:quote", - "@raze__syn__1_0_60//:syn", - "@raze__wasm_bindgen_backend__0_2_70//:wasm_bindgen_backend", - "@raze__wasm_bindgen_shared__0_2_70//:wasm_bindgen_shared", + "@raze__quote__1_0_9//:quote", + "@raze__syn__1_0_63//:syn", + "@raze__wasm_bindgen_backend__0_2_71//:wasm_bindgen_backend", + "@raze__wasm_bindgen_shared__0_2_71//:wasm_bindgen_shared", ], ) diff --git a/cargo/remote/BUILD.wasm-bindgen-shared-0.2.70.bazel b/cargo/remote/BUILD.wasm-bindgen-shared-0.2.71.bazel similarity index 97% rename from cargo/remote/BUILD.wasm-bindgen-shared-0.2.70.bazel rename to cargo/remote/BUILD.wasm-bindgen-shared-0.2.71.bazel index 948418481..be6dfff8d 100644 --- a/cargo/remote/BUILD.wasm-bindgen-shared-0.2.70.bazel +++ b/cargo/remote/BUILD.wasm-bindgen-shared-0.2.71.bazel @@ -52,7 +52,7 @@ cargo_build_script( "cargo-raze", "manual", ], - version = "0.2.70", + version = "0.2.71", visibility = ["//visibility:private"], deps = [ ], @@ -74,7 +74,7 @@ rust_library( "cargo-raze", "manual", ], - version = "0.2.70", + version = "0.2.71", # buildifier: leave-alone deps = [ ":wasm_bindgen_shared_build_script", diff --git a/cargo/remote/BUILD.web-sys-0.3.47.bazel b/cargo/remote/BUILD.web-sys-0.3.48.bazel similarity index 91% rename from cargo/remote/BUILD.web-sys-0.3.47.bazel rename to cargo/remote/BUILD.web-sys-0.3.48.bazel index 592a5df06..5ba0a654c 100644 --- a/cargo/remote/BUILD.web-sys-0.3.47.bazel +++ b/cargo/remote/BUILD.web-sys-0.3.48.bazel @@ -60,11 +60,11 @@ rust_library( "cargo-raze", "manual", ], - version = "0.3.47", + version = "0.3.48", # buildifier: leave-alone deps = [ - "@raze__js_sys__0_3_47//:js_sys", - "@raze__wasm_bindgen__0_2_70//:wasm_bindgen", + "@raze__js_sys__0_3_48//:js_sys", + "@raze__wasm_bindgen__0_2_71//:wasm_bindgen", ], ) diff --git a/cargo/remote/BUILD.which-4.0.2.bazel b/cargo/remote/BUILD.which-4.0.2.bazel index 5f45a1ed3..0d07803d4 100644 --- a/cargo/remote/BUILD.which-4.0.2.bazel +++ b/cargo/remote/BUILD.which-4.0.2.bazel @@ -49,8 +49,8 @@ rust_library( version = "4.0.2", # buildifier: leave-alone deps = [ - "@raze__libc__0_2_85//:libc", - "@raze__thiserror__1_0_23//:thiserror", + "@raze__libc__0_2_88//:libc", + "@raze__thiserror__1_0_24//:thiserror", ], ) diff --git a/defs.bzl b/defs.bzl index 12e2288aa..fd3d60bfe 100644 --- a/defs.bzl +++ b/defs.bzl @@ -11,7 +11,7 @@ load("@build_bazel_rules_svelte//:defs.bzl", "rules_svelte_dependencies") load("@com_github_ali5h_rules_pip//:defs.bzl", "pip_import") load("//pip/pyqt5:defs.bzl", "install_pyqt5") -anki_version = "2.1.41" +anki_version = "2.1.43" def setup_deps(): bazel_skylib_workspace() diff --git a/ftl/core/decks.ftl b/ftl/core/decks.ftl index 29e359c5c..3c74c863d 100644 --- a/ftl/core/decks.ftl +++ b/ftl/core/decks.ftl @@ -32,6 +32,7 @@ decks-reschedule-cards-based-on-my-answers = Reschedule cards based on my answer decks-study = Study decks-study-deck = Study Deck decks-the-provided-search-did-not-match = The provided search did not match any cards. Would you like to revise it? +decks-unmovable-cards = Show any excluded cards decks-it-has-card = { $count -> [one] It has { $count } card. diff --git a/ftl/core/errors.ftl b/ftl/core/errors.ftl index 5f5ea6eb5..cc3b138d0 100644 --- a/ftl/core/errors.ftl +++ b/ftl/core/errors.ftl @@ -1,3 +1,4 @@ errors-invalid-input-empty = Invalid input. errors-invalid-input-details = Invalid input: { $details } errors-parse-number-fail = A number was invalid or out of range. +errors-filtered-parent-deck = Filtered decks can not have child decks. diff --git a/ftl/core/scheduling.ftl b/ftl/core/scheduling.ftl index bdb27f723..0cd733ac7 100644 --- a/ftl/core/scheduling.ftl +++ b/ftl/core/scheduling.ftl @@ -170,6 +170,6 @@ scheduling-set-due-date-done = } scheduling-forgot-cards = { $cards -> - [one] Forgot { $card } card. + [one] Forgot { $cards } card. *[other] Forgot { $cards } cards. } diff --git a/ftl/core/undo.ftl b/ftl/core/undo.ftl new file mode 100644 index 000000000..e60c9a2a4 --- /dev/null +++ b/ftl/core/undo.ftl @@ -0,0 +1,17 @@ +undo-undo = Undo +undo-redo = Redo +# eg "Undo Answer Card" +undo-undo-action = Undo { $val } +# eg "Answer Card Undone" +undo-action-undone = { $action } undone +undo-redo-action = Redo { $action } +undo-action-redone = { $action } redone + +## Action that can be undone + +undo-answer-card = Answer Card +undo-unbury-unsuspend = Unbury/Unsuspend +undo-add-note = Add Note +undo-update-tag = Update Tag +undo-update-note = Update Note +undo-update-card = Update Card diff --git a/ftl/duplicate-string.py b/ftl/duplicate-string.py index 32a38d2a7..9f44d8b7a 100644 --- a/ftl/duplicate-string.py +++ b/ftl/duplicate-string.py @@ -46,10 +46,10 @@ def write_entry(fname, key, entry): entry.id.name = key if not os.path.exists(fname): - return - - with open(fname) as file: - orig = file.read() + orig = "" + else: + with open(fname) as file: + orig = file.read() obj = parse(orig) for ent in obj.body: if isinstance(ent, Junk): diff --git a/ftl/qt/addons.ftl b/ftl/qt/addons.ftl index 43894125d..07ed57887 100644 --- a/ftl/qt/addons.ftl +++ b/ftl/qt/addons.ftl @@ -64,3 +64,4 @@ addons-delete-the-numd-selected-addon = [one] Delete the { $count } selected add-on? *[other] Delete the { $count } selected add-ons? } +addons-choose-update-window-title = Update Add-ons diff --git a/pylib/.pylintrc b/pylib/.pylintrc index 08be0d577..53b937e12 100644 --- a/pylib/.pylintrc +++ b/pylib/.pylintrc @@ -7,7 +7,8 @@ ignored-classes= FormatTimespanIn, AnswerCardIn, UnburyCardsInCurrentDeckIn, - BuryOrSuspendCardsIn + BuryOrSuspendCardsIn, + NoteIsDuplicateOrEmptyOut [MESSAGES CONTROL] disable=C,R, diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index bfb4a937b..e08bb46f3 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -74,19 +74,9 @@ class Card: self.flags = c.flags self.data = c.data - def _bugcheck(self) -> None: - if ( - self.queue == QUEUE_TYPE_REV - and self.odue - and not self.col.decks.isDyn(self.did) - ): - hooks.card_odue_was_invalid() - - def flush(self) -> None: - self._bugcheck() - hooks.card_will_flush(self) + def _to_backend_card(self) -> _pb.Card: # mtime & usn are set by backend - card = _pb.Card( + return _pb.Card( id=self.id, note_id=self.nid, deck_id=self.did, @@ -104,10 +94,15 @@ class Card: flags=self.flags, data=self.data, ) + + def flush(self) -> None: + hooks.card_will_flush(self) if self.id != 0: - self.col._backend.update_card(card) + self.col._backend.update_card( + card=self._to_backend_card(), skip_undo_entry=True + ) else: - self.id = self.col._backend.add_card(card) + raise Exception("card.flush() expects an existing card") def question(self, reload: bool = False, browser: bool = False) -> str: return self.render_output(reload, browser).question_and_style() @@ -141,7 +136,7 @@ class Card: def note(self, reload: bool = False) -> Note: if not self._note or reload: - self._note = self.col.getNote(self.nid) + self._note = self.col.get_note(self.nid) return self._note def note_type(self) -> NoteType: @@ -197,9 +192,14 @@ class Card: del d["timerStarted"] return f"{super().__repr__()} {pprint.pformat(d, width=300)}" - def userFlag(self) -> int: + def user_flag(self) -> int: return self.flags & 0b111 - def setUserFlag(self, flag: int) -> None: + def set_user_flag(self, flag: int) -> None: assert 0 <= flag <= 7 self.flags = (self.flags & ~0b111) | flag + + # legacy + + userFlag = user_flag + setUserFlag = set_user_flag diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index a1535ad79..6212ef34a 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -11,6 +11,7 @@ import sys import time import traceback import weakref +from dataclasses import dataclass, field from typing import Any, List, Literal, Optional, Sequence, Tuple, Union import anki._backend.backend_pb2 as _pb @@ -27,13 +28,14 @@ from anki.decks import DeckManager from anki.errors import AnkiError, DBError from anki.lang import TR, FormatTimeSpan from anki.media import MediaManager, media_paths_from_col_path -from anki.models import ModelManager +from anki.models import ModelManager, NoteType from anki.notes import Note from anki.sched import Scheduler as V1Scheduler from anki.scheduler import Scheduler as V2TestScheduler from anki.schedv2 import Scheduler as V2Scheduler from anki.sync import SyncAuth, SyncOutput, SyncStatus from anki.tags import TagManager +from anki.types import assert_exhaustive from anki.utils import ( devMode, from_json_bytes, @@ -52,11 +54,28 @@ EmptyCardsReport = _pb.EmptyCardsReport GraphPreferences = _pb.GraphPreferences BuiltinSort = _pb.SortOrder.Builtin Preferences = _pb.Preferences +UndoStatus = _pb.UndoStatus +DefaultsForAdding = _pb.DeckAndNotetype + + +@dataclass +class ReviewUndo: + card: Card + was_leech: bool + + +@dataclass +class Checkpoint: + name: str + + +@dataclass +class BackendUndo: + name: str class Collection: sched: Union[V1Scheduler, V2Scheduler] - _undo: List[Any] def __init__( self, @@ -72,9 +91,6 @@ class Collection: self.path = os.path.abspath(path) self.reopen() - self.log(self.path, anki.version) - self._lastSave = time.time() - self.clearUndo() self.media = MediaManager(self, server) self.models = ModelManager(self) self.decks = DeckManager(self) @@ -82,11 +98,6 @@ class Collection: self.conf = ConfigManager(self) self._loadScheduler() - def __repr__(self) -> str: - d = dict(self.__dict__) - del d["models"] - return f"{super().__repr__()} {pprint.pformat(d, width=300)}" - def name(self) -> Any: return os.path.splitext(os.path.basename(self.path))[0] @@ -139,59 +150,43 @@ class Collection: if ver == 1: self.sched = V1Scheduler(self) elif ver == 2: - if os.getenv("TEST_SCHEDULER"): + if self.is_2021_test_scheduler_enabled(): self.sched = V2TestScheduler(self) # type: ignore else: self.sched = V2Scheduler(self) def upgrade_to_v2_scheduler(self) -> None: self._backend.upgrade_scheduler() - self.clearUndo() + self.clear_python_undo() self._loadScheduler() + def is_2021_test_scheduler_enabled(self) -> bool: + return self.get_config_bool(Config.Bool.SCHED_2021) + + def set_2021_test_scheduler_enabled(self, enabled: bool) -> None: + if self.is_2021_test_scheduler_enabled() != enabled: + self.set_config_bool(Config.Bool.SCHED_2021, enabled) + self._loadScheduler() + # DB-related ########################################################################## # legacy properties; these will likely go away in the future - def _get_crt(self) -> int: + @property + def crt(self) -> int: return self.db.scalar("select crt from col") - def _set_crt(self, val: int) -> None: - self.db.execute("update col set crt=?", val) + @crt.setter + def crt(self, crt: int) -> None: + self.db.execute("update col set crt = ?", crt) - def _get_scm(self) -> int: - return self.db.scalar("select scm from col") - - def _set_scm(self, val: int) -> None: - self.db.execute("update col set scm=?", val) - - def _get_usn(self) -> int: - return self.db.scalar("select usn from col") - - def _set_usn(self, val: int) -> None: - self.db.execute("update col set usn=?", val) - - def _get_mod(self) -> int: + @property + def mod(self) -> int: return self.db.scalar("select mod from col") - def _set_mod(self, val: int) -> None: - self.db.execute("update col set mod=?", val) - - def _get_ls(self) -> int: - return self.db.scalar("select ls from col") - - def _set_ls(self, val: int) -> None: - self.db.execute("update col set ls=?", val) - - crt = property(_get_crt, _set_crt) - mod = property(_get_mod, _set_mod) - _usn = property(_get_usn, _set_usn) - scm = property(_get_scm, _set_scm) - ls = property(_get_ls, _set_ls) - # legacy - def setMod(self, mod: Optional[int] = None) -> None: + def setMod(self) -> None: # this is now a no-op, as modifications to things like the config # will mark the collection modified automatically pass @@ -204,15 +199,18 @@ class Collection: # to check if the backend updated the modification time. return self.db.last_begin_at != self.mod - def save( - self, name: Optional[str] = None, mod: Optional[int] = None, trx: bool = True - ) -> None: + def save(self, name: Optional[str] = None, trx: bool = True) -> None: "Flush, commit DB, and take out another write lock if trx=True." # commit needed? - if self.db.mod or self.modified_after_begin(): - self.mod = intTime(1000) if mod is None else mod + if self.db.modified_in_python or self.modified_after_begin(): + if self.db.modified_in_python: + self.db.execute("update col set mod = ?", intTime(1000)) + self.db.modified_in_python = False + else: + # modifications made by the backend will have already bumped + # mtime + pass self.db.commit() - self.db.mod = False if trx: self.db.begin() elif not trx: @@ -220,15 +218,16 @@ class Collection: # outside of a transaction, we need to roll back self.db.rollback() - self._markOp(name) - self._lastSave = time.time() + self._save_checkpoint(name) - def autosave(self) -> Optional[bool]: - "Save if 5 minutes has passed since last save. True if saved." - if time.time() - self._lastSave > 300: + def autosave(self) -> None: + """Save any pending changes. + If a checkpoint was taken in the last 5 minutes, don't save.""" + if not self._have_outstanding_checkpoint(): + # if there's no active checkpoint, we can save immediately + self.save() + elif time.time() - self._last_checkpoint_at > 300: self.save() - return True - return None def close(self, save: bool = True, downgrade: bool = False) -> None: "Disconnect from DB." @@ -237,7 +236,7 @@ class Collection: self.save(trx=False) else: self.db.rollback() - self.models._clear_cache() + self._clear_caches() self._backend.close_collection(downgrade_to_schema11=downgrade) self.db = None self.media.close() @@ -247,19 +246,26 @@ class Collection: # save and cleanup, but backend will take care of collection close if self.db: self.save(trx=False) - self.models._clear_cache() + self._clear_caches() self.db = None self.media.close() self._closeLog() def rollback(self) -> None: + self._clear_caches() self.db.rollback() self.db.begin() + def _clear_caches(self) -> None: + self.models._clear_cache() + def reopen(self, after_full_sync: bool = False) -> None: assert not self.db assert self.path.endswith(".anki2") + self._last_checkpoint_at = time.time() + self._undo: _UndoInfo = None + (media_dir, media_db) = media_paths_from_col_path(self.path) log_path = "" @@ -288,15 +294,17 @@ class Collection: if check and not hooks.schema_will_change(proceed=True): raise AnkiError("abortSchemaMod") self.scm = intTime(1000) - self.setMod() self.save() - def schemaChanged(self) -> Any: + def schemaChanged(self) -> bool: "True if schema changed since last sync." - return self.scm > self.ls + return self.db.scalar("select scm > ls from col") - def usn(self) -> Any: - return self._usn if self.server else -1 + def usn(self) -> int: + if self.server: + return self.db.scalar("select usn from col") + else: + return -1 def beforeUpload(self) -> None: "Called before a full upload." @@ -307,12 +315,25 @@ class Collection: # Object creation helpers ########################################################################## - def getCard(self, id: int) -> Card: + def get_card(self, id: int) -> Card: return Card(self, id) - def getNote(self, id: int) -> Note: + def update_card(self, card: Card) -> None: + """Save card changes to database, and add an undo entry. + Unlike card.flush(), this will invalidate any current checkpoint.""" + self._backend.update_card(card=card._to_backend_card(), skip_undo_entry=False) + + def get_note(self, id: int) -> Note: return Note(self, id=id) + def update_note(self, note: Note) -> None: + """Save note changes to database, and add an undo entry. + Unlike note.flush(), this will invalidate any current checkpoint.""" + self._backend.update_note(note=note._to_backend_note(), skip_undo_entry=False) + + getCard = get_card + getNote = get_note + # Utils ########################################################################## @@ -325,6 +346,7 @@ class Collection: def reset(self) -> None: "Rebuild the queue and reload data after DB modified." + self.autosave() self.sched.reset() # Deletion logging @@ -339,15 +361,11 @@ class Collection: # Notes ########################################################################## - def noteCount(self) -> Any: - return self.db.scalar("select count() from notes") - - def newNote(self, forDeck: bool = True) -> Note: - "Return a new note with the current model." - return Note(self, self.models.current(forDeck)) + def new_note(self, notetype: NoteType) -> Note: + return Note(self, notetype) def add_note(self, note: Note, deck_id: int) -> None: - note.id = self._backend.add_note(note=note.to_backend_note(), deck_id=deck_id) + note.id = self._backend.add_note(note=note._to_backend_note(), deck_id=deck_id) def remove_notes(self, note_ids: Sequence[int]) -> None: hooks.notes_will_be_deleted(self, note_ids) @@ -364,8 +382,44 @@ class Collection: def card_ids_of_note(self, note_id: int) -> Sequence[int]: return self._backend.cards_of_note(note_id) + def defaults_for_adding( + self, *, current_review_card: Optional[Card] + ) -> DefaultsForAdding: + """Get starting deck and notetype for add screen. + An option in the preferences controls whether this will be based on the current deck + or current notetype. + """ + if card := current_review_card: + home_deck = card.odid or card.did + else: + home_deck = 0 + + return self._backend.defaults_for_adding( + home_deck_of_current_review_card=home_deck, + ) + + def default_deck_for_notetype(self, notetype_id: int) -> Optional[int]: + """If 'change deck depending on notetype' is enabled in the preferences, + return the last deck used with the provided notetype, if any..""" + if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK): + return None + + return ( + self._backend.default_deck_for_notetype( + ntid=notetype_id, + ) + or None + ) + # legacy + def noteCount(self) -> int: + return self.db.scalar("select count() from notes") + + def newNote(self, forDeck: bool = True) -> Note: + "Return a new note with the current model." + return Note(self, self.models.current(forDeck)) + def addNote(self, note: Note) -> int: self.add_note(note, note.model()["did"]) return len(note.cards()) @@ -620,11 +674,9 @@ class Collection: return default def set_config(self, key: str, val: Any) -> None: - self.setMod() self.conf.set(key, val) def remove_config(self, key: str) -> None: - self.setMod() self.conf.remove(key) def all_config(self) -> Dict[str, Any]: @@ -635,14 +687,12 @@ class Collection: return self._backend.get_config_bool(key) def set_config_bool(self, key: Config.Bool.Key.V, value: bool) -> None: - self.setMod() self._backend.set_config_bool(key=key, value=value) def get_config_string(self, key: Config.String.Key.V) -> str: return self._backend.get_config_string(key) def set_config_string(self, key: Config.String.Key.V, value: str) -> None: - self.setMod() self._backend.set_config_string(key=key, value=value) # Stats @@ -694,13 +744,19 @@ table.review-log {{ {revlog_style} }} # Timeboxing ########################################################################## + # fixme: there doesn't seem to be a good reason why this code is in main.py + # instead of covered in reviewer, and the reps tracking is covered by both + # the scheduler and reviewer.py. in the future, we should probably move + # reps tracking to reviewer.py, and remove the startTimebox() calls from + # other locations like overview.py. We just need to make sure not to reset + # the count on things like edits, which we probably could do by checking + # the previous state in moveToState. def startTimebox(self) -> None: self._startTime = time.time() self._startReps = self.sched.reps - # FIXME: Use Literal[False] when on Python 3.8 - def timeboxReached(self) -> Union[bool, Tuple[Any, int]]: + def timeboxReached(self) -> Union[Literal[False], Tuple[Any, int]]: "Return (elapsedTime, reps) if timebox reached, or False." if not self.conf["timeLim"]: # timeboxing disabled @@ -712,88 +768,150 @@ table.review-log {{ {revlog_style} }} # Undo ########################################################################## - # this data structure is a mess, and will be updated soon - # in the review case, [1, "Review", [firstReviewedCard, secondReviewedCard, ...], wasLeech] - # in the checkpoint case, [2, "action name"] - # wasLeech should have been recorded for each card, not globally - def clearUndo(self) -> None: + def undo_status(self) -> UndoStatus: + "Return the undo status. At the moment, redo is not supported." + # check backend first + if status := self._check_backend_undo_status(): + return status + + if not self._undo: + return UndoStatus() + + if isinstance(self._undo, _ReviewsUndo): + return UndoStatus(undo=self.tr(TR.SCHEDULING_REVIEW)) + elif isinstance(self._undo, Checkpoint): + return UndoStatus(undo=self._undo.name) + else: + assert_exhaustive(self._undo) + assert False + + return status + + def clear_python_undo(self) -> None: + """Clear the Python undo state. + The backend will automatically clear backend undo state when + any SQL DML is executed, or an operation that doesn't support undo + is run.""" self._undo = None - def undoName(self) -> Any: - "Undo menu item name, or None if undo unavailable." - if not self._undo: + def undo(self) -> Union[None, BackendUndo, Checkpoint, ReviewUndo]: + """Returns ReviewUndo if undoing a v1/v2 scheduler review. + Returns None if the undo queue was empty.""" + # backend? + status = self._backend.get_undo_status() + if status.undo: + self._backend.undo() + self.clear_python_undo() + return BackendUndo(name=status.undo) + + if isinstance(self._undo, _ReviewsUndo): + return self._undo_review() + elif isinstance(self._undo, Checkpoint): + return self._undo_checkpoint() + elif self._undo is None: return None - return self._undo[1] - - def undo(self) -> Any: - if self._undo[0] == 1: - return self._undoReview() else: - self._undoOp() + assert_exhaustive(self._undo) + assert False - def markReview(self, card: Card) -> None: - old: List[Any] = [] - if self._undo: - if self._undo[0] == 1: - old = self._undo[2] - self.clearUndo() - wasLeech = card.note().hasTag("leech") or False - self._undo = [ - 1, - self.tr(TR.SCHEDULING_REVIEW), - old + [copy.copy(card)], - wasLeech, - ] + def _check_backend_undo_status(self) -> Optional[UndoStatus]: + """Return undo status if undo available on backend. + If backend has undo available, clear the Python undo state.""" + status = self._backend.get_undo_status() + if status.undo or status.redo: + self.clear_python_undo() + return status + else: + return None + + def save_card_review_undo_info(self, card: Card) -> None: + "Used by V1 and V2 schedulers to record state prior to review." + if not isinstance(self._undo, _ReviewsUndo): + self._undo = _ReviewsUndo() + + was_leech = card.note().has_tag("leech") + entry = ReviewUndo(card=copy.copy(card), was_leech=was_leech) + self._undo.entries.append(entry) + + def _have_outstanding_checkpoint(self) -> bool: + self._check_backend_undo_status() + return isinstance(self._undo, Checkpoint) + + def _undo_checkpoint(self) -> Checkpoint: + assert isinstance(self._undo, Checkpoint) + self.rollback() + undo = self._undo + self.clear_python_undo() + return undo + + def _save_checkpoint(self, name: Optional[str]) -> None: + "Call via .save(). If name not provided, clear any existing checkpoint." + self._last_checkpoint_at = time.time() + if name: + self._undo = Checkpoint(name=name) + else: + # saving disables old checkpoint, but not review undo + if not isinstance(self._undo, _ReviewsUndo): + self.clear_python_undo() + + def _undo_review(self) -> ReviewUndo: + "Undo a v1/v2 review." + assert isinstance(self._undo, _ReviewsUndo) + entry = self._undo.entries.pop() + if not self._undo.entries: + self.clear_python_undo() + + card = entry.card - def _undoReview(self) -> Any: - data = self._undo[2] - wasLeech = self._undo[3] - c = data.pop() # pytype: disable=attribute-error - if not data: - self.clearUndo() # remove leech tag if it didn't have it before - if not wasLeech and c.note().hasTag("leech"): - c.note().delTag("leech") - c.note().flush() + if not entry.was_leech and card.note().has_tag("leech"): + card.note().remove_tag("leech") + card.note().flush() + # write old data - c.flush() + card.flush() + # and delete revlog entry if not previewing - conf = self.sched._cardConf(c) + conf = self.sched._cardConf(card) previewing = conf["dyn"] and not conf["resched"] if not previewing: last = self.db.scalar( - "select id from revlog where cid = ? " "order by id desc limit 1", c.id + "select id from revlog where cid = ? " "order by id desc limit 1", + card.id, ) self.db.execute("delete from revlog where id = ?", last) + # restore any siblings self.db.execute( "update cards set queue=type,mod=?,usn=? where queue=-2 and nid=?", intTime(), self.usn(), - c.nid, + card.nid, ) - # and finally, update daily counts - n = c.queue - if c.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW): + + # update daily counts + n = card.queue + if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW): n = QUEUE_TYPE_LRN type = ("new", "lrn", "rev")[n] - self.sched._updateStats(c, type, -1) + self.sched._updateStats(card, type, -1) self.sched.reps -= 1 - return c.id - def _markOp(self, name: Optional[str]) -> None: - "Call via .save()" - if name: - self._undo = [2, name] - else: - # saving disables old checkpoint, but not review undo - if self._undo and self._undo[0] == 2: - self.clearUndo() + # and refresh the queues + self.sched.reset() - def _undoOp(self) -> None: - self.rollback() - self.clearUndo() + return entry + + # legacy + + clearUndo = clear_python_undo + markReview = save_card_review_undo_info + + def undoName(self) -> Optional[str]: + "Undo menu item name, or None if undo unavailable." + status = self.undo_status() + return status.undo or None # DB maintenance ########################################################################## @@ -869,7 +987,7 @@ table.review-log {{ {revlog_style} }} # Card Flags ########################################################################## - def setUserFlag(self, flag: int, cids: List[int]) -> None: + def set_user_flag_for_cards(self, flag: int, cids: List[int]) -> None: assert 0 <= flag <= 7 self.db.execute( "update cards set flags = (flags & ~?) | ?, usn=?, mod=? where id in %s" @@ -925,3 +1043,11 @@ table.review-log {{ {revlog_style} }} # legacy name _Collection = Collection + + +@dataclass +class _ReviewsUndo: + entries: List[ReviewUndo] = field(default_factory=list) + + +_UndoInfo = Union[_ReviewsUndo, Checkpoint, None] diff --git a/pylib/anki/dbproxy.py b/pylib/anki/dbproxy.py index 1bb4f9d95..3561613c5 100644 --- a/pylib/anki/dbproxy.py +++ b/pylib/anki/dbproxy.py @@ -24,7 +24,7 @@ class DBProxy: def __init__(self, backend: anki._backend.RustBackend) -> None: self._backend = backend - self.mod = False + self.modified_in_python = False self.last_begin_at = 0 # Transactions @@ -54,7 +54,7 @@ class DBProxy: s = sql.strip().lower() for stmt in "insert", "update", "delete": if s.startswith(stmt): - self.mod = True + self.modified_in_python = True sql, args2 = emulate_named_args(sql, args, kwargs) # fetch rows return self._backend.db_query(sql, args2, first_row_only) @@ -92,7 +92,7 @@ class DBProxy: ################ def executemany(self, sql: str, args: Iterable[Sequence[ValueForDB]]) -> None: - self.mod = True + self.modified_in_python = True if isinstance(args, list): list_args = args else: diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 0ca2b6a7c..9347f128d 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -12,7 +12,7 @@ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb from anki.consts import * -from anki.errors import DeckIsFilteredError, DeckRenameError, NotFoundError +from anki.errors import NotFoundError from anki.utils import from_json_bytes, ids2str, intTime, to_json_bytes # public exports @@ -254,12 +254,9 @@ class DeckManager: def update(self, g: Deck, preserve_usn: bool = True) -> None: "Add or update an existing deck. Used for syncing and merging." - try: - g["id"] = self.col._backend.add_or_update_deck_legacy( - deck=to_json_bytes(g), preserve_usn_and_mtime=preserve_usn - ) - except DeckIsFilteredError as exc: - raise DeckRenameError("deck was filtered") from exc + g["id"] = self.col._backend.add_or_update_deck_legacy( + deck=to_json_bytes(g), preserve_usn_and_mtime=preserve_usn + ) def rename(self, g: Deck, newName: str) -> None: "Rename deck prefix to NAME if not exists. Updates children." @@ -385,7 +382,7 @@ class DeckManager: return deck["name"] return self.col.tr(TR.DECKS_NO_DECK) - def nameOrNone(self, did: int) -> Optional[str]: + def name_if_exists(self, did: int) -> Optional[str]: deck = self.get(did, default=False) if deck: return deck["name"] @@ -573,3 +570,4 @@ class DeckManager: # legacy newDyn = new_filtered + nameOrNone = name_if_exists diff --git a/pylib/anki/errors.py b/pylib/anki/errors.py index 0fd0a5abb..7ca87996d 100644 --- a/pylib/anki/errors.py +++ b/pylib/anki/errors.py @@ -47,7 +47,15 @@ class ExistsError(Exception): pass -class DeckIsFilteredError(Exception): +class DeckRenameError(Exception): + """Legacy error, use DeckIsFilteredError instead.""" + + def __init__(self, description: str, *args: object) -> None: + super().__init__(description, *args) + self.description = description + + +class DeckIsFilteredError(StringError, DeckRenameError): pass @@ -78,7 +86,7 @@ def backend_exception_to_pylib(err: _pb.BackendError) -> Exception: elif val == "exists": return ExistsError() elif val == "deck_is_filtered": - return DeckIsFilteredError() + return DeckIsFilteredError(err.localized) elif val == "proto_error": return StringError(err.localized) else: @@ -95,12 +103,3 @@ class AnkiError(Exception): def __str__(self) -> str: return self.type - - -class DeckRenameError(Exception): - def __init__(self, description: str) -> None: - super().__init__() - self.description = description - - def __str__(self) -> str: - return f"Couldn't rename deck: {self.description}" diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index 269839a14..6a9d7fc9a 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -296,7 +296,6 @@ class AnkiExporter(Exporter): self.dst.crt = self.src.crt # todo: tags? self.count = self.dst.cardCount() - self.dst.setMod() self.postExport() self.dst.close(downgrade=True) diff --git a/pylib/anki/importing/noteimp.py b/pylib/anki/importing/noteimp.py index 7095a6408..8865c2674 100644 --- a/pylib/anki/importing/noteimp.py +++ b/pylib/anki/importing/noteimp.py @@ -250,7 +250,7 @@ class NoteImporter(Importer): self.col.tags.join(n.tags), n.fieldsStr, "", - "", + 0, 0, "", ] diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 3b6a25222..c585c1b61 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -166,7 +166,6 @@ class ModelManager: def setCurrent(self, m: NoteType) -> None: self.col.conf["curModel"] = m["id"] - self.col.setMod() # Retrieving and creating models ############################################################# diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 19b8e5fb8..b438937db 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -14,6 +14,8 @@ from anki.consts import MODEL_STD from anki.models import NoteType, Template from anki.utils import joinFields +DuplicateOrEmptyResult = _pb.NoteIsDuplicateOrEmptyOut.State + class Note: # not currently exposed @@ -53,7 +55,7 @@ class Note: self.fields = list(n.fields) self._fmap = self.col.models.fieldMap(self.model()) - def to_backend_note(self) -> _pb.Note: + def _to_backend_note(self) -> _pb.Note: hooks.note_will_flush(self) return _pb.Note( id=self.id, @@ -66,8 +68,12 @@ class Note: ) def flush(self) -> None: + """This preserves any current checkpoint. + For an undo entry, use col.update_note() instead.""" assert self.id != 0 - self.col._backend.update_note(self.to_backend_note()) + self.col._backend.update_note( + note=self._to_backend_note(), skip_undo_entry=True + ) def __repr__(self) -> str: d = dict(self.__dict__) @@ -122,7 +128,7 @@ class Note: _model = property(model) def cloze_numbers_in_fields(self) -> Sequence[int]: - return self.col._backend.cloze_numbers_in_note(self.to_backend_note()) + return self.col._backend.cloze_numbers_in_note(self._to_backend_note()) # Dict interface ################################################## @@ -154,16 +160,10 @@ class Note: # Tags ################################################## - def hasTag(self, tag: str) -> Any: + def has_tag(self, tag: str) -> bool: return self.col.tags.inList(tag, self.tags) - def stringTags(self) -> Any: - return self.col.tags.join(self.col.tags.canonify(self.tags)) - - def setTagsFromStr(self, tags: str) -> None: - self.tags = self.col.tags.split(tags) - - def delTag(self, tag: str) -> None: + def remove_tag(self, tag: str) -> None: rem = [] for t in self.tags: if t.lower() == tag.lower(): @@ -171,15 +171,26 @@ class Note: for r in rem: self.tags.remove(r) - def addTag(self, tag: str) -> None: - # duplicates will be stripped on save + def add_tag(self, tag: str) -> None: + "Add tag. Duplicates will be stripped on save." self.tags.append(tag) + def stringTags(self) -> Any: + return self.col.tags.join(self.col.tags.canonify(self.tags)) + + def setTagsFromStr(self, tags: str) -> None: + self.tags = self.col.tags.split(tags) + + hasTag = has_tag + addTag = add_tag + delTag = remove_tag + # Unique/duplicate check ################################################## - def dupeOrEmpty(self) -> int: - "1 if first is empty; 2 if first is a duplicate, 0 otherwise." + def duplicate_or_empty(self) -> DuplicateOrEmptyResult.V: return self.col._backend.note_is_duplicate_or_empty( - self.to_backend_note() + self._to_backend_note() ).state + + dupeOrEmpty = duplicate_or_empty diff --git a/pylib/anki/sched.py b/pylib/anki/sched.py index 7e98e9c6e..8eb73a00f 100644 --- a/pylib/anki/sched.py +++ b/pylib/anki/sched.py @@ -12,7 +12,7 @@ import anki from anki import hooks from anki.cards import Card from anki.consts import * -from anki.decks import Deck, QueueConfig +from anki.decks import QueueConfig from anki.schedv2 import Scheduler as V2 from anki.utils import ids2str, intTime @@ -45,10 +45,11 @@ class Scheduler(V2): def answerCard(self, card: Card, ease: int) -> None: self.col.log() assert 1 <= ease <= 4 - self.col.markReview(card) + self.col.save_card_review_undo_info(card) if self._burySiblingsOnAnswer: self._burySiblings(card) card.reps += 1 + self.reps += 1 # former is for logging new cards, latter also covers filt. decks card.wasNew = card.type == CARD_TYPE_NEW # type: ignore wasNewQ = card.queue == QUEUE_TYPE_NEW @@ -427,25 +428,6 @@ and due <= ? limit ?)""", def _deckRevLimit(self, did: int) -> int: return self._deckNewLimit(did, self._deckRevLimitSingle) - def _deckRevLimitSingle(self, d: Deck) -> int: # type: ignore[override] - if d["dyn"]: - return self.reportLimit - c = self.col.decks.confForDid(d["id"]) - limit = max(0, c["rev"]["perDay"] - self.counts_for_deck_today(d["id"]).review) - return hooks.scheduler_review_limit_for_single_deck(limit, d) - - def _revForDeck(self, did: int, lim: int) -> int: # type: ignore[override] - lim = min(lim, self.reportLimit) - return self.col.db.scalar( - f""" -select count() from -(select 1 from cards where did = ? and queue = {QUEUE_TYPE_REV} -and due <= ? limit ?)""", - did, - self.today, - lim, - ) - def _resetRev(self) -> None: self._revQueue: List[Any] = [] self._revDids = self.col.decks.active()[:] @@ -624,7 +606,7 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""", if card.lapses >= lf and (card.lapses - lf) % (max(lf // 2, 1)) == 0: # add a leech tag f = card.note() - f.addTag("leech") + f.add_tag("leech") f.flush() # handle a = conf["leechAction"] diff --git a/pylib/anki/scheduler.py b/pylib/anki/scheduler.py index bf989c005..17920aee7 100644 --- a/pylib/anki/scheduler.py +++ b/pylib/anki/scheduler.py @@ -8,458 +8,112 @@ used by Anki. from __future__ import annotations -import pprint -import random -import time from heapq import * -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, List, Optional, Sequence, Tuple, Union import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb from anki import hooks from anki.cards import Card from anki.consts import * -from anki.decks import Deck, DeckConfig, DeckManager, DeckTreeNode, QueueConfig +from anki.decks import DeckConfig, DeckTreeNode, QueueConfig from anki.notes import Note from anki.types import assert_exhaustive from anki.utils import from_json_bytes, ids2str, intTime +QueuedCards = _pb.GetQueuedCardsOut.QueuedCards CongratsInfo = _pb.CongratsInfoOut -CountsForDeckToday = _pb.CountsForDeckTodayOut SchedTimingToday = _pb.SchedTimingTodayOut UnburyCurrentDeck = _pb.UnburyCardsInCurrentDeckIn BuryOrSuspend = _pb.BuryOrSuspendCardsIn +# fixme: reviewer.cardQueue/editCurrent/undo handling/retaining current card + class Scheduler: - _burySiblingsOnAnswer = True + is_2021 = True def __init__(self, col: anki.collection.Collection) -> None: self.col = col.weakref() - self.queueLimit = 50 - self.reportLimit = 1000 - self.dynReportLimit = 99999 + # don't rely on this, it will likely be removed out in the future self.reps = 0 - self.today: Optional[int] = None - self._haveQueues = False - self._lrnCutoff = 0 - self._updateCutoff() - # Daily cutoff + # Timing ########################################################################## - def _updateCutoff(self) -> None: - timing = self._timing_today() - self.today = timing.days_elapsed - self.dayCutoff = timing.next_day_at - - def _checkDay(self) -> None: - # check if the day has rolled over - if time.time() > self.dayCutoff: - self.reset() - - def _timing_today(self) -> SchedTimingToday: + def timing_today(self) -> SchedTimingToday: return self.col._backend.sched_timing_today() + @property + def today(self) -> int: + return self.timing_today().days_elapsed + + @property + def dayCutoff(self) -> int: + return self.timing_today().next_day_at + # Fetching the next card ########################################################################## def reset(self) -> None: - self.col.decks.update_active() - self._updateCutoff() - self._reset_counts() - self._resetLrn() - self._resetRev() - self._resetNew() - self._haveQueues = True + # backend automatically resets queues as operations are performed + pass - def _reset_counts(self) -> None: - tree = self.deck_due_tree(self.col.decks.selected()) - node = self.col.decks.find_deck_in_tree(tree, int(self.col.conf["curDeck"])) - if not node: - # current deck points to a missing deck - self.newCount = 0 - self.revCount = 0 - self._immediate_learn_count = 0 + def get_queued_cards( + self, + *, + fetch_limit: int = 1, + intraday_learning_only: bool = False, + ) -> Union[QueuedCards, CongratsInfo]: + info = self.col._backend.get_queued_cards( + fetch_limit=fetch_limit, intraday_learning_only=intraday_learning_only + ) + kind = info.WhichOneof("value") + if kind == "queued_cards": + return info.queued_cards + elif kind == "congrats_info": + return info.congrats_info else: - self.newCount = node.new_count - self.revCount = node.review_count - self._immediate_learn_count = node.learn_count + assert_exhaustive(kind) + assert False def getCard(self) -> Optional[Card]: - """Pop the next card from the queue. None if finished.""" - self._checkDay() - if not self._haveQueues: - self.reset() - card = self._getCard() - if card: - self.col.log(card) - if not self._burySiblingsOnAnswer: - self._burySiblings(card) - self.reps += 1 + """Fetch the next card from the queue. None if finished.""" + response = self.get_queued_cards() + if isinstance(response, QueuedCards): + backend_card = response.cards[0].card + card = Card(self.col) + card._load_from_backend_card(backend_card) card.startTimer() return card - return None - - def _getCard(self) -> Optional[Card]: - """Return the next due card, or None.""" - # learning card due? - c = self._getLrnCard() - if c: - return c - - # new first, or time for one? - if self._timeForNewCard(): - c = self._getNewCard() - if c: - return c - - # day learning first and card due? - dayLearnFirst = self.col.conf.get("dayLearnFirst", False) - if dayLearnFirst: - c = self._getLrnDayCard() - if c: - return c - - # card due for review? - c = self._getRevCard() - if c: - return c - - # day learning card due? - if not dayLearnFirst: - c = self._getLrnDayCard() - if c: - return c - - # new cards left? - c = self._getNewCard() - if c: - return c - - # collapse or finish - return self._getLrnCard(collapse=True) - - # Fetching new cards - ########################################################################## - - def _resetNew(self) -> None: - self._newDids = self.col.decks.active()[:] - self._newQueue: List[int] = [] - self._updateNewCardRatio() - - def _fillNew(self, recursing: bool = False) -> bool: - if self._newQueue: - return True - if not self.newCount: - return False - while self._newDids: - did = self._newDids[0] - lim = min(self.queueLimit, self._deckNewLimit(did)) - if lim: - # fill the queue with the current did - self._newQueue = self.col.db.list( - f""" - select id from cards where did = ? and queue = {QUEUE_TYPE_NEW} order by due,ord limit ?""", - did, - lim, - ) - if self._newQueue: - self._newQueue.reverse() - return True - # nothing left in the deck; move to next - self._newDids.pop(0) - - # if we didn't get a card but the count is non-zero, - # we need to check again for any cards that were - # removed from the queue but not buried - if recursing: - print("bug: fillNew()") - return False - self._reset_counts() - self._resetNew() - return self._fillNew(recursing=True) - - def _getNewCard(self) -> Optional[Card]: - if self._fillNew(): - self.newCount -= 1 - return self.col.getCard(self._newQueue.pop()) - return None - - def _updateNewCardRatio(self) -> None: - if self.col.conf["newSpread"] == NEW_CARDS_DISTRIBUTE: - if self.newCount: - self.newCardModulus = (self.newCount + self.revCount) // self.newCount - # if there are cards to review, ensure modulo >= 2 - if self.revCount: - self.newCardModulus = max(2, self.newCardModulus) - return - self.newCardModulus = 0 - - def _timeForNewCard(self) -> Optional[bool]: - "True if it's time to display a new card when distributing." - if not self.newCount: - return False - if self.col.conf["newSpread"] == NEW_CARDS_LAST: - return False - elif self.col.conf["newSpread"] == NEW_CARDS_FIRST: - return True - elif self.newCardModulus: - return self.reps != 0 and self.reps % self.newCardModulus == 0 else: - # shouldn't reach return None - def _deckNewLimit( - self, did: int, fn: Optional[Callable[[Deck], int]] = None - ) -> int: - if not fn: - fn = self._deckNewLimitSingle - sel = self.col.decks.get(did) - lim = -1 - # for the deck and each of its parents - for g in [sel] + self.col.decks.parents(did): - rem = fn(g) - if lim == -1: - lim = rem - else: - lim = min(rem, lim) - return lim + def _is_finished(self) -> bool: + "Don't use this, it is a stop-gap until this code is refactored." + info = self.get_queued_cards() + return isinstance(info, CongratsInfo) - def _newForDeck(self, did: int, lim: int) -> int: - "New count for a single deck." - if not lim: - return 0 - lim = min(lim, self.reportLimit) - return self.col.db.scalar( - f""" -select count() from -(select 1 from cards where did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""", - did, - lim, - ) + def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]: + info = self.get_queued_cards() + if isinstance(info, CongratsInfo): + counts = [0, 0, 0] + else: + counts = [info.new_count, info.learning_count, info.review_count] - def _deckNewLimitSingle(self, g: DeckConfig) -> int: - "Limit for deck without parent limits." - if g["dyn"]: - return self.dynReportLimit - c = self.col.decks.confForDid(g["id"]) - limit = max(0, c["new"]["perDay"] - self.counts_for_deck_today(g["id"]).new) - return hooks.scheduler_new_limit_for_single_deck(limit, g) + return tuple(counts) # type: ignore - def totalNewForCurrentDeck(self) -> int: - return self.col.db.scalar( - f""" -select count() from cards where id in ( -select id from cards where did in %s and queue = {QUEUE_TYPE_NEW} limit ?)""" - % self._deckLimit(), - self.reportLimit, - ) + @property + def newCount(self) -> int: + return self.counts()[0] - # Fetching learning cards - ########################################################################## + @property + def lrnCount(self) -> int: + return self.counts()[1] - # scan for any newly due learning cards every minute - def _updateLrnCutoff(self, force: bool) -> bool: - nextCutoff = intTime() + self.col.conf["collapseTime"] - if nextCutoff - self._lrnCutoff > 60 or force: - self._lrnCutoff = nextCutoff - return True - return False - - def _maybeResetLrn(self, force: bool) -> None: - if self._updateLrnCutoff(force): - self._resetLrn() - - def _resetLrnCount(self) -> None: - # sub-day - self.lrnCount = ( - self.col.db.scalar( - f""" -select count() from cards where did in %s and queue = {QUEUE_TYPE_LRN} -and due < ?""" - % (self._deckLimit()), - self._lrnCutoff, - ) - or 0 - ) - # day - self.lrnCount += self.col.db.scalar( - f""" -select count() from cards where did in %s and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} -and due <= ?""" - % (self._deckLimit()), - self.today, - ) - # previews - self.lrnCount += self.col.db.scalar( - f""" -select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW} -""" - % (self._deckLimit()) - ) - - def _resetLrn(self) -> None: - self._updateLrnCutoff(force=True) - self._resetLrnCount() - self._lrnQueue: List[Tuple[int, int]] = [] - self._lrnDayQueue: List[int] = [] - self._lrnDids = self.col.decks.active()[:] - - # sub-day learning - def _fillLrn(self) -> Union[bool, List[Any]]: - if not self.lrnCount: - return False - if self._lrnQueue: - return True - cutoff = intTime() + self.col.conf["collapseTime"] - self._lrnQueue = self.col.db.all( # type: ignore - f""" -select due, id from cards where -did in %s and queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_PREVIEW}) and due < ? -limit %d""" - % (self._deckLimit(), self.reportLimit), - cutoff, - ) - for i in range(len(self._lrnQueue)): - self._lrnQueue[i] = (self._lrnQueue[i][0], self._lrnQueue[i][1]) - # as it arrives sorted by did first, we need to sort it - self._lrnQueue.sort() - return self._lrnQueue - - def _getLrnCard(self, collapse: bool = False) -> Optional[Card]: - self._maybeResetLrn(force=collapse and self.lrnCount == 0) - if self._fillLrn(): - cutoff = time.time() - if collapse: - cutoff += self.col.conf["collapseTime"] - if self._lrnQueue[0][0] < cutoff: - id = heappop(self._lrnQueue)[1] - card = self.col.getCard(id) - self.lrnCount -= 1 - return card - return None - - # daily learning - def _fillLrnDay(self) -> Optional[bool]: - if not self.lrnCount: - return False - if self._lrnDayQueue: - return True - while self._lrnDids: - did = self._lrnDids[0] - # fill the queue with the current did - self._lrnDayQueue = self.col.db.list( - f""" -select id from cards where -did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""", - did, - self.today, - self.queueLimit, - ) - if self._lrnDayQueue: - # order - r = random.Random() - r.seed(self.today) - r.shuffle(self._lrnDayQueue) - # is the current did empty? - if len(self._lrnDayQueue) < self.queueLimit: - self._lrnDids.pop(0) - return True - # nothing left in the deck; move to next - self._lrnDids.pop(0) - # shouldn't reach here - return False - - def _getLrnDayCard(self) -> Optional[Card]: - if self._fillLrnDay(): - self.lrnCount -= 1 - return self.col.getCard(self._lrnDayQueue.pop()) - return None - - # Fetching reviews - ########################################################################## - - def _currentRevLimit(self) -> int: - d = self.col.decks.get(self.col.decks.selected(), default=False) - return self._deckRevLimitSingle(d) - - def _deckRevLimitSingle( - self, d: Dict[str, Any], parentLimit: Optional[int] = None - ) -> int: - # invalid deck selected? - if not d: - return 0 - - if d["dyn"]: - return self.dynReportLimit - - c = self.col.decks.confForDid(d["id"]) - lim = max(0, c["rev"]["perDay"] - self.counts_for_deck_today(d["id"]).review) - - if parentLimit is not None: - lim = min(parentLimit, lim) - elif "::" in d["name"]: - for parent in self.col.decks.parents(d["id"]): - # pass in dummy parentLimit so we don't do parent lookup again - lim = min(lim, self._deckRevLimitSingle(parent, parentLimit=lim)) - return hooks.scheduler_review_limit_for_single_deck(lim, d) - - def _revForDeck( - self, did: int, lim: int, childMap: DeckManager.childMapNode - ) -> Any: - dids = [did] + self.col.decks.childDids(did, childMap) - lim = min(lim, self.reportLimit) - return self.col.db.scalar( - f""" -select count() from -(select 1 from cards where did in %s and queue = {QUEUE_TYPE_REV} -and due <= ? limit ?)""" - % ids2str(dids), - self.today, - lim, - ) - - def _resetRev(self) -> None: - self._revQueue: List[int] = [] - - def _fillRev(self, recursing: bool = False) -> bool: - "True if a review card can be fetched." - if self._revQueue: - return True - if not self.revCount: - return False - - lim = min(self.queueLimit, self._currentRevLimit()) - if lim: - self._revQueue = self.col.db.list( - f""" -select id from cards where -did in %s and queue = {QUEUE_TYPE_REV} and due <= ? -order by due, random() -limit ?""" - % self._deckLimit(), - self.today, - lim, - ) - - if self._revQueue: - # preserve order - self._revQueue.reverse() - return True - - if recursing: - print("bug: fillRev2()") - return False - self._reset_counts() - self._resetRev() - return self._fillRev(recursing=True) - - def _getRevCard(self) -> Optional[Card]: - if self._fillRev(): - self.revCount -= 1 - return self.col.getCard(self._revQueue.pop()) - return None + @property + def reviewCount(self) -> int: + return self.counts()[2] # Answering a card ########################################################################## @@ -468,15 +122,11 @@ limit ?""" assert 1 <= ease <= 4 assert 0 <= card.queue <= 4 - self.col.markReview(card) - - if self._burySiblingsOnAnswer: - self._burySiblings(card) - new_state = self._answerCard(card, ease) - if not self._handle_leech(card, new_state): - self._maybe_requeue_card(card) + self._handle_leech(card, new_state) + + self.reps += 1 def _answerCard(self, card: Card, ease: int) -> _pb.SchedulingState: states = self.col._backend.get_next_card_states(card.id) @@ -523,45 +173,6 @@ limit ?""" else: return False - def _maybe_requeue_card(self, card: Card) -> None: - # preview cards - if card.queue == QUEUE_TYPE_PREVIEW: - # adjust the count immediately, and rely on the once a minute - # checks to requeue it - self.lrnCount += 1 - return - - # learning cards - if not card.queue == QUEUE_TYPE_LRN: - return - if card.due >= (intTime() + self.col.conf["collapseTime"]): - return - - # card is due within collapse time, so we'll want to add it - # back to the learning queue - self.lrnCount += 1 - - # if the queue is not empty and there's nothing else to do, make - # sure we don't put it at the head of the queue and end up showing - # it twice in a row - if self._lrnQueue and not self.revCount and not self.newCount: - smallestDue = self._lrnQueue[0][0] - card.due = max(card.due, smallestDue + 1) - - heappush(self._lrnQueue, (card.due, card.id)) - - def _cardConf(self, card: Card) -> DeckConfig: - return self.col.decks.confForDid(card.did) - - def _home_config(self, card: Card) -> DeckConfig: - return self.col.decks.confForDid(card.odid or card.did) - - def _deckLimit(self) -> str: - return ids2str(self.col.decks.active()) - - def counts_for_deck_today(self, deck_id: int) -> CountsForDeckToday: - return self.col._backend.counts_for_deck_today(deck_id) - # Next times ########################################################################## # fixme: move these into tests_schedv2 in the future @@ -618,61 +229,15 @@ limit ?""" return self._interval_for_state(new_state) - # Sibling spacing - ########################################################################## - - def _burySiblings(self, card: Card) -> None: - toBury: List[int] = [] - conf = self._home_config(card) - bury_new = conf["new"].get("bury", True) - bury_rev = conf["rev"].get("bury", True) - # loop through and remove from queues - for cid, queue in self.col.db.execute( - f""" -select id, queue from cards where nid=? and id!=? -and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", - card.nid, - card.id, - self.today, - ): - if queue == QUEUE_TYPE_REV: - queue_obj = self._revQueue - if bury_rev: - toBury.append(cid) - else: - queue_obj = self._newQueue - if bury_new: - toBury.append(cid) - - # even if burying disabled, we still discard to give same-day spacing - try: - queue_obj.remove(cid) - except ValueError: - pass - # then bury - if toBury: - self.bury_cards(toBury, manual=False) - # Review-related UI helpers ########################################################################## - def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]: - counts = [self.newCount, self.lrnCount, self.revCount] - if card: - idx = self.countIdx(card) - counts[idx] += 1 - new, lrn, rev = counts - return (new, lrn, rev) - def countIdx(self, card: Card) -> int: if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW): return QUEUE_TYPE_LRN return card.queue def answerButtons(self, card: Card) -> int: - conf = self._cardConf(card) - if card.odid and not conf["resched"]: - return 2 return 4 def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str: @@ -708,18 +273,14 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", did = self.col.decks.current()["id"] self.col._backend.extend_limits(deck_id=did, new_delta=new, review_delta=rev) - def _is_finished(self) -> bool: - "Don't use this, it is a stop-gap until this code is refactored." - return not any((self.newCount, self.revCount, self._immediate_learn_count)) - + # fixme: used by custom study def totalRevForCurrentDeck(self) -> int: return self.col.db.scalar( f""" select count() from cards where id in ( -select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? limit ?)""" +select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? limit 9999)""" % self._deckLimit(), self.today, - self.reportLimit, ) # Filtered deck handling @@ -832,11 +393,6 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l ########################################################################## - def __repr__(self) -> str: - d = dict(self.__dict__) - del d["col"] - return f"{super().__repr__()} {pprint.pformat(d, width=300)}" - # unit tests def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]: return (ivl, ivl) @@ -844,13 +400,18 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l # Legacy aliases and helpers ########################################################################## + # fixme: only used by totalRevForCurrentDeck and old deck stats + def _deckLimit(self) -> str: + self.col.decks.update_active() + return ids2str(self.col.decks.active()) + def reschedCards( self, card_ids: List[int], min_interval: int, max_interval: int ) -> None: self.set_due_date(card_ids, f"{min_interval}-{max_interval}!") def buryNote(self, nid: int) -> None: - note = self.col.getNote(nid) + note = self.col.get_note(nid) self.bury_cards(note.card_ids()) def unburyCards(self) -> None: @@ -943,6 +504,12 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe ) return from_json_bytes(self.col._backend.deck_tree_legacy())[5] + def _cardConf(self, card: Card) -> DeckConfig: + return self.col.decks.confForDid(card.did) + + def _home_config(self, card: Card) -> DeckConfig: + return self.col.decks.confForDid(card.odid or card.did) + def _newConf(self, card: Card) -> QueueConfig: return self._home_config(card)["new"] diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index c152f7acc..6a73412e1 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -14,7 +14,7 @@ import anki._backend.backend_pb2 as _pb from anki import hooks from anki.cards import Card from anki.consts import * -from anki.decks import Deck, DeckConfig, DeckManager, DeckTreeNode, QueueConfig +from anki.decks import Deck, DeckConfig, DeckTreeNode, QueueConfig from anki.lang import FormatTimeSpan from anki.notes import Note from anki.utils import from_json_bytes, ids2str, intTime @@ -39,6 +39,7 @@ class Scheduler: haveCustomStudy = True _burySiblingsOnAnswer = True revCount: int + is_2021 = False def __init__(self, col: anki.collection.Collection) -> None: self.col = col.weakref() @@ -102,7 +103,6 @@ class Scheduler: self.col.log(card) if not self._burySiblingsOnAnswer: self._burySiblings(card) - self.reps += 1 card.startTimer() return card return None @@ -390,9 +390,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""", d = self.col.decks.get(self.col.decks.selected(), default=False) return self._deckRevLimitSingle(d) - def _deckRevLimitSingle( - self, d: Dict[str, Any], parentLimit: Optional[int] = None - ) -> int: + def _deckRevLimitSingle(self, d: Dict[str, Any]) -> int: # invalid deck selected? if not d: return 0 @@ -403,29 +401,8 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""", c = self.col.decks.confForDid(d["id"]) lim = max(0, c["rev"]["perDay"] - self.counts_for_deck_today(d["id"]).review) - if parentLimit is not None: - lim = min(parentLimit, lim) - elif "::" in d["name"]: - for parent in self.col.decks.parents(d["id"]): - # pass in dummy parentLimit so we don't do parent lookup again - lim = min(lim, self._deckRevLimitSingle(parent, parentLimit=lim)) return hooks.scheduler_review_limit_for_single_deck(lim, d) - def _revForDeck( - self, did: int, lim: int, childMap: DeckManager.childMapNode - ) -> Any: - dids = [did] + self.col.decks.childDids(did, childMap) - lim = min(lim, self.reportLimit) - return self.col.db.scalar( - f""" -select count() from -(select 1 from cards where did in %s and queue = {QUEUE_TYPE_REV} -and due <= ? limit ?)""" - % ids2str(dids), - self.today, - lim, - ) - def _resetRev(self) -> None: self._revQueue: List[int] = [] @@ -474,7 +451,7 @@ limit ?""" self.col.log() assert 1 <= ease <= 4 assert 0 <= card.queue <= 4 - self.col.markReview(card) + self.col.save_card_review_undo_info(card) if self._burySiblingsOnAnswer: self._burySiblings(card) @@ -489,6 +466,7 @@ limit ?""" self._answerCardPreview(card, ease) return + self.reps += 1 card.reps += 1 new_delta = 0 @@ -1100,7 +1078,7 @@ limit ?""" if card.lapses >= lf and (card.lapses - lf) % (max(lf // 2, 1)) == 0: # add a leech tag f = card.note() - f.addTag("leech") + f.add_tag("leech") f.flush() # handle a = conf["leechAction"] @@ -1345,7 +1323,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l self.set_due_date(card_ids, f"{min_interval}-{max_interval}!") def buryNote(self, nid: int) -> None: - note = self.col.getNote(nid) + note = self.col.get_note(nid) self.bury_cards(note.card_ids()) def unburyCards(self) -> None: diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index 73715fa7e..5e2ae27ce 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -88,6 +88,9 @@ class TagManager: nids=nids, tags=tags, replacement=replacement, regex=regex ) + def bulk_remove(self, nids: Sequence[int], tags: str) -> int: + return self.bulk_update(nids, tags, "", False) + def rename(self, old: str, new: str) -> int: "Rename provided tag, returning number of changed notes." nids = self.col.find_notes(anki.collection.SearchNode(tag=old)) diff --git a/pylib/anki/template.py b/pylib/anki/template.py index 94fd00a21..131633b9d 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -178,7 +178,7 @@ class TemplateRenderContext: fields["Card"] = self._template["name"] else: fields["Card"] = "" - flag = self._card.userFlag() + flag = self._card.user_flag() fields["CardFlag"] = flag and f"flag{flag}" or "" self._fields = fields @@ -239,7 +239,7 @@ class TemplateRenderContext: if self._template: # card layout screen out = self._col._backend.render_uncommitted_card( - note=self._note.to_backend_note(), + note=self._note._to_backend_note(), card_ord=self._card.ord, template=to_json_bytes(self._template), fill_empty=self._fill_empty, diff --git a/pylib/rsbridge/cargo/BUILD.bazel b/pylib/rsbridge/cargo/BUILD.bazel index b087ffd91..759fb5bf5 100644 --- a/pylib/rsbridge/cargo/BUILD.bazel +++ b/pylib/rsbridge/cargo/BUILD.bazel @@ -14,7 +14,7 @@ licenses([ # Aliased targets alias( name = "pyo3", - actual = "@raze__pyo3__0_13_1//:pyo3", + actual = "@raze__pyo3__0_13_2//:pyo3", tags = [ "cargo-raze", "manual", diff --git a/pylib/tests/test_collection.py b/pylib/tests/test_collection.py index 00824bebd..1a0c64822 100644 --- a/pylib/tests/test_collection.py +++ b/pylib/tests/test_collection.py @@ -71,15 +71,15 @@ def test_noteAddDelete(): c0 = note.cards()[0] assert "three" in c0.q() # it should not be a duplicate - assert not note.dupeOrEmpty() + assert not note.duplicate_or_empty() # now let's make a duplicate note2 = col.newNote() note2["Front"] = "one" note2["Back"] = "" - assert note2.dupeOrEmpty() + assert note2.duplicate_or_empty() # empty first field should not be permitted either note2["Front"] = " " - assert note2.dupeOrEmpty() + assert note2.duplicate_or_empty() def test_fieldChecksum(): @@ -104,13 +104,13 @@ def test_addDelTags(): note2["Front"] = "2" col.addNote(note2) # adding for a given id - col.tags.bulkAdd([note.id], "foo") + col.tags.bulk_add([note.id], "foo") note.load() note2.load() assert "foo" in note.tags assert "foo" not in note2.tags # should be canonified - col.tags.bulkAdd([note.id], "foo aaa") + col.tags.bulk_add([note.id], "foo aaa") note.load() assert note.tags[0] == "aaa" assert len(note.tags) == 2 diff --git a/pylib/tests/test_find.py b/pylib/tests/test_find.py index 0c23b0117..eb5eca906 100644 --- a/pylib/tests/test_find.py +++ b/pylib/tests/test_find.py @@ -61,7 +61,7 @@ def test_findCards(): assert len(col.findCards("tag:monkey")) == 1 assert len(col.findCards("tag:sheep -tag:monkey")) == 1 assert len(col.findCards("-tag:sheep")) == 4 - col.tags.bulkAdd(col.db.list("select id from notes"), "foo bar") + col.tags.bulk_add(col.db.list("select id from notes"), "foo bar") assert len(col.findCards("tag:foo")) == len(col.findCards("tag:bar")) == 5 col.tags.bulkRem(col.db.list("select id from notes"), "foo") assert len(col.findCards("tag:foo")) == 0 diff --git a/pylib/tests/test_flags.py b/pylib/tests/test_flags.py index 283405e81..3cb85be6f 100644 --- a/pylib/tests/test_flags.py +++ b/pylib/tests/test_flags.py @@ -13,30 +13,30 @@ def test_flags(): c.flags = origBits c.flush() # no flags to start with - assert c.userFlag() == 0 + assert c.user_flag() == 0 assert len(col.findCards("flag:0")) == 1 assert len(col.findCards("flag:1")) == 0 # set flag 2 - col.setUserFlag(2, [c.id]) + col.set_user_flag_for_cards(2, [c.id]) c.load() - assert c.userFlag() == 2 + assert c.user_flag() == 2 assert c.flags & origBits == origBits assert len(col.findCards("flag:0")) == 0 assert len(col.findCards("flag:2")) == 1 assert len(col.findCards("flag:3")) == 0 # change to 3 - col.setUserFlag(3, [c.id]) + col.set_user_flag_for_cards(3, [c.id]) c.load() - assert c.userFlag() == 3 + assert c.user_flag() == 3 # unset - col.setUserFlag(0, [c.id]) + col.set_user_flag_for_cards(0, [c.id]) c.load() - assert c.userFlag() == 0 + assert c.user_flag() == 0 # should work with Cards method as well - c.setUserFlag(2) - assert c.userFlag() == 2 - c.setUserFlag(3) - assert c.userFlag() == 3 - c.setUserFlag(0) - assert c.userFlag() == 0 + c.set_user_flag(2) + assert c.user_flag() == 2 + c.set_user_flag(3) + assert c.user_flag() == 3 + c.set_user_flag(0) + assert c.user_flag() == 0 diff --git a/pylib/tests/test_importing.py b/pylib/tests/test_importing.py index 84470b2a1..6c8bcd022 100644 --- a/pylib/tests/test_importing.py +++ b/pylib/tests/test_importing.py @@ -51,7 +51,7 @@ def test_anki2_mediadupes(): imp = Anki2Importer(empty, col.path) imp.run() assert os.listdir(empty.media.dir()) == ["foo.mp3"] - n = empty.getNote(empty.db.scalar("select id from notes")) + n = empty.get_note(empty.db.scalar("select id from notes")) assert "foo.mp3" in n.fields[0] # if the local file content is different, and import should trigger a # rename @@ -61,7 +61,7 @@ def test_anki2_mediadupes(): imp = Anki2Importer(empty, col.path) imp.run() assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid] - n = empty.getNote(empty.db.scalar("select id from notes")) + n = empty.get_note(empty.db.scalar("select id from notes")) assert "_" in n.fields[0] # if the localized media file already exists, we rewrite the note and # media @@ -72,7 +72,7 @@ def test_anki2_mediadupes(): imp.run() assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid] assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid] - n = empty.getNote(empty.db.scalar("select id from notes")) + n = empty.get_note(empty.db.scalar("select id from notes")) assert "_" in n.fields[0] @@ -162,8 +162,8 @@ def test_csv(): assert len(i.log) == 10 assert i.total == 5 # but importing should not clobber tags if they're unmapped - n = col.getNote(col.db.scalar("select id from notes")) - n.addTag("test") + n = col.get_note(col.db.scalar("select id from notes")) + n.add_tag("test") n.flush() i.run() n.load() @@ -217,7 +217,7 @@ def test_tsv_tag_modified(): n["Front"] = "1" n["Back"] = "2" n["Top"] = "3" - n.addTag("four") + n.add_tag("four") col.addNote(n) # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file @@ -253,8 +253,8 @@ def test_tsv_tag_multiple_tags(): n["Front"] = "1" n["Back"] = "2" n["Top"] = "3" - n.addTag("four") - n.addTag("five") + n.add_tag("four") + n.add_tag("five") col.addNote(n) # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file diff --git a/pylib/tests/test_models.py b/pylib/tests/test_models.py index 47e6cf55f..b0d584f6f 100644 --- a/pylib/tests/test_models.py +++ b/pylib/tests/test_models.py @@ -46,37 +46,37 @@ def test_fields(): # add a field field = col.models.newField("foo") col.models.addField(m, field) - assert col.getNote(col.models.nids(m)[0]).fields == ["1", "2", ""] + assert col.get_note(col.models.nids(m)[0]).fields == ["1", "2", ""] assert col.models.scmhash(m) != h # rename it field = m["flds"][2] col.models.renameField(m, field, "bar") - assert col.getNote(col.models.nids(m)[0])["bar"] == "" + assert col.get_note(col.models.nids(m)[0])["bar"] == "" # delete back col.models.remField(m, m["flds"][1]) - assert col.getNote(col.models.nids(m)[0]).fields == ["1", ""] + assert col.get_note(col.models.nids(m)[0]).fields == ["1", ""] # move 0 -> 1 col.models.moveField(m, m["flds"][0], 1) - assert col.getNote(col.models.nids(m)[0]).fields == ["", "1"] + assert col.get_note(col.models.nids(m)[0]).fields == ["", "1"] # move 1 -> 0 col.models.moveField(m, m["flds"][1], 0) - assert col.getNote(col.models.nids(m)[0]).fields == ["1", ""] + assert col.get_note(col.models.nids(m)[0]).fields == ["1", ""] # add another and put in middle field = col.models.newField("baz") col.models.addField(m, field) - note = col.getNote(col.models.nids(m)[0]) + note = col.get_note(col.models.nids(m)[0]) note["baz"] = "2" note.flush() - assert col.getNote(col.models.nids(m)[0]).fields == ["1", "", "2"] + assert col.get_note(col.models.nids(m)[0]).fields == ["1", "", "2"] # move 2 -> 1 col.models.moveField(m, m["flds"][2], 1) - assert col.getNote(col.models.nids(m)[0]).fields == ["1", "2", ""] + assert col.get_note(col.models.nids(m)[0]).fields == ["1", "2", ""] # move 0 -> 2 col.models.moveField(m, m["flds"][0], 2) - assert col.getNote(col.models.nids(m)[0]).fields == ["2", "", "1"] + assert col.get_note(col.models.nids(m)[0]).fields == ["2", "", "1"] # move 0 -> 1 col.models.moveField(m, m["flds"][0], 1) - assert col.getNote(col.models.nids(m)[0]).fields == ["", "2", "1"] + assert col.get_note(col.models.nids(m)[0]).fields == ["", "2", "1"] def test_templates(): diff --git a/pylib/tests/test_sched2021.py b/pylib/tests/test_sched2021.py new file mode 100644 index 000000000..5d698d2ce --- /dev/null +++ b/pylib/tests/test_sched2021.py @@ -0,0 +1,4 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from .test_schedv2 import * diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index b88d3cf97..6504147b9 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -559,6 +559,8 @@ def test_suspend(): def test_cram(): col = getEmptyCol() + opt = col.models.byName("Basic (and reversed card)") + col.models.setCurrent(opt) note = col.newNote() note["Front"] = "one" col.addNote(note) @@ -654,10 +656,9 @@ def test_cram(): c.load() assert col.sched.answerButtons(c) == 4 # add a sibling so we can test minSpace, etc - c.col = None - c2 = copy.deepcopy(c) - c2.col = c.col = col - c2.id = 0 + note["Back"] = "foo" + note.flush() + c2 = note.cards()[1] c2.ord = 1 c2.due = 325 c2.flush() diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 04c46ff33..3d39dc816 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -1,7 +1,11 @@ # coding: utf-8 import copy +import os import time +from typing import Tuple + +import pytest from anki import hooks from anki.consts import * @@ -11,9 +15,18 @@ from anki.utils import intTime from tests.shared import getEmptyCol as getEmptyColOrig +# This file is used to exercise both the legacy Python 2.1 scheduler, +# and the experimental new one in Rust. Most tests run on both, but a few +# tests have been implemented separately where the behaviour differs. +def is_2021() -> bool: + return "2021" in os.getenv("PYTEST_CURRENT_TEST") + + def getEmptyCol(): col = getEmptyColOrig() col.upgrade_to_v2_scheduler() + if is_2021(): + col.set_2021_test_scheduler_enabled(True) return col @@ -183,6 +196,7 @@ def test_learn(): c.type = CARD_TYPE_NEW c.queue = QUEUE_TYPE_LRN c.flush() + col.sched.reset() col.sched.answerCard(c, 4) assert c.type == CARD_TYPE_REV assert c.queue == QUEUE_TYPE_REV @@ -274,6 +288,9 @@ def test_learn_day(): note = col.newNote() note["Front"] = "one" col.addNote(note) + note = col.newNote() + note["Front"] = "two" + col.addNote(note) col.sched.reset() c = col.sched.getCard() conf = col.sched._cardConf(c) @@ -283,11 +300,14 @@ def test_learn_day(): col.sched.answerCard(c, 3) # two reps to graduate, 1 more today assert c.left % 1000 == 3 - assert col.sched.counts() == (0, 1, 0) - c = col.sched.getCard() + assert col.sched.counts() == (1, 1, 0) + c.load() ni = col.sched.nextIvl assert ni(c, 3) == 86400 - # answering it will place it in queue 3 + # answer the other dummy card + col.sched.answerCard(col.sched.getCard(), 4) + # answering the first one will place it in queue 3 + c = col.sched.getCard() col.sched.answerCard(c, 3) assert c.due == col.sched.today + 1 assert c.queue == QUEUE_TYPE_DAY_LEARN_RELEARN @@ -296,7 +316,11 @@ def test_learn_day(): c.due -= 1 c.flush() col.reset() - assert col.sched.counts() == (0, 1, 0) + if is_2021(): + # it appears in the review queue + assert col.sched.counts() == (0, 0, 1) + else: + assert col.sched.counts() == (0, 1, 0) c = col.sched.getCard() # nextIvl should work assert ni(c, 3) == 86400 * 2 @@ -408,7 +432,7 @@ def test_reviews(): assert "leech" in c.note().tags -def test_review_limits(): +def review_limits_setup() -> Tuple[anki.collection.Collection, Dict]: col = getEmptyCol() parent = col.decks.get(col.decks.id("parent")) @@ -442,6 +466,14 @@ def test_review_limits(): c.due = 0 c.flush() + return col, child + + +def test_review_limits(): + if is_2021(): + pytest.skip("old sched only") + col, child = review_limits_setup() + tree = col.sched.deck_due_tree().children # (('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),))) assert tree[0].review_count == 5 # parent @@ -462,6 +494,30 @@ def test_review_limits(): assert tree[0].children[0].review_count == 9 # child +def test_review_limits_new(): + if not is_2021(): + pytest.skip("new sched only") + col, child = review_limits_setup() + + tree = col.sched.deck_due_tree().children + assert tree[0].review_count == 5 # parent + assert tree[0].children[0].review_count == 5 # child capped by parent + + # child .counts() are bound by parents + col.decks.select(child["id"]) + col.sched.reset() + assert col.sched.counts() == (0, 0, 5) + + # answering a card in the child should decrement both child and parent count + c = col.sched.getCard() + col.sched.answerCard(c, 3) + assert col.sched.counts() == (0, 0, 4) + + tree = col.sched.deck_due_tree().children + assert tree[0].review_count == 4 # parent + assert tree[0].children[0].review_count == 4 # child + + def test_button_spacing(): col = getEmptyCol() note = col.newNote() @@ -801,9 +857,16 @@ def test_preview(): col.reset() # grab the first card c = col.sched.getCard() - assert col.sched.answerButtons(c) == 2 + + if is_2021(): + passing_grade = 4 + else: + passing_grade = 2 + + assert col.sched.answerButtons(c) == passing_grade assert col.sched.nextIvl(c, 1) == 600 - assert col.sched.nextIvl(c, 2) == 0 + assert col.sched.nextIvl(c, passing_grade) == 0 + # failing it will push its due time back due = c.due col.sched.answerCard(c, 1) @@ -814,7 +877,7 @@ def test_preview(): assert c2.id != c.id # passing it will remove it - col.sched.answerCard(c2, 2) + col.sched.answerCard(c2, passing_grade) assert c2.queue == QUEUE_TYPE_NEW assert c2.reps == 0 assert c2.type == CARD_TYPE_NEW @@ -851,14 +914,22 @@ def test_ordcycle(): note["Back"] = "1" col.addNote(note) assert col.cardCount() == 3 + + conf = col.decks.get_config(1) + conf["new"]["bury"] = False + col.decks.save(conf) col.reset() + # ordinals should arrive in order - assert col.sched.getCard().ord == 0 - assert col.sched.getCard().ord == 1 - assert col.sched.getCard().ord == 2 + for i in range(3): + c = col.sched.getCard() + assert c.ord == i + col.sched.answerCard(c, 4) def test_counts_idx(): + if is_2021(): + pytest.skip("old sched only") col = getEmptyCol() note = col.newNote() note["Front"] = "one" @@ -882,57 +953,88 @@ def test_counts_idx(): assert col.sched.counts() == (0, 1, 0) +def test_counts_idx_new(): + if not is_2021(): + pytest.skip("new sched only") + col = getEmptyCol() + note = col.newNote() + note["Front"] = "one" + note["Back"] = "two" + col.addNote(note) + note = col.newNote() + note["Front"] = "two" + note["Back"] = "two" + col.addNote(note) + col.reset() + assert col.sched.counts() == (2, 0, 0) + c = col.sched.getCard() + # getCard does not decrement counts + assert col.sched.counts() == (2, 0, 0) + assert col.sched.countIdx(c) == 0 + # answer to move to learn queue + col.sched.answerCard(c, 1) + assert col.sched.counts() == (1, 1, 0) + assert col.sched.countIdx(c) == 1 + # fetching next will not decrement the count + c = col.sched.getCard() + assert col.sched.counts() == (1, 1, 0) + assert col.sched.countIdx(c) == 0 + + def test_repCounts(): col = getEmptyCol() note = col.newNote() note["Front"] = "one" col.addNote(note) - col.reset() - # lrnReps should be accurate on pass/fail - assert col.sched.counts() == (1, 0, 0) - col.sched.answerCard(col.sched.getCard(), 1) - assert col.sched.counts() == (0, 1, 0) - col.sched.answerCard(col.sched.getCard(), 1) - assert col.sched.counts() == (0, 1, 0) - col.sched.answerCard(col.sched.getCard(), 3) - assert col.sched.counts() == (0, 1, 0) - col.sched.answerCard(col.sched.getCard(), 1) - assert col.sched.counts() == (0, 1, 0) - col.sched.answerCard(col.sched.getCard(), 3) - assert col.sched.counts() == (0, 1, 0) - col.sched.answerCard(col.sched.getCard(), 3) - assert col.sched.counts() == (0, 0, 0) note = col.newNote() note["Front"] = "two" col.addNote(note) col.reset() - # initial pass should be correct too - col.sched.answerCard(col.sched.getCard(), 3) - assert col.sched.counts() == (0, 1, 0) + # lrnReps should be accurate on pass/fail + assert col.sched.counts() == (2, 0, 0) col.sched.answerCard(col.sched.getCard(), 1) + assert col.sched.counts() == (1, 1, 0) + col.sched.answerCard(col.sched.getCard(), 1) + assert col.sched.counts() == (0, 2, 0) + col.sched.answerCard(col.sched.getCard(), 3) + assert col.sched.counts() == (0, 2, 0) + col.sched.answerCard(col.sched.getCard(), 1) + assert col.sched.counts() == (0, 2, 0) + col.sched.answerCard(col.sched.getCard(), 3) assert col.sched.counts() == (0, 1, 0) col.sched.answerCard(col.sched.getCard(), 4) assert col.sched.counts() == (0, 0, 0) - # immediate graduate should work note = col.newNote() note["Front"] = "three" col.addNote(note) + note = col.newNote() + note["Front"] = "four" + col.addNote(note) col.reset() + # initial pass and immediate graduate should be correct too + assert col.sched.counts() == (2, 0, 0) + col.sched.answerCard(col.sched.getCard(), 3) + assert col.sched.counts() == (1, 1, 0) + col.sched.answerCard(col.sched.getCard(), 4) + assert col.sched.counts() == (0, 1, 0) col.sched.answerCard(col.sched.getCard(), 4) assert col.sched.counts() == (0, 0, 0) # and failing a review should too note = col.newNote() - note["Front"] = "three" + note["Front"] = "five" col.addNote(note) c = note.cards()[0] c.type = CARD_TYPE_REV c.queue = QUEUE_TYPE_REV c.due = col.sched.today c.flush() + note = col.newNote() + note["Front"] = "six" + col.addNote(note) col.reset() - assert col.sched.counts() == (0, 0, 1) + assert col.sched.counts() == (1, 0, 1) col.sched.answerCard(col.sched.getCard(), 1) - assert col.sched.counts() == (0, 1, 0) + assert col.sched.counts() == (1, 1, 0) def test_timing(): @@ -968,12 +1070,25 @@ def test_collapse(): note = col.newNote() note["Front"] = "one" col.addNote(note) + # and another, so we don't get the same twice in a row + note = col.newNote() + note["Front"] = "two" + col.addNote(note) col.reset() - # test collapsing + # first note c = col.sched.getCard() col.sched.answerCard(c, 1) - c = col.sched.getCard() - col.sched.answerCard(c, 4) + # second note + c2 = col.sched.getCard() + assert c2.nid != c.nid + col.sched.answerCard(c2, 1) + # first should become available again, despite it being due in the future + c3 = col.sched.getCard() + assert c3.due > intTime() + col.sched.answerCard(c3, 4) + # answer other + c4 = col.sched.getCard() + col.sched.answerCard(c4, 4) assert not col.sched.getCard() @@ -1049,13 +1164,20 @@ def test_deckFlow(): note["Front"] = "three" default1 = note.model()["did"] = col.decks.id("Default::1") col.addNote(note) - # should get top level one first, then ::1, then ::2 col.reset() assert col.sched.counts() == (3, 0, 0) - for i in "one", "three", "two": - c = col.sched.getCard() - assert c.note()["Front"] == i - col.sched.answerCard(c, 3) + if is_2021(): + # cards arrive in position order by default + for i in "one", "two", "three": + c = col.sched.getCard() + assert c.note()["Front"] == i + col.sched.answerCard(c, 3) + else: + # should get top level one first, then ::1, then ::2 + for i in "one", "three", "two": + c = col.sched.getCard() + assert c.note()["Front"] == i + col.sched.answerCard(c, 3) def test_reorder(): @@ -1120,13 +1242,13 @@ def test_resched(): note["Front"] = "one" col.addNote(note) c = note.cards()[0] - col.sched.reschedCards([c.id], 0, 0) + col.sched.set_due_date([c.id], "0") c.load() assert c.due == col.sched.today assert c.ivl == 1 assert c.queue == QUEUE_TYPE_REV and c.type == CARD_TYPE_REV # make it due tomorrow - col.sched.reschedCards([c.id], 1, 1) + col.sched.set_due_date([c.id], "1") c.load() assert c.due == col.sched.today + 1 assert c.ivl == 1 diff --git a/pylib/tests/test_undo.py b/pylib/tests/test_undo.py index eae507fec..7bfaa2381 100644 --- a/pylib/tests/test_undo.py +++ b/pylib/tests/test_undo.py @@ -22,7 +22,7 @@ def test_op(): # it should be listed as undoable assert col.undoName() == "studyopts" # with about 5 minutes until it's clobbered - assert time.time() - col._lastSave < 1 + assert time.time() - col._last_checkpoint_at < 1 # undoing should restore the old value col.undo() assert not col.undoName() @@ -38,7 +38,7 @@ def test_op(): note["Front"] = "one" col.addNote(note) col.reset() - assert col.undoName() == "add" + assert "add" in col.undoName().lower() c = col.sched.getCard() col.sched.answerCard(c, 2) assert col.undoName() == "Review" @@ -50,31 +50,28 @@ def test_review(): note = col.newNote() note["Front"] = "one" col.addNote(note) + note = col.newNote() + note["Front"] = "two" + col.addNote(note) col.reset() - assert not col.undoName() # answer - assert col.sched.counts() == (1, 0, 0) + assert col.sched.counts() == (2, 0, 0) c = col.sched.getCard() assert c.queue == QUEUE_TYPE_NEW col.sched.answerCard(c, 3) assert c.left % 1000 == 1 - assert col.sched.counts() == (0, 1, 0) + assert col.sched.counts() == (1, 1, 0) assert c.queue == QUEUE_TYPE_LRN # undo assert col.undoName() col.undo() col.reset() - assert col.sched.counts() == (1, 0, 0) + assert col.sched.counts() == (2, 0, 0) c.load() assert c.queue == QUEUE_TYPE_NEW assert c.left % 1000 != 1 assert not col.undoName() # we should be able to undo multiple answers too - note = col.newNote() - note["Front"] = "two" - col.addNote(note) - col.reset() - assert col.sched.counts() == (2, 0, 0) c = col.sched.getCard() col.sched.answerCard(c, 3) c = col.sched.getCard() diff --git a/qt/aqt/about.py b/qt/aqt/about.py index d49a21a36..04d577c2d 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -28,7 +28,7 @@ class ClosableQDialog(QDialog): def show(mw: aqt.AnkiQt) -> QDialog: dialog = ClosableQDialog(mw) disable_help_button(dialog) - mw.setupDialogGC(dialog) + mw.garbage_collect_on_dialog_finish(dialog) abt = aqt.forms.about.Ui_About() abt.setupUi(dialog) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index c719d4bbb..6c38f16e3 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -1,17 +1,18 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import Any, Callable, List, Optional + +from typing import Callable, List, Optional import aqt.deckchooser import aqt.editor import aqt.forms -import aqt.modelchooser from anki.collection import SearchNode from anki.consts import MODEL_CLOZE -from anki.notes import Note +from anki.notes import DuplicateOrEmptyResult, Note from anki.utils import htmlToTextLine, isMac from aqt import AnkiQt, gui_hooks from aqt.main import ResetReason +from aqt.notetypechooser import NoteTypeChooser from aqt.qt import * from aqt.sound import av_player from aqt.utils import ( @@ -34,7 +35,7 @@ from aqt.utils import ( class AddCards(QDialog): def __init__(self, mw: AnkiQt) -> None: QDialog.__init__(self, None, Qt.Window) - mw.setupDialogGC(self) + mw.garbage_collect_on_dialog_finish(self) self.mw = mw self.form = aqt.forms.addcards.Ui_Dialog() self.form.setupUi(self) @@ -42,15 +43,13 @@ class AddCards(QDialog): disable_help_button(self) self.setMinimumHeight(300) self.setMinimumWidth(400) - self.setupChoosers() + self.setup_choosers() self.setupEditor() self.setupButtons() - self.onReset() + self._load_new_note() self.history: List[int] = [] - self.previousNote: Optional[Note] = None + self._last_added_note: Optional[Note] = None restoreGeom(self, "add") - gui_hooks.state_did_reset.append(self.onReset) - gui_hooks.current_note_type_did_change.append(self.onModelChange) addCloseShortcut(self) gui_hooks.add_cards_did_init(self) self.show() @@ -58,11 +57,20 @@ class AddCards(QDialog): def setupEditor(self) -> None: self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self, True) - def setupChoosers(self) -> None: - self.modelChooser = aqt.modelchooser.ModelChooser( - self.mw, self.form.modelArea, on_activated=self.show_notetype_selector + def setup_choosers(self) -> None: + defaults = self.mw.col.defaults_for_adding( + current_review_card=self.mw.reviewer.card + ) + self.notetype_chooser = NoteTypeChooser( + mw=self.mw, + widget=self.form.modelArea, + starting_notetype_id=defaults.notetype_id, + on_button_activated=self.show_notetype_selector, + on_notetype_changed=self.on_notetype_change, + ) + self.deck_chooser = aqt.deckchooser.DeckChooser( + self.mw, self.form.deckArea, starting_deck_id=defaults.deck_id ) - self.deckChooser = aqt.deckchooser.DeckChooser(self.mw, self.form.deckArea) def helpRequested(self) -> None: openHelp(HelpPage.ADDING_CARD_AND_NOTE) @@ -72,7 +80,7 @@ class AddCards(QDialog): ar = QDialogButtonBox.ActionRole # add self.addButton = bb.addButton(tr(TR.ACTIONS_ADD), ar) - qconnect(self.addButton.clicked, self.addCards) + qconnect(self.addButton.clicked, self.add_current_note) self.addButton.setShortcut(QKeySequence("Ctrl+Return")) self.addButton.setToolTip(shortcut(tr(TR.ADDING_ADD_SHORTCUT_CTRLANDENTER))) # close @@ -99,42 +107,52 @@ class AddCards(QDialog): self.editor.setNote(note, focusTo=0) def show_notetype_selector(self) -> None: - self.editor.saveNow(self.modelChooser.onModelChange) + self.editor.saveNow(self.notetype_chooser.choose_notetype) - def onModelChange(self, unused: Any = None) -> None: - oldNote = self.editor.note - note = self.mw.col.newNote() - self.previousNote = None - if oldNote: - oldFields = list(oldNote.keys()) - newFields = list(note.keys()) - for n, f in enumerate(note.model()["flds"]): - fieldName = f["name"] + def on_notetype_change(self, notetype_id: int) -> None: + # need to adjust current deck? + if deck_id := self.mw.col.default_deck_for_notetype(notetype_id): + self.deck_chooser.selected_deck_id = deck_id + + # only used for detecting changed sticky fields on close + self._last_added_note = None + + # copy fields into new note with the new notetype + old = self.editor.note + new = self._new_note() + if old: + old_fields = list(old.keys()) + new_fields = list(new.keys()) + for n, f in enumerate(new.model()["flds"]): + field_name = f["name"] # copy identical fields - if fieldName in oldFields: - note[fieldName] = oldNote[fieldName] - elif n < len(oldNote.model()["flds"]): + if field_name in old_fields: + new[field_name] = old[field_name] + elif n < len(old.model()["flds"]): # set non-identical fields by field index - oldFieldName = oldNote.model()["flds"][n]["name"] - if oldFieldName not in newFields: - note.fields[n] = oldNote.fields[n] - self.editor.note = note - # When on model change is called, reset is necessarily called. - # Reset load note, so it is not required to load it here. + old_field_name = old.model()["flds"][n]["name"] + if old_field_name not in new_fields: + new.fields[n] = old.fields[n] - def onReset(self, model: None = None, keep: bool = False) -> None: - oldNote = self.editor.note - note = self.mw.col.newNote() - flds = note.model()["flds"] - # copy fields from old note - if oldNote: - for n in range(min(len(note.fields), len(oldNote.fields))): - if not keep or flds[n]["sticky"]: - note.fields[n] = oldNote.fields[n] + # and update editor state + self.editor.note = new + self.editor.loadNote() + + def _load_new_note(self, sticky_fields_from: Optional[Note] = None) -> None: + note = self._new_note() + if old_note := sticky_fields_from: + flds = note.model()["flds"] + # copy fields from old note + if old_note: + for n in range(min(len(note.fields), len(old_note.fields))): + if flds[n]["sticky"]: + note.fields[n] = old_note.fields[n] self.setAndFocusNote(note) - def removeTempNote(self, note: Note) -> None: - print("removeTempNote() will go away") + def _new_note(self) -> Note: + return self.mw.col.new_note( + self.mw.col.models.get(self.notetype_chooser.selected_notetype_id) + ) def addHistory(self, note: Note) -> None: self.history.insert(0, note.id) @@ -145,7 +163,7 @@ class AddCards(QDialog): m = QMenu(self) for nid in self.history: if self.mw.col.findNotes(SearchNode(nid=nid)): - note = self.mw.col.getNote(nid) + note = self.mw.col.get_note(nid) fields = note.fields txt = htmlToTextLine(", ".join(fields)) if len(txt) > 30: @@ -163,43 +181,55 @@ class AddCards(QDialog): def editHistory(self, nid: int) -> None: aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),)) - def addNote(self, note: Note) -> Optional[Note]: - note.model()["did"] = self.deckChooser.selectedId() - ret = note.dupeOrEmpty() - problem = None - if ret == 1: - problem = tr(TR.ADDING_THE_FIRST_FIELD_IS_EMPTY) - problem = gui_hooks.add_cards_will_add_note(problem, note) - if problem is not None: - showWarning(problem, help=HelpPage.ADDING_CARD_AND_NOTE) - return None - if note.model()["type"] == MODEL_CLOZE: - if not note.cloze_numbers_in_fields(): - if not askUser(tr(TR.ADDING_YOU_HAVE_A_CLOZE_DELETION_NOTE)): - return None - self.mw.col.add_note(note, self.deckChooser.selectedId()) - self.mw.col.clearUndo() - self.addHistory(note) - self.previousNote = note - self.mw.requireReset(reason=ResetReason.AddCardsAddNote, context=self) - gui_hooks.add_cards_did_add_note(note) - return note + def add_current_note(self) -> None: + self.editor.saveNow(self._add_current_note) - def addCards(self) -> None: - self.editor.saveNow(self._addCards) + def _add_current_note(self) -> None: + note = self.editor.note - def _addCards(self) -> None: - self.editor.saveAddModeVars() - if not self.addNote(self.editor.note): + if not self._note_can_be_added(note): return + target_deck_id = self.deck_chooser.selected_deck_id + self.mw.col.add_note(note, target_deck_id) + + # only used for detecting changed sticky fields on close + self._last_added_note = note + + self.addHistory(note) + self.mw.requireReset(reason=ResetReason.AddCardsAddNote, context=self) + # workaround for PyQt focus bug self.editor.hideCompleters() tooltip(tr(TR.ADDING_ADDED), period=500) av_player.stop_and_clear_queue() - self.onReset(keep=True) - self.mw.col.autosave() + self._load_new_note(sticky_fields_from=note) + self.mw.col.autosave() # fixme: + + gui_hooks.add_cards_did_add_note(note) + + def _note_can_be_added(self, note: Note) -> bool: + result = note.duplicate_or_empty() + if result == DuplicateOrEmptyResult.EMPTY: + problem = tr(TR.ADDING_THE_FIRST_FIELD_IS_EMPTY) + else: + # duplicate entries are allowed these days + problem = None + + # filter problem through add-ons + problem = gui_hooks.add_cards_will_add_note(problem, note) + if problem is not None: + showWarning(problem, help=HelpPage.ADDING_CARD_AND_NOTE) + return False + + # missing cloze deletion? + if note.model()["type"] == MODEL_CLOZE: + if not note.cloze_numbers_in_fields(): + if not askUser(tr(TR.ADDING_YOU_HAVE_A_CLOZE_DELETION_NOTE)): + return False + + return True def keyPressEvent(self, evt: QKeyEvent) -> None: "Show answer on RET or register answer." @@ -212,12 +242,9 @@ class AddCards(QDialog): self.ifCanClose(self._reject) def _reject(self) -> None: - gui_hooks.state_did_reset.remove(self.onReset) - gui_hooks.current_note_type_did_change.remove(self.onModelChange) av_player.stop_and_clear_queue() self.editor.cleanup() - self.modelChooser.cleanup() - self.deckChooser.cleanup() + self.notetype_chooser.cleanup() self.mw.maybeReset() saveGeom(self, "add") aqt.dialogs.markClosed("AddCards") @@ -225,7 +252,7 @@ class AddCards(QDialog): def ifCanClose(self, onOk: Callable) -> None: def afterSave() -> None: - ok = self.editor.fieldsAreBlank(self.previousNote) or askUser( + ok = self.editor.fieldsAreBlank(self._last_added_note) or askUser( tr(TR.ADDING_CLOSE_AND_LOSE_CURRENT_INPUT), defaultno=True ) if ok: @@ -239,3 +266,15 @@ class AddCards(QDialog): cb() self.ifCanClose(doClose) + + # legacy aliases + + addCards = add_current_note + _addCards = _add_current_note + onModelChange = on_notetype_change + + def addNote(self, note: Note) -> None: + print("addNote() is obsolete") + + def removeTempNote(self, note: Note) -> None: + print("removeTempNote() will go away") diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 0166276c9..e50ac35db 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -10,6 +10,7 @@ import zipfile from collections import defaultdict from concurrent.futures import Future from dataclasses import dataclass +from datetime import datetime from typing import IO, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union from urllib.parse import parse_qs, urlparse from zipfile import ZipFile @@ -104,6 +105,7 @@ class AddonMeta: max_point_version: int branch_index: int human_version: Optional[str] + update_enabled: bool def human_name(self) -> str: return self.provided_name or self.dir_name @@ -139,6 +141,7 @@ class AddonMeta: max_point_version=json_meta.get("max_point_version", 0) or 0, branch_index=json_meta.get("branch_index", 0) or 0, human_version=json_meta.get("human_version"), + update_enabled=json_meta.get("update_enabled", True), ) @@ -242,6 +245,7 @@ class AddonManager: json_obj["branch_index"] = addon.branch_index if addon.human_version is not None: json_obj["human_version"] = addon.human_version + json_obj["update_enabled"] = addon.update_enabled self.writeAddonMeta(addon.dir_name, json_obj) @@ -551,19 +555,19 @@ class AddonManager: if updated: self.write_addon_meta(addon) - def updates_required(self, items: List[UpdateInfo]) -> List[int]: + def updates_required(self, items: List[UpdateInfo]) -> List[UpdateInfo]: """Return ids of add-ons requiring an update.""" need_update = [] for item in items: addon = self.addon_meta(str(item.id)) # update if server mtime is newer if not addon.is_latest(item.suitable_branch_last_modified): - need_update.append(item.id) + need_update.append(item) elif not addon.compatible() and item.suitable_branch_last_modified > 0: # Addon is currently disabled, and a suitable branch was found on the # server. Ignore our stored mtime (which may have been set incorrectly # in the past) and require an update. - need_update.append(item.id) + need_update.append(item) return need_update @@ -1132,6 +1136,163 @@ def download_addons( ###################################################################### +class ChooseAddonsToUpdateList(QListWidget): + ADDON_ID_ROLE = 101 + + def __init__( + self, + parent: QWidget, + mgr: AddonManager, + updated_addons: List[UpdateInfo], + ) -> None: + QListWidget.__init__(self, parent) + self.mgr = mgr + self.updated_addons = sorted( + updated_addons, key=lambda addon: addon.suitable_branch_last_modified + ) + self.ignore_check_evt = False + self.setup() + self.setContextMenuPolicy(Qt.CustomContextMenu) + qconnect(self.itemClicked, self.on_click) + qconnect(self.itemChanged, self.on_check) + qconnect(self.itemDoubleClicked, self.on_double_click) + qconnect(self.customContextMenuRequested, self.on_context_menu) + + def setup(self) -> None: + header_item = QListWidgetItem("", self) + header_item.setFlags(Qt.ItemFlag(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)) + header_item.setBackground(Qt.lightGray) + self.header_item = header_item + for update_info in self.updated_addons: + addon_id = update_info.id + addon_meta = self.mgr.addon_meta(str(addon_id)) + update_enabled = addon_meta.update_enabled + addon_name = addon_meta.human_name() + update_timestamp = update_info.suitable_branch_last_modified + update_time = datetime.fromtimestamp(update_timestamp) + + addon_label = f"{update_time:%Y-%m-%d} {addon_name}" + item = QListWidgetItem(addon_label, self) + # Not user checkable because it overlaps with itemClicked signal + item.setFlags(Qt.ItemFlag(Qt.ItemIsEnabled)) + if update_enabled: + item.setCheckState(Qt.Checked) + else: + item.setCheckState(Qt.Unchecked) + item.setData(self.ADDON_ID_ROLE, addon_id) + self.refresh_header_check_state() + + def bool_to_check(self, check_bool: bool) -> Qt.CheckState: + if check_bool: + return Qt.Checked + else: + return Qt.Unchecked + + def checked(self, item: QListWidgetItem) -> bool: + return item.checkState() == Qt.Checked + + def on_click(self, item: QListWidgetItem) -> None: + if item == self.header_item: + return + checked = self.checked(item) + self.check_item(item, self.bool_to_check(not checked)) + self.refresh_header_check_state() + + def on_check(self, item: QListWidgetItem) -> None: + if self.ignore_check_evt: + return + if item == self.header_item: + self.header_checked(item.checkState()) + + def on_double_click(self, item: QListWidgetItem) -> None: + if item == self.header_item: + checked = self.checked(item) + self.check_item(self.header_item, self.bool_to_check(not checked)) + self.header_checked(self.bool_to_check(not checked)) + + def on_context_menu(self, point: QPoint) -> None: + item = self.itemAt(point) + addon_id = item.data(self.ADDON_ID_ROLE) + m = QMenu() + a = m.addAction(tr(TR.ADDONS_VIEW_ADDON_PAGE)) + qconnect(a.triggered, lambda _: openLink(f"{aqt.appShared}info/{addon_id}")) + m.exec_(QCursor.pos()) + + def check_item(self, item: QListWidgetItem, check: Qt.CheckState) -> None: + "call item.setCheckState without triggering on_check" + self.ignore_check_evt = True + item.setCheckState(check) + self.ignore_check_evt = False + + def header_checked(self, check: Qt.CheckState) -> None: + for i in range(1, self.count()): + self.check_item(self.item(i), check) + + def refresh_header_check_state(self) -> None: + for i in range(1, self.count()): + item = self.item(i) + if not self.checked(item): + self.check_item(self.header_item, Qt.Unchecked) + return + self.check_item(self.header_item, Qt.Checked) + + def get_selected_addon_ids(self) -> List[int]: + addon_ids = [] + for i in range(1, self.count()): + item = self.item(i) + if self.checked(item): + addon_id = item.data(self.ADDON_ID_ROLE) + addon_ids.append(addon_id) + return addon_ids + + def save_check_state(self) -> None: + for i in range(1, self.count()): + item = self.item(i) + addon_id = item.data(self.ADDON_ID_ROLE) + addon_meta = self.mgr.addon_meta(str(addon_id)) + addon_meta.update_enabled = self.checked(item) + self.mgr.write_addon_meta(addon_meta) + + +class ChooseAddonsToUpdateDialog(QDialog): + def __init__( + self, parent: QWidget, mgr: AddonManager, updated_addons: List[UpdateInfo] + ) -> None: + QDialog.__init__(self, parent) + self.setWindowTitle(tr(TR.ADDONS_CHOOSE_UPDATE_WINDOW_TITLE)) + self.setWindowModality(Qt.WindowModal) + self.mgr = mgr + self.updated_addons = updated_addons + self.setup() + restoreGeom(self, "addonsChooseUpdate") + + def setup(self) -> None: + layout = QVBoxLayout() + label = QLabel(tr(TR.ADDONS_THE_FOLLOWING_ADDONS_HAVE_UPDATES_AVAILABLE)) + layout.addWidget(label) + addons_list_widget = ChooseAddonsToUpdateList( + self, self.mgr, self.updated_addons + ) + layout.addWidget(addons_list_widget) + self.addons_list_widget = addons_list_widget + + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) # type: ignore + qconnect(button_box.button(QDialogButtonBox.Ok).clicked, self.accept) + qconnect(button_box.button(QDialogButtonBox.Cancel).clicked, self.reject) + layout.addWidget(button_box) + self.setLayout(layout) + + def ask(self) -> List[int]: + "Returns a list of selected addons' ids" + ret = self.exec_() + saveGeom(self, "addonsChooseUpdate") + self.addons_list_widget.save_check_state() + if ret == QDialog.Accepted: + return self.addons_list_widget.get_selected_addon_ids() + else: + return [] + + def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]: """Fetch update info from AnkiWeb in one or more batches.""" all_info: List[Dict] = [] @@ -1164,9 +1325,10 @@ def check_and_prompt_for_updates( parent: QWidget, mgr: AddonManager, on_done: Callable[[List[DownloadLogEntry]], None], + requested_by_user: bool = True, ) -> None: def on_updates_received(client: HttpClient, items: List[Dict]) -> None: - handle_update_info(parent, mgr, client, items, on_done) + handle_update_info(parent, mgr, client, items, on_done, requested_by_user) check_for_updates(mgr, on_updates_received) @@ -1235,35 +1397,39 @@ def handle_update_info( client: HttpClient, items: List[Dict], on_done: Callable[[List[DownloadLogEntry]], None], + requested_by_user: bool = True, ) -> None: update_info = mgr.extract_update_info(items) mgr.update_supported_versions(update_info) - updated_ids = mgr.updates_required(update_info) + updated_addons = mgr.updates_required(update_info) - if not updated_ids: + if not updated_addons: on_done([]) return - prompt_to_update(parent, mgr, client, updated_ids, on_done) + prompt_to_update(parent, mgr, client, updated_addons, on_done, requested_by_user) def prompt_to_update( parent: QWidget, mgr: AddonManager, client: HttpClient, - ids: List[int], + updated_addons: List[UpdateInfo], on_done: Callable[[List[DownloadLogEntry]], None], + requested_by_user: bool = True, ) -> None: - names = map(lambda x: mgr.addonName(str(x)), ids) - if not askUser( - tr(TR.ADDONS_THE_FOLLOWING_ADDONS_HAVE_UPDATES_AVAILABLE) - + "\n\n" - + "\n".join(names) - ): - # on_done is not called if the user cancels - return + if not requested_by_user: + prompt_update = False + for addon in updated_addons: + if mgr.addon_meta(str(addon.id)).update_enabled: + prompt_update = True + if not prompt_update: + return + ids = ChooseAddonsToUpdateDialog(parent, mgr, updated_addons).ask() + if not ids: + return download_addons(parent, mgr, ids, on_done, client) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 43641d488..d5970c032 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -411,9 +411,9 @@ class StatusDelegate(QItemDelegate): option.direction = Qt.RightToLeft col = None - if c.userFlag() > 0: - col = getattr(colors, f"FLAG{c.userFlag()}_BG") - elif c.note().hasTag("Marked"): + if c.user_flag() > 0: + col = getattr(colors, f"FLAG{c.user_flag()}_BG") + elif c.note().has_tag("Marked"): col = colors.MARKED_BG elif c.queue == QUEUE_TYPE_SUSPENDED: col = colors.SUSPENDED_BG @@ -490,8 +490,11 @@ class Browser(QMainWindow): f.actionCreateFilteredDeck.setShortcuts(["Ctrl+G", "Ctrl+Alt+G"]) # notes qconnect(f.actionAdd.triggered, self.mw.onAddCard) - qconnect(f.actionAdd_Tags.triggered, lambda: self.addTags()) - qconnect(f.actionRemove_Tags.triggered, lambda: self.deleteTags()) + qconnect(f.actionAdd_Tags.triggered, lambda: self.add_tags_to_selected_notes()) + qconnect( + f.actionRemove_Tags.triggered, + lambda: self.remove_tags_from_selected_notes(), + ) qconnect(f.actionClear_Unused_Tags.triggered, self.clearUnusedTags) qconnect(f.actionToggle_Mark.triggered, lambda: self.onMark()) qconnect(f.actionChangeModel.triggered, self.onChangeModel) @@ -505,7 +508,7 @@ class Browser(QMainWindow): qconnect(f.actionReposition.triggered, self.reposition) qconnect(f.action_set_due_date.triggered, self.set_due_date) qconnect(f.action_forget.triggered, self.forget_cards) - qconnect(f.actionToggle_Suspend.triggered, self.onSuspend) + qconnect(f.actionToggle_Suspend.triggered, self.suspend_selected_cards) qconnect(f.actionRed_Flag.triggered, lambda: self.onSetFlag(1)) qconnect(f.actionOrange_Flag.triggered, lambda: self.onSetFlag(2)) qconnect(f.actionGreen_Flag.triggered, lambda: self.onSetFlag(3)) @@ -575,7 +578,7 @@ class Browser(QMainWindow): self.mw.maybeReset() aqt.dialogs.markClosed("Browser") self._closeEventHasCleanedUp = True - self.mw.gcWindow(self) + self.mw.deferred_delete_and_garbage_collect(self) self.close() def closeWithCallback(self, onsuccess: Callable) -> None: @@ -855,13 +858,11 @@ QTableView {{ gridline-color: {grid} }} if type == "noteFld": ord = not ord self.col.set_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS, ord) - self.col.setMod() self.col.save() self.search() else: if self.col.get_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS) != ord: self.col.set_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS, ord) - self.col.setMod() self.col.save() self.model.reverse() self.setSortIndicator() @@ -1197,53 +1198,46 @@ where id in %s""" # Tags ###################################################################### - def addTags( + def add_tags_to_selected_notes( self, tags: Optional[str] = None, - label: Optional[str] = None, - prompt: Optional[str] = None, - func: Optional[Callable] = None, ) -> None: - self.editor.saveNow(lambda: self._addTags(tags, label, prompt, func)) + "Shows prompt if tags not provided." + self.editor.saveNow( + lambda: self._update_tags_of_selected_notes( + func=self.col.tags.bulk_add, + tags=tags, + prompt=tr(TR.BROWSING_ENTER_TAGS_TO_ADD), + ) + ) - def _addTags( + def remove_tags_from_selected_notes(self, tags: Optional[str] = None) -> None: + "Shows prompt if tags not provided." + self.editor.saveNow( + lambda: self._update_tags_of_selected_notes( + func=self.col.tags.bulk_remove, + tags=tags, + prompt=tr(TR.BROWSING_ENTER_TAGS_TO_DELETE), + ) + ) + + def _update_tags_of_selected_notes( self, + func: Callable[[List[int], str], int], tags: Optional[str], - label: Optional[str], prompt: Optional[str], - func: Optional[Callable], ) -> None: - if prompt is None: - prompt = tr(TR.BROWSING_ENTER_TAGS_TO_ADD) + "If tags provided, prompt skipped. If tags not provided, prompt must be." if tags is None: - (tags, r) = getTag(self, self.col, prompt) - else: - r = True - if not r: - return - if func is None: - func = self.col.tags.bulkAdd - if label is None: - label = tr(TR.BROWSING_ADD_TAGS) - if label: - self.mw.checkpoint(label) + (tags, ok) = getTag(self, self.col, prompt) + if not ok: + return + self.model.beginReset() func(self.selectedNotes(), tags) self.model.endReset() self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self) - def deleteTags( - self, tags: Optional[str] = None, label: Optional[str] = None - ) -> None: - if label is None: - label = tr(TR.BROWSING_DELETE_TAGS) - self.addTags( - tags, - label, - tr(TR.BROWSING_ENTER_TAGS_TO_DELETE), - func=self.col.tags.bulkRem, - ) - def clearUnusedTags(self) -> None: self.editor.saveNow(self._clearUnusedTags) @@ -1254,19 +1248,22 @@ where id in %s""" self.mw.taskman.run_in_background(self.col.tags.registerNotes, on_done) + addTags = add_tags_to_selected_notes + deleteTags = remove_tags_from_selected_notes + # Suspending ###################################################################### - def isSuspended(self) -> bool: + def current_card_is_suspended(self) -> bool: return bool(self.card and self.card.queue == QUEUE_TYPE_SUSPENDED) - def onSuspend(self) -> None: - self.editor.saveNow(self._onSuspend) + def suspend_selected_cards(self) -> None: + self.editor.saveNow(self._suspend_selected_cards) - def _onSuspend(self) -> None: - sus = not self.isSuspended() + def _suspend_selected_cards(self) -> None: + want_suspend = not self.current_card_is_suspended() c = self.selectedCards() - if sus: + if want_suspend: self.col.sched.suspend_cards(c) else: self.col.sched.unsuspend_cards(c) @@ -1291,13 +1288,13 @@ where id in %s""" def _on_set_flag(self, n: int) -> None: # flag needs toggling off? - if n == self.card.userFlag(): + if n == self.card.user_flag(): n = 0 - self.col.setUserFlag(n, self.selectedCards()) + self.col.set_user_flag_for_cards(n, self.selectedCards()) self.model.reset() def _updateFlagsMenu(self) -> None: - flag = self.card and self.card.userFlag() + flag = self.card and self.card.user_flag() flag = flag or 0 f = self.form @@ -1317,12 +1314,12 @@ where id in %s""" if mark is None: mark = not self.isMarked() if mark: - self.addTags(tags="marked") + self.add_tags_to_selected_notes(tags="marked") else: - self.deleteTags(tags="marked") + self.remove_tags_from_selected_notes(tags="marked") def isMarked(self) -> bool: - return bool(self.card and self.card.note().hasTag("Marked")) + return bool(self.card and self.card.note().has_tag("Marked")) # Repositioning ###################################################################### @@ -1558,7 +1555,7 @@ where id in %s""" def _onFindDupes(self) -> None: d = QDialog(self) - self.mw.setupDialogGC(d) + self.mw.garbage_collect_on_dialog_finish(d) frm = aqt.forms.finddupes.Ui_Dialog() frm.setupUi(d) restoreGeom(d, "findDupes") @@ -1647,7 +1644,7 @@ where id in %s""" nids = set() for _, nidlist in res: nids.update(nidlist) - self.col.tags.bulkAdd(list(nids), tr(TR.BROWSING_DUPLICATE)) + self.col.tags.bulk_add(list(nids), tr(TR.BROWSING_DUPLICATE)) self.mw.progress.finish() self.model.endReset() self.mw.requireReset(reason=ResetReason.BrowserTagDupes, context=self) diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 590330ec7..01474cd21 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -47,7 +47,7 @@ class CardLayout(QDialog): fill_empty: bool = False, ) -> None: QDialog.__init__(self, parent or mw, Qt.Window) - mw.setupDialogGC(self) + mw.garbage_collect_on_dialog_finish(self) self.mw = aqt.mw self.note = note self.ord = ord @@ -700,8 +700,8 @@ class CardLayout(QDialog): f.afmt.setText(t.get("bafmt", "")) if t.get("bfont"): f.overrideFont.setChecked(True) - f.font.setCurrentFont(QFont(t.get("bfont", "Arial"))) - f.fontSize.setValue(t.get("bsize", 12)) + f.font.setCurrentFont(QFont(t.get("bfont") or "Arial")) + f.fontSize.setValue(t.get("bsize") or 12) qconnect(f.buttonBox.accepted, lambda: self.onBrowserDisplayOk(f)) d.exec_() diff --git a/qt/aqt/data/web/css/BUILD.bazel b/qt/aqt/data/web/css/BUILD.bazel index ff0045e68..f27b9d463 100644 --- a/qt/aqt/data/web/css/BUILD.bazel +++ b/qt/aqt/data/web/css/BUILD.bazel @@ -32,6 +32,7 @@ filegroup( "core.css", "css_local", "editor", + "//qt/aqt/data/web/css/vendor", ], visibility = ["//qt:__subpackages__"], ) diff --git a/qt/aqt/data/web/css/vendor/BUILD.bazel b/qt/aqt/data/web/css/vendor/BUILD.bazel new file mode 100644 index 000000000..23b604f63 --- /dev/null +++ b/qt/aqt/data/web/css/vendor/BUILD.bazel @@ -0,0 +1,20 @@ +load("//ts:vendor.bzl", "copy_bootstrap_css", "copy_bootstrap_icons") + +copy_bootstrap_css(name = "bootstrap") + +copy_bootstrap_icons(name = "bootstrap-icons") + +files = [ + "bootstrap", + "bootstrap-icons", +] + +directories = [] + +filegroup( + name = "vendor", + srcs = glob(["*.css"]) + + ["//qt/aqt/data/web/css/vendor:{}".format(file) for file in files] + + ["//qt/aqt/data/web/css/vendor/{}".format(dir) for dir in directories], + visibility = ["//qt:__subpackages__"], +) diff --git a/qt/aqt/data/web/js/vendor/BUILD.bazel b/qt/aqt/data/web/js/vendor/BUILD.bazel index f12e3be27..fbdae540a 100644 --- a/qt/aqt/data/web/js/vendor/BUILD.bazel +++ b/qt/aqt/data/web/js/vendor/BUILD.bazel @@ -1,4 +1,11 @@ -load("//ts:vendor.bzl", "copy_css_browser_selector", "copy_jquery", "copy_jquery_ui", "copy_protobufjs") +load( + "//ts:vendor.bzl", + "copy_css_browser_selector", + "copy_jquery", + "copy_jquery_ui", + "copy_protobufjs", + "copy_bootstrap_js", +) copy_jquery(name = "jquery") @@ -8,11 +15,14 @@ copy_protobufjs(name = "protobufjs") copy_css_browser_selector(name = "css-browser-selector") +copy_bootstrap_js(name = "bootstrap") + files = [ "jquery", "jquery-ui", "protobufjs", "css-browser-selector", + "bootstrap", ] directories = [ diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 1afc1c298..bbaeae8b0 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -9,7 +9,7 @@ from typing import Any, List import aqt from anki.decks import DeckTreeNode -from anki.errors import DeckRenameError +from anki.errors import DeckIsFilteredError from anki.utils import intTime from aqt import AnkiQt, gui_hooks from aqt.qt import * @@ -88,11 +88,7 @@ class DeckBrowser: elif cmd == "import": self.mw.onImport() elif cmd == "create": - deck = getOnlyText(tr(TR.DECKS_NAME_FOR_DECK)) - if deck: - self.mw.col.decks.id(deck) - gui_hooks.sidebar_should_refresh_decks() - self.refresh() + self._on_create() elif cmd == "drag": source, target = arg.split(",") self._handle_drag_and_drop(int(source), int(target or 0)) @@ -272,8 +268,8 @@ class DeckBrowser: try: self.mw.col.decks.rename(deck, newName) gui_hooks.sidebar_should_refresh_decks() - except DeckRenameError as e: - showWarning(e.description) + except DeckIsFilteredError as err: + showWarning(str(err)) return self.show() @@ -291,7 +287,11 @@ class DeckBrowser: self._renderPage(reuse=True) def _handle_drag_and_drop(self, source: int, target: int) -> None: - self.mw.col.decks.drag_drop_decks([source], target) + try: + self.mw.col.decks.drag_drop_decks([source], target) + except Exception as e: + showWarning(str(e)) + return gui_hooks.sidebar_should_refresh_decks() self.show() @@ -356,6 +356,17 @@ class DeckBrowser: def _onShared(self) -> None: openLink(f"{aqt.appShared}decks/") + def _on_create(self) -> None: + deck = getOnlyText(tr(TR.DECKS_NAME_FOR_DECK)) + if deck: + try: + self.mw.col.decks.id(deck) + except DeckIsFilteredError as err: + showWarning(str(err)) + return + gui_hooks.sidebar_should_refresh_decks() + self.refresh() + ###################################################################### def _v1_upgrade_message(self) -> str: diff --git a/qt/aqt/deckchooser.py b/qt/aqt/deckchooser.py index d7f806c4a..70056aaa0 100644 --- a/qt/aqt/deckchooser.py +++ b/qt/aqt/deckchooser.py @@ -1,61 +1,79 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import Any -from aqt import AnkiQt, gui_hooks +from typing import Optional + +from aqt import AnkiQt from aqt.qt import * from aqt.utils import TR, HelpPage, shortcut, tr class DeckChooser(QHBoxLayout): def __init__( - self, mw: AnkiQt, widget: QWidget, label: bool = True, start: Any = None + self, + mw: AnkiQt, + widget: QWidget, + label: bool = True, + starting_deck_id: Optional[int] = None, ) -> None: QHBoxLayout.__init__(self) self._widget = widget # type: ignore self.mw = mw - self.label = label + self._setup_ui(show_label=label) + + self._selected_deck_id = 0 + # default to current deck if starting id not provided + if starting_deck_id is None: + starting_deck_id = self.mw.col.get_config("curDeck", default=1) or 1 + self.selected_deck_id = starting_deck_id + + def _setup_ui(self, show_label: bool) -> None: self.setContentsMargins(0, 0, 0, 0) self.setSpacing(8) - self.setupDecks() - self._widget.setLayout(self) - gui_hooks.current_note_type_did_change.append(self.onModelChangeNew) - def setupDecks(self) -> None: - if self.label: + # text label before button? + if show_label: self.deckLabel = QLabel(tr(TR.DECKS_DECK)) self.addWidget(self.deckLabel) + # decks box - self.deck = QPushButton(clicked=self.onDeckChange) # type: ignore + self.deck = QPushButton() + qconnect(self.deck.clicked, self.choose_deck) self.deck.setAutoDefault(False) self.deck.setToolTip(shortcut(tr(TR.QT_MISC_TARGET_DECK_CTRLANDD))) - QShortcut(QKeySequence("Ctrl+D"), self._widget, activated=self.onDeckChange) # type: ignore - self.addWidget(self.deck) - # starting label - if self.mw.col.conf.get("addToCur", True): - col = self.mw.col - did = col.conf["curDeck"] - if col.decks.isDyn(did): - # if they're reviewing, try default to current card - c = self.mw.reviewer.card - if self.mw.state == "review" and c: - if not c.odid: - did = c.did - else: - did = c.odid - else: - did = 1 - self.setDeckName( - self.mw.col.decks.nameOrNone(did) or tr(TR.QT_MISC_DEFAULT) - ) - else: - self.setDeckName( - self.mw.col.decks.nameOrNone(self.mw.col.models.current()["did"]) - or tr(TR.QT_MISC_DEFAULT) - ) - # layout + qconnect( + QShortcut(QKeySequence("Ctrl+D"), self._widget).activated, self.choose_deck + ) sizePolicy = QSizePolicy(QSizePolicy.Policy(7), QSizePolicy.Policy(0)) self.deck.setSizePolicy(sizePolicy) + self.addWidget(self.deck) + + self._widget.setLayout(self) + + def selected_deck_name(self) -> str: + return ( + self.mw.col.decks.name_if_exists(self.selected_deck_id) or "missing default" + ) + + @property + def selected_deck_id(self) -> int: + self._ensure_selected_deck_valid() + + return self._selected_deck_id + + @selected_deck_id.setter + def selected_deck_id(self, id: int) -> None: + if id != self._selected_deck_id: + self._selected_deck_id = id + self._ensure_selected_deck_valid() + self._update_button_label() + + def _ensure_selected_deck_valid(self) -> None: + if not self.mw.col.decks.get(self._selected_deck_id, default=False): + self.selected_deck_id = 1 + + def _update_button_label(self) -> None: + self.deck.setText(self.selected_deck_name().replace("&", "&&")) def show(self) -> None: self._widget.show() # type: ignore @@ -63,23 +81,10 @@ class DeckChooser(QHBoxLayout): def hide(self) -> None: self._widget.hide() # type: ignore - def cleanup(self) -> None: - gui_hooks.current_note_type_did_change.remove(self.onModelChangeNew) - - def onModelChangeNew(self, unused: Any = None) -> None: - self.onModelChange() - - def onModelChange(self) -> None: - if not self.mw.col.conf.get("addToCur", True): - self.setDeckName( - self.mw.col.decks.nameOrNone(self.mw.col.models.current()["did"]) - or tr(TR.QT_MISC_DEFAULT) - ) - - def onDeckChange(self) -> None: + def choose_deck(self) -> None: from aqt.studydeck import StudyDeck - current = self.deckName() + current = self.selected_deck_name() ret = StudyDeck( self.mw, current=current, @@ -91,20 +96,15 @@ class DeckChooser(QHBoxLayout): geomKey="selectDeck", ) if ret.name: - self.setDeckName(ret.name) + self.selected_deck_id = self.mw.col.decks.byName(ret.name)["id"] - def setDeckName(self, name: str) -> None: - self.deck.setText(name.replace("&", "&&")) - self._deckName = name + # legacy - def deckName(self) -> str: - return self._deckName + onDeckChange = choose_deck + deckName = selected_deck_name def selectedId(self) -> int: - # save deck name - name = self.deckName() - if not name.strip(): - did = 1 - else: - did = self.mw.col.decks.id(name) - return did + return self.selected_deck_id + + def cleanup(self) -> None: + pass diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index 8c88df964..79a743e04 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.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 Callable, List, Optional +from typing import Callable, List, Optional, Tuple import aqt from anki.collection import SearchNode -from anki.decks import Deck, DeckRenameError -from anki.errors import InvalidInput +from anki.decks import Deck +from anki.errors import DeckIsFilteredError, InvalidInput from anki.lang import without_unicode_isolation from aqt import AnkiQt, colors, gui_hooks from aqt.qt import * @@ -40,12 +40,13 @@ class DeckConf(QDialog): QDialog.__init__(self, mw) self.mw = mw + self.col = self.mw.col self.did: Optional[int] = None self.form = aqt.forms.dyndconf.Ui_Dialog() self.form.setupUi(self) self.mw.checkpoint(tr(TR.ACTIONS_OPTIONS)) self.initialSetup() - self.old_deck = self.mw.col.decks.current() + self.old_deck = self.col.decks.current() if deck and deck["dyn"]: # modify existing dyn deck @@ -69,10 +70,14 @@ class DeckConf(QDialog): self.set_custom_searches(search, search_2) qconnect(self.form.search_button.clicked, self.on_search_button) qconnect(self.form.search_button_2.clicked, self.on_search_button_2) - color = theme_manager.color(colors.LINK) + qconnect(self.form.hint_button.clicked, self.on_hint_button) + blue = theme_manager.color(colors.LINK) + grey = theme_manager.color(colors.DISABLED) self.setStyleSheet( - f"""QPushButton[flat=true] {{ text-align: left; color: {color}; padding: 0; border: 0 }} - QPushButton[flat=true]:hover {{ text-decoration: underline }}""" + f"""QPushButton[label] {{ padding: 0; border: 0 }} + QPushButton[label]:hover {{ text-decoration: underline }} + QPushButton[label="search"] {{ color: {blue} }} + QPushButton[label="hint"] {{ color: {grey} }}""" ) disable_help_button(self) self.setWindowModality(Qt.WindowModal) @@ -83,7 +88,7 @@ class DeckConf(QDialog): without_unicode_isolation(tr(TR.ACTIONS_OPTIONS_FOR, val=self.deck["name"])) ) self.form.buttonBox.button(QDialogButtonBox.Ok).setText(label) - if self.mw.col.schedVer() == 1: + if self.col.schedVer() == 1: self.form.secondFilter.setVisible(False) restoreGeom(self, "dyndeckconf") @@ -100,23 +105,23 @@ class DeckConf(QDialog): def new_dyn_deck(self) -> None: suffix: int = 1 - while self.mw.col.decks.id_for_name( + while self.col.decks.id_for_name( without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=suffix)) ): suffix += 1 name: str = without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=suffix)) - self.did = self.mw.col.decks.new_filtered(name) - self.deck = self.mw.col.decks.current() + self.did = self.col.decks.new_filtered(name) + self.deck = self.col.decks.current() def set_default_searches(self, deck_name: str) -> None: self.form.search.setText( - self.mw.col.build_search_string( + self.col.build_search_string( SearchNode(deck=deck_name), SearchNode(card_state=SearchNode.CARD_STATE_DUE), ) ) self.form.search_2.setText( - self.mw.col.build_search_string( + self.col.build_search_string( SearchNode(deck=deck_name), SearchNode(card_state=SearchNode.CARD_STATE_NEW), ) @@ -152,7 +157,7 @@ class DeckConf(QDialog): def _on_search_button(self, line: QLineEdit) -> None: try: - search = self.mw.col.build_search_string(line.text()) + search = self.col.build_search_string(line.text()) except InvalidInput as err: line.setFocus() line.selectAll() @@ -160,9 +165,61 @@ class DeckConf(QDialog): else: aqt.dialogs.open("Browser", self.mw, search=(search,)) + def on_hint_button(self) -> None: + """Open the browser to show cards that match the typed-in filters but cannot be included + due to internal limitations. + """ + manual_filters = (self.form.search.text(), *self._second_filter()) + implicit_filters = ( + SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED), + SearchNode(card_state=SearchNode.CARD_STATE_BURIED), + *self._learning_search_node(), + *self._filtered_search_node(), + ) + manual_filter = self.col.group_searches(*manual_filters, joiner="OR") + implicit_filter = self.col.group_searches(*implicit_filters, joiner="OR") + try: + search = self.col.build_search_string(manual_filter, implicit_filter) + except InvalidInput as err: + show_invalid_search_error(err) + else: + aqt.dialogs.open("Browser", self.mw, search=(search,)) + + def _second_filter(self) -> Tuple[str, ...]: + if self.form.secondFilter.isChecked(): + return (self.form.search_2.text(),) + return () + + def _learning_search_node(self) -> Tuple[SearchNode, ...]: + """Return a search node that matches learning cards if the old scheduler is enabled. + If it's a rebuild, exclude cards from this filtered deck as those will be reset. + """ + if self.col.schedVer() == 1: + if self.did is None: + return ( + self.col.group_searches( + SearchNode(card_state=SearchNode.CARD_STATE_LEARN), + SearchNode(negated=SearchNode(deck=self.deck["name"])), + ), + ) + return (SearchNode(card_state=SearchNode.CARD_STATE_LEARN),) + return () + + def _filtered_search_node(self) -> Tuple[SearchNode]: + """Return a search node that matches cards in filtered decks, if applicable excluding those + in the deck being rebuild.""" + if self.did is None: + return ( + self.col.group_searches( + SearchNode(deck="filtered"), + SearchNode(negated=SearchNode(deck=self.deck["name"])), + ), + ) + return (SearchNode(deck="filtered"),) + def _onReschedToggled(self, _state: int) -> None: self.form.previewDelayWidget.setVisible( - not self.form.resched.isChecked() and self.mw.col.schedVer() > 1 + not self.form.resched.isChecked() and self.col.schedVer() > 1 ) def loadConf(self, deck: Optional[Deck] = None) -> None: @@ -175,7 +232,7 @@ class DeckConf(QDialog): search, limit, order = d["terms"][0] f.search.setText(search) - if self.mw.col.schedVer() == 1: + if self.col.schedVer() == 1: if d["delays"]: f.steps.setText(self.listToUser(d["delays"])) f.stepsOn.setChecked(True) @@ -205,35 +262,36 @@ class DeckConf(QDialog): d = self.deck if f.name.text() and d["name"] != f.name.text(): - self.mw.col.decks.rename(d, f.name.text()) + self.col.decks.rename(d, f.name.text()) gui_hooks.sidebar_should_refresh_decks() d["resched"] = f.resched.isChecked() d["delays"] = None - if self.mw.col.schedVer() == 1 and f.stepsOn.isChecked(): + if self.col.schedVer() == 1 and f.stepsOn.isChecked(): steps = self.userToList(f.steps) if steps: d["delays"] = steps else: d["delays"] = None - search = self.mw.col.build_search_string(f.search.text()) + search = self.col.build_search_string(f.search.text()) terms = [[search, f.limit.value(), f.order.currentIndex()]] if f.secondFilter.isChecked(): - search_2 = self.mw.col.build_search_string(f.search_2.text()) + search_2 = self.col.build_search_string(f.search_2.text()) terms.append([search_2, f.limit_2.value(), f.order_2.currentIndex()]) d["terms"] = terms d["previewDelay"] = f.previewDelay.value() - self.mw.col.decks.save(d) + self.col.decks.save(d) def reject(self) -> None: if self.did: - self.mw.col.decks.rem(self.did) - self.mw.col.decks.select(self.old_deck["id"]) + self.col.decks.rem(self.did) + self.col.decks.select(self.old_deck["id"]) + self.mw.reset() saveGeom(self, "dyndeckconf") QDialog.reject(self) aqt.dialogs.markClosed("DynDeckConfDialog") @@ -243,10 +301,10 @@ class DeckConf(QDialog): self.saveConf() except InvalidInput as err: show_invalid_search_error(err) - except DeckRenameError as err: - showWarning(err.description) + except DeckIsFilteredError as err: + showWarning(str(err)) else: - if not self.mw.col.sched.rebuild_filtered_deck(self.deck["id"]): + if not self.col.sched.rebuild_filtered_deck(self.deck["id"]): if askUser(tr(TR.DECKS_THE_PROVIDED_SEARCH_DID_NOT_MATCH)): return saveGeom(self, "dyndeckconf") diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index 2c3832663..4a540d76b 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -10,7 +10,7 @@ from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tooltip, t class EditCurrent(QDialog): def __init__(self, mw: aqt.AnkiQt) -> None: QDialog.__init__(self, None, Qt.Window) - mw.setupDialogGC(self) + mw.garbage_collect_on_dialog_finish(self) self.mw = mw self.form = aqt.forms.editcurrent.Ui_Dialog() self.form.setupUi(self) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index b0a29856a..3346dfbc6 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -179,19 +179,16 @@ class Editor: "colour", tr(TR.EDITING_SET_FOREGROUND_COLOUR_F7), """ -
""", + +""", ), self._addButton( None, "changeCol", tr(TR.EDITING_CHANGE_COLOUR_F8), """ -
""", + +""", ), self._addButton( "text_cloze", "cloze", tr(TR.EDITING_CLOZE_DELETION_CTRLANDSHIFTANDC) @@ -222,8 +219,16 @@ class Editor: # then load page self.web.stdHtml( _html % (bgcol, topbuts, tr(TR.EDITING_SHOW_DUPLICATES)), - css=["css/editor.css"], - js=["js/vendor/jquery.min.js", "js/editor.js"], + css=[ + "css/vendor/bootstrap.min.css", + "css/vendor/bootstrap-icons.css", + "css/editor.css", + ], + js=[ + "js/vendor/jquery.min.js", + "js/vendor/bootstrap.bundle.min.js", + "js/editor.js", + ], context=self, ) self.web.eval("preventButtonFocus();") @@ -310,11 +315,11 @@ class Editor: iconstr = self.resourceToData(icon) else: iconstr = f"/_anki/imgs/{icon}.png" - imgelm = f"""""" + imgelm = f"""""" else: imgelm = "" if label or not imgelm: - labelelm = f"""{label or cmd}""" + labelelm = label or cmd else: labelelm = "" if id: @@ -329,7 +334,7 @@ class Editor: if rightside: class_ = "linkb" else: - class_ = "" + class_ = "rounded" if not disables: class_ += " perm" return """""" % ( if elap > 86_400: check_and_prompt_for_updates( - self, self.addonManager, self.on_updates_installed + self, + self.addonManager, + self.on_updates_installed, + requested_by_user=False, ) self.pm.set_last_addon_update_check(intTime()) @@ -908,7 +912,7 @@ title="%s" %s>%s""" % ( self.media_syncer.start() def on_collection_sync_finished() -> None: - self.col.clearUndo() + self.col.clear_python_undo() self.col.models._clear_cache() gui_hooks.sync_did_finish() self.reset() @@ -1021,41 +1025,81 @@ title="%s" %s>%s""" % ( ########################################################################## def onUndo(self) -> None: - n = self.col.undoName() - if not n: - return - cid = self.col.undo() - if cid and self.state == "review": - card = self.col.getCard(cid) - self.col.sched.reset() - self.reviewer.cardQueue.append(card) - self.reviewer.nextCard() - gui_hooks.review_did_undo(cid) - else: - self.reset() - tooltip(tr(TR.QT_MISC_REVERTED_TO_STATE_PRIOR_TO, val=n.lower())) - gui_hooks.state_did_revert(n) - self.maybeEnableUndo() + reviewing = self.state == "review" + result = self.col.undo() + just_refresh_reviewer = False - def maybeEnableUndo(self) -> None: - if self.col and self.col.undoName(): - self.form.actionUndo.setText(tr(TR.QT_MISC_UNDO2, val=self.col.undoName())) + if result is None: + # should not happen + showInfo("nothing to undo") + self.update_undo_actions() + return + + elif isinstance(result, ReviewUndo): + name = tr(TR.SCHEDULING_REVIEW) + + if reviewing: + # push the undone card to the top of the queue + cid = result.card.id + card = self.col.getCard(cid) + self.reviewer.cardQueue.append(card) + + gui_hooks.review_did_undo(cid) + + just_refresh_reviewer = True + + elif isinstance(result, BackendUndo): + name = result.name + + if reviewing and self.col.sched.is_2021: + # new scheduler will have taken care of updating queue + just_refresh_reviewer = True + + elif isinstance(result, Checkpoint): + name = result.name + + else: + assert_exhaustive(result) + assert False + + if just_refresh_reviewer: + self.reviewer.nextCard() + else: + # full queue+gui reset required + self.reset() + + tooltip(tr(TR.UNDO_ACTION_UNDONE, action=name)) + gui_hooks.state_did_revert(name) + self.update_undo_actions() + + def update_undo_actions(self) -> None: + """Update menu text and enable/disable menu item as appropriate. + Plural as this may handle redo in the future too.""" + if self.col: + status = self.col.undo_status() + undo_action = status.undo or None + else: + undo_action = None + + if undo_action: + undo_action = tr(TR.UNDO_UNDO_ACTION, val=undo_action) + self.form.actionUndo.setText(undo_action) self.form.actionUndo.setEnabled(True) gui_hooks.undo_state_did_change(True) else: - self.form.actionUndo.setText(tr(TR.QT_MISC_UNDO)) + self.form.actionUndo.setText(tr(TR.UNDO_UNDO)) self.form.actionUndo.setEnabled(False) gui_hooks.undo_state_did_change(False) def checkpoint(self, name: str) -> None: self.col.save(name) - self.maybeEnableUndo() + self.update_undo_actions() def autosave(self) -> None: - saved = self.col.autosave() - self.maybeEnableUndo() - if saved: - self.doGC() + self.col.autosave() + self.update_undo_actions() + + maybeEnableUndo = update_undo_actions # Other menu operations ########################################################################## @@ -1218,6 +1262,8 @@ title="%s" %s>%s""" % ( self.progress.timer(10 * 60 * 1000, self.onRefreshTimer, True) # check media sync every 5 minutes self.progress.timer(5 * 60 * 1000, self.on_autosync_timer, True) + # periodic garbage collection + self.progress.timer(15 * 60 * 1000, self.garbage_collect_now, False) # ensure Python interpreter runs at least once per second, so that # SIGINT/SIGTERM is processed without a long delay self.progress.timer(1000, lambda: None, True, False) @@ -1583,22 +1629,39 @@ title="%s" %s>%s""" % ( # GC ########################################################################## - # ensure gc runs in main thread + # The default Python garbage collection can trigger on any thread. This can + # cause crashes if Qt objects are garbage-collected, as Qt expects access + # only on the main thread. So Anki disables the default GC on startup, and + # instead runs it on a timer, and after dialog close. + # The gc after dialog close is necessary to free up the memory and extra + # processes that webviews spawn, as a lot of the GUI code creates ref cycles. - def setupDialogGC(self, obj: Any) -> None: - qconnect(obj.finished, lambda: self.gcWindow(obj)) + def garbage_collect_on_dialog_finish(self, dialog: QDialog) -> None: + qconnect( + dialog.finished, lambda: self.deferred_delete_and_garbage_collect(dialog) + ) - def gcWindow(self, obj: Any) -> None: + def deferred_delete_and_garbage_collect(self, obj: QObject) -> None: obj.deleteLater() - self.progress.timer(1000, self.doGC, False, requiresCollection=False) + self.progress.timer( + 1000, self.garbage_collect_now, False, requiresCollection=False + ) - def disableGC(self) -> None: + def disable_automatic_garbage_collection(self) -> None: gc.collect() gc.disable() - def doGC(self) -> None: + def garbage_collect_now(self) -> None: + # gc.collect() has optional arguments that will cause problems if + # it's passed directly to a QTimer, and pylint complains if we + # wrap it in a lambda, so we use this trivial wrapper gc.collect() + # legacy aliases + + setupDialogGC = garbage_collect_on_dialog_finish + gcWindow = deferred_delete_and_garbage_collect + # Crash log ########################################################################## diff --git a/qt/aqt/modelchooser.py b/qt/aqt/modelchooser.py index cf86deb0e..0ffe9b34f 100644 --- a/qt/aqt/modelchooser.py +++ b/qt/aqt/modelchooser.py @@ -8,6 +8,8 @@ from aqt.utils import TR, HelpPage, shortcut, tr class ModelChooser(QHBoxLayout): + "New code should prefer NoteTypeChooser." + def __init__( self, mw: AnkiQt, diff --git a/qt/aqt/notetypechooser.py b/qt/aqt/notetypechooser.py new file mode 100644 index 000000000..dfddd5bd1 --- /dev/null +++ b/qt/aqt/notetypechooser.py @@ -0,0 +1,145 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from typing import List, Optional + +from aqt import AnkiQt, gui_hooks +from aqt.qt import * +from aqt.utils import TR, HelpPage, shortcut, tr + + +class NoteTypeChooser(QHBoxLayout): + """ + Unlike the older modelchooser, this does not modify the "current model", + so changes made here do not affect other parts of the UI. To read the + currently selected notetype id, use .selected_notetype_id. + + By default, a chooser will pop up when the button is pressed. You can + override this by providing `on_button_activated`. Call .choose_notetype() + to run the normal behaviour. + + `on_notetype_changed` will be called with the new notetype ID if the user + selects a different notetype, or if the currently-selected notetype is + deleted. + """ + + def __init__( + self, + *, + mw: AnkiQt, + widget: QWidget, + starting_notetype_id: int, + on_button_activated: Optional[Callable[[], None]] = None, + on_notetype_changed: Optional[Callable[[int], None]] = None, + show_prefix_label: bool = True, + ) -> None: + QHBoxLayout.__init__(self) + self._widget = widget # type: ignore + self.mw = mw + if on_button_activated: + self.on_button_activated = on_button_activated + else: + self.on_button_activated = self.choose_notetype + self._setup_ui(show_label=show_prefix_label) + gui_hooks.state_did_reset.append(self.reset_state) + self._selected_notetype_id = 0 + # triggers UI update; avoid firing changed hook on startup + self.on_notetype_changed = None + self.selected_notetype_id = starting_notetype_id + self.on_notetype_changed = on_notetype_changed + + def _setup_ui(self, show_label: bool) -> None: + self.setContentsMargins(0, 0, 0, 0) + self.setSpacing(8) + + if show_label: + self.label = QLabel(tr(TR.NOTETYPES_TYPE)) + self.addWidget(self.label) + + # button + self.button = QPushButton() + self.button.setToolTip(shortcut(tr(TR.QT_MISC_CHANGE_NOTE_TYPE_CTRLANDN))) + qconnect( + QShortcut(QKeySequence("Ctrl+N"), self._widget).activated, + self.on_button_activated, + ) + self.button.setAutoDefault(False) + self.addWidget(self.button) + qconnect(self.button.clicked, self.on_button_activated) + sizePolicy = QSizePolicy(QSizePolicy.Policy(7), QSizePolicy.Policy(0)) + self.button.setSizePolicy(sizePolicy) + self._widget.setLayout(self) + + def cleanup(self) -> None: + gui_hooks.state_did_reset.remove(self.reset_state) + + def reset_state(self) -> None: + self._ensure_selected_notetype_valid() + + def show(self) -> None: + self._widget.show() # type: ignore + + def hide(self) -> None: + self._widget.hide() # type: ignore + + def onEdit(self) -> None: + import aqt.models + + aqt.models.Models(self.mw, self._widget) + + def choose_notetype(self) -> None: + from aqt.studydeck import StudyDeck + + current = self.selected_notetype_name() + + # edit button + edit = QPushButton(tr(TR.QT_MISC_MANAGE)) + qconnect(edit.clicked, self.onEdit) + + def nameFunc() -> List[str]: + return sorted(self.mw.col.models.allNames()) + + ret = StudyDeck( + self.mw, + names=nameFunc, + accept=tr(TR.ACTIONS_CHOOSE), + title=tr(TR.QT_MISC_CHOOSE_NOTE_TYPE), + help=HelpPage.NOTE_TYPE, + current=current, + parent=self._widget, + buttons=[edit], + cancel=True, + geomKey="selectModel", + ) + if not ret.name: + return + + notetype = self.mw.col.models.byName(ret.name) + if (id := notetype["id"]) != self._selected_notetype_id: + self.selected_notetype_id = id + + @property + def selected_notetype_id(self) -> int: + # theoretically this should not be necessary, as we're listening to + # resets + self._ensure_selected_notetype_valid() + + return self._selected_notetype_id + + @selected_notetype_id.setter + def selected_notetype_id(self, id: int) -> None: + if id != self._selected_notetype_id: + self._selected_notetype_id = id + self._ensure_selected_notetype_valid() + self._update_button_label() + if func := self.on_notetype_changed: + func(self._selected_notetype_id) + + def selected_notetype_name(self) -> str: + return self.mw.col.models.get(self.selected_notetype_id)["name"] + + def _ensure_selected_notetype_valid(self) -> None: + if not self.mw.col.models.get(self._selected_notetype_id): + self.selected_notetype_id = self.mw.col.models.all_names_and_ids()[0].id + + def _update_button_label(self) -> None: + self.button.setText(self.selected_notetype_name().replace("&", "&&")) diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 00dd9eaf8..aacd48a1d 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -165,8 +165,6 @@ class Preferences(QDialog): self.mw.col.set_preferences(self.prefs) - d.setMod() - # Network ###################################################################### @@ -209,7 +207,6 @@ class Preferences(QDialog): ) if self.form.fullSync.isChecked(): self.mw.col.modSchema(check=False) - self.mw.col.setMod() # Backup ###################################################################### diff --git a/qt/aqt/previewer.py b/qt/aqt/previewer.py index 0e6a1dc98..26a55ff77 100644 --- a/qt/aqt/previewer.py +++ b/qt/aqt/previewer.py @@ -222,7 +222,6 @@ class Previewer(QDialog): def _on_show_both_sides(self, toggle: bool) -> None: self._show_both_sides = toggle self.mw.col.set_config_bool(Config.Bool.PREVIEW_BOTH_SIDES, toggle) - self.mw.col.setMod() if self._state == "answer" and not toggle: self._state = "question" self.render_card() diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 3702d2db5..4d3ba62dc 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -204,8 +204,8 @@ class Reviewer: bodyclass = theme_manager.body_classes_for_card_ord(c.ord) self.web.eval(f"_showQuestion({json.dumps(q)},'{bodyclass}');") - self._drawFlag() - self._drawMark() + self._update_flag_icon() + self._update_mark_icon() self._showAnswerButton() self.mw.web.setFocus() # user hook @@ -215,11 +215,14 @@ class Reviewer: print("use card.autoplay() instead of reviewer.autoplay(card)") return card.autoplay() - def _drawFlag(self) -> None: - self.web.eval(f"_drawFlag({self.card.userFlag()});") + def _update_flag_icon(self) -> None: + self.web.eval(f"_drawFlag({self.card.user_flag()});") - def _drawMark(self) -> None: - self.web.eval(f"_drawMark({json.dumps(self.card.note().hasTag('marked'))});") + def _update_mark_icon(self) -> None: + self.web.eval(f"_drawMark({json.dumps(self.card.note().has_tag('marked'))});") + + _drawMark = _update_mark_icon + _drawFlag = _update_flag_icon # Showing the answer ########################################################################## @@ -287,16 +290,16 @@ class Reviewer: ("m", self.showContextMenu), ("r", self.replayAudio), (Qt.Key_F5, self.replayAudio), - ("Ctrl+1", lambda: self.setFlag(1)), - ("Ctrl+2", lambda: self.setFlag(2)), - ("Ctrl+3", lambda: self.setFlag(3)), - ("Ctrl+4", lambda: self.setFlag(4)), - ("*", self.onMark), - ("=", self.onBuryNote), - ("-", self.onBuryCard), - ("!", self.onSuspend), - ("@", self.onSuspendCard), - ("Ctrl+Delete", self.onDelete), + ("Ctrl+1", lambda: self.set_flag_on_current_card(1)), + ("Ctrl+2", lambda: self.set_flag_on_current_card(2)), + ("Ctrl+3", lambda: self.set_flag_on_current_card(3)), + ("Ctrl+4", lambda: self.set_flag_on_current_card(4)), + ("*", self.toggle_mark_on_current_note), + ("=", self.bury_current_note), + ("-", self.bury_current_card), + ("!", self.suspend_current_note), + ("@", self.suspend_current_card), + ("Ctrl+Delete", self.delete_current_note), ("Ctrl+Shift+D", self.on_set_due), ("v", self.onReplayRecorded), ("Shift+v", self.onRecordVoice), @@ -695,7 +698,7 @@ time = %(time)d; # note the shortcuts listed here also need to be defined above def _contextMenu(self) -> List[Any]: - currentFlag = self.card and self.card.userFlag() + currentFlag = self.card and self.card.user_flag() opts = [ [ tr(TR.STUDYING_FLAG_CARD), @@ -703,36 +706,36 @@ time = %(time)d; [ tr(TR.ACTIONS_RED_FLAG), "Ctrl+1", - lambda: self.setFlag(1), + lambda: self.set_flag_on_current_card(1), dict(checked=currentFlag == 1), ], [ tr(TR.ACTIONS_ORANGE_FLAG), "Ctrl+2", - lambda: self.setFlag(2), + lambda: self.set_flag_on_current_card(2), dict(checked=currentFlag == 2), ], [ tr(TR.ACTIONS_GREEN_FLAG), "Ctrl+3", - lambda: self.setFlag(3), + lambda: self.set_flag_on_current_card(3), dict(checked=currentFlag == 3), ], [ tr(TR.ACTIONS_BLUE_FLAG), "Ctrl+4", - lambda: self.setFlag(4), + lambda: self.set_flag_on_current_card(4), dict(checked=currentFlag == 4), ], ], ], - [tr(TR.STUDYING_MARK_NOTE), "*", self.onMark], - [tr(TR.STUDYING_BURY_CARD), "-", self.onBuryCard], - [tr(TR.STUDYING_BURY_NOTE), "=", self.onBuryNote], + [tr(TR.STUDYING_MARK_NOTE), "*", self.toggle_mark_on_current_note], + [tr(TR.STUDYING_BURY_CARD), "-", self.bury_current_card], + [tr(TR.STUDYING_BURY_NOTE), "=", self.bury_current_note], [tr(TR.ACTIONS_SET_DUE_DATE), "Ctrl+Shift+D", self.on_set_due], - [tr(TR.ACTIONS_SUSPEND_CARD), "@", self.onSuspendCard], - [tr(TR.STUDYING_SUSPEND_NOTE), "!", self.onSuspend], - [tr(TR.STUDYING_DELETE_NOTE), "Ctrl+Delete", self.onDelete], + [tr(TR.ACTIONS_SUSPEND_CARD), "@", self.suspend_current_card], + [tr(TR.STUDYING_SUSPEND_NOTE), "!", self.suspend_current_note], + [tr(TR.STUDYING_DELETE_NOTE), "Ctrl+Delete", self.delete_current_note], [tr(TR.ACTIONS_OPTIONS), "O", self.onOptions], None, [tr(TR.ACTIONS_REPLAY_AUDIO), "R", self.replayAudio], @@ -779,22 +782,24 @@ time = %(time)d; def onOptions(self) -> None: self.mw.onDeckConf(self.mw.col.decks.get(self.card.odid or self.card.did)) - def setFlag(self, flag: int) -> None: + def set_flag_on_current_card(self, flag: int) -> None: # need to toggle off? - if self.card.userFlag() == flag: + if self.card.user_flag() == flag: flag = 0 - self.card.setUserFlag(flag) - self.card.flush() - self._drawFlag() + self.card.set_user_flag(flag) + self.mw.col.update_card(self.card) + self.mw.update_undo_actions() + self._update_flag_icon() - def onMark(self) -> None: - f = self.card.note() - if f.hasTag("marked"): - f.delTag("marked") + def toggle_mark_on_current_note(self) -> None: + note = self.card.note() + if note.has_tag("marked"): + note.remove_tag("marked") else: - f.addTag("marked") - f.flush() - self._drawMark() + note.add_tag("marked") + self.mw.col.update_note(note) + self.mw.update_undo_actions() + self._update_mark_icon() def on_set_due(self) -> None: if self.mw.state != "review" or not self.card: @@ -808,41 +813,36 @@ time = %(time)d; on_done=self.mw.reset, ) - def onSuspend(self) -> None: - self.mw.checkpoint(tr(TR.STUDYING_SUSPEND)) + def suspend_current_note(self) -> None: self.mw.col.sched.suspend_cards([c.id for c in self.card.note().cards()]) + self.mw.reset() tooltip(tr(TR.STUDYING_NOTE_SUSPENDED)) - self.mw.reset() - def onSuspendCard(self) -> None: - self.mw.checkpoint(tr(TR.STUDYING_SUSPEND)) + def suspend_current_card(self) -> None: self.mw.col.sched.suspend_cards([self.card.id]) + self.mw.reset() tooltip(tr(TR.STUDYING_CARD_SUSPENDED)) - self.mw.reset() - def onDelete(self) -> None: - # need to check state because the shortcut is global to the main - # window - if self.mw.state != "review" or not self.card: - return - self.mw.checkpoint(tr(TR.ACTIONS_DELETE)) - cnt = len(self.card.note().cards()) - self.mw.col.remove_notes([self.card.note().id]) - self.mw.reset() - tooltip(tr(TR.STUDYING_NOTE_AND_ITS_CARD_DELETED, count=cnt)) - - def onBuryCard(self) -> None: - self.mw.checkpoint(tr(TR.STUDYING_BURY)) + def bury_current_card(self) -> None: self.mw.col.sched.bury_cards([self.card.id]) self.mw.reset() tooltip(tr(TR.STUDYING_CARD_BURIED)) - def onBuryNote(self) -> None: - self.mw.checkpoint(tr(TR.STUDYING_BURY)) + def bury_current_note(self) -> None: self.mw.col.sched.bury_note(self.card.note()) self.mw.reset() tooltip(tr(TR.STUDYING_NOTE_BURIED)) + def delete_current_note(self) -> None: + # need to check state because the shortcut is global to the main + # window + if self.mw.state != "review" or not self.card: + return + cnt = len(self.card.note().cards()) + self.mw.col.remove_notes([self.card.note().id]) + self.mw.reset() + tooltip(tr(TR.STUDYING_NOTE_AND_ITS_CARD_DELETED, count=cnt)) + def onRecordVoice(self) -> None: def after_record(path: str) -> None: self._recordedAudio = path @@ -855,3 +855,13 @@ time = %(time)d; tooltip(tr(TR.STUDYING_YOU_HAVENT_RECORDED_YOUR_VOICE_YET)) return av_player.play_file(self._recordedAudio) + + # legacy + + onBuryCard = bury_current_card + onBuryNote = bury_current_note + onSuspend = suspend_current_note + onSuspendCard = suspend_current_card + onDelete = delete_current_note + onMark = toggle_mark_on_current_note + setFlag = set_flag_on_current_card diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 6dc14f9dd..4b42a7b55 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -11,7 +11,7 @@ from typing import Dict, Iterable, List, Optional, Tuple, cast import aqt from anki.collection import Config, SearchJoiner, SearchNode from anki.decks import DeckTreeNode -from anki.errors import DeckRenameError, InvalidInput +from anki.errors import DeckIsFilteredError, InvalidInput from anki.notes import Note from anki.tags import TagTreeNode from anki.types import assert_exhaustive @@ -587,7 +587,11 @@ class SidebarTreeView(QTreeView): def on_done(fut: Future) -> None: self.browser.model.endReset() - fut.result() + try: + fut.result() + except Exception as e: + showWarning(str(e)) + return self.refresh() self.mw.deckBrowser.refresh() @@ -1133,8 +1137,8 @@ class SidebarTreeView(QTreeView): self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK)) try: self.mw.col.decks.rename(deck, new_name) - except DeckRenameError as e: - showWarning(e.description) + except DeckIsFilteredError as err: + showWarning(str(err)) return self.refresh( lambda other: other.item_type == SidebarItemType.DECK diff --git a/qt/aqt/stats.py b/qt/aqt/stats.py index 04158ae6c..a031b9ba2 100644 --- a/qt/aqt/stats.py +++ b/qt/aqt/stats.py @@ -27,7 +27,7 @@ class NewDeckStats(QDialog): def __init__(self, mw: aqt.main.AnkiQt) -> None: QDialog.__init__(self, mw, Qt.Window) - mw.setupDialogGC(self) + mw.garbage_collect_on_dialog_finish(self) self.mw = mw self.name = "deckStats" self.period = 0 @@ -108,7 +108,7 @@ class DeckStats(QDialog): def __init__(self, mw: aqt.main.AnkiQt) -> None: QDialog.__init__(self, mw, Qt.Window) - mw.setupDialogGC(self) + mw.garbage_collect_on_dialog_finish(self) self.mw = mw self.name = "deckStats" self.period = 0 diff --git a/qt/aqt/studydeck.py b/qt/aqt/studydeck.py index d44ff1d26..cedac958c 100644 --- a/qt/aqt/studydeck.py +++ b/qt/aqt/studydeck.py @@ -4,6 +4,7 @@ from typing import List, Optional import aqt +from anki.errors import DeckIsFilteredError from aqt import gui_hooks from aqt.qt import * from aqt.utils import ( @@ -17,6 +18,7 @@ from aqt.utils import ( saveGeom, shortcut, showInfo, + showWarning, tr, ) @@ -37,8 +39,6 @@ class StudyDeck(QDialog): geomKey: str = "default", ) -> None: QDialog.__init__(self, parent or mw) - if buttons is None: - buttons = [] self.mw = mw self.form = aqt.forms.studydeck.Ui_Dialog() self.form.setupUi(self) @@ -52,7 +52,7 @@ class StudyDeck(QDialog): self.form.buttonBox.removeButton( self.form.buttonBox.button(QDialogButtonBox.Cancel) ) - if buttons: + if buttons is not None: for b in buttons: self.form.buttonBox.addButton(b, QDialogButtonBox.ActionRole) else: @@ -164,7 +164,11 @@ class StudyDeck(QDialog): n = getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=default) n = n.strip() if n: - did = self.mw.col.decks.id(n) + try: + did = self.mw.col.decks.id(n) + except DeckIsFilteredError as err: + showWarning(str(err)) + return # deck name may not be the same as user input. ex: ", :: self.name = self.mw.col.decks.name(did) # make sure we clean up reset hook when manually exiting diff --git a/repos.bzl b/repos.bzl index 4de5f80a6..c9a1befd5 100644 --- a/repos.bzl +++ b/repos.bzl @@ -132,12 +132,12 @@ def register_repos(): ################ core_i18n_repo = "anki-core-i18n" - core_i18n_commit = "6ab5e0ee365c411701ded28d612dfe58e84abd0e" - core_i18n_zip_csum = "43b787f7e984ae75f43c0da2ba05aa3f85cdcad16232ebed060abf3d36f22545" + core_i18n_commit = "08132e398863b7c57ac3345c34c96db7f346a385" + core_i18n_zip_csum = "24ed30d44bd277f0b19080084cb2bc5bf2cd8f6e691362928e7cb96f33ffda4c" qtftl_i18n_repo = "anki-desktop-ftl" - qtftl_i18n_commit = "5310d51de506f3f99705d9cc88a64eafa0bb2062" - qtftl_i18n_zip_csum = "0d6ded85e02bedaa4b279122f6cffa43f24adb10cba9865c6417973d779fd725" + qtftl_i18n_commit = "3668fb523ffa8162d5de878e6037f6cf9a98f97d" + qtftl_i18n_zip_csum = "2cb5b89d125edd1d3a6d7349b8aa2f1dc3a0a007aaf3d1f4ca08ea353e6676ee" i18n_build_content = """ filegroup( diff --git a/rslib/BUILD.bazel b/rslib/BUILD.bazel index 52a886621..cfc878e27 100644 --- a/rslib/BUILD.bazel +++ b/rslib/BUILD.bazel @@ -66,9 +66,9 @@ rust_library( compile_data = _anki_compile_data, crate_features = _anki_features, proc_macro_deps = [ + "//rslib/cargo:async_trait", "//rslib/cargo:serde_derive", "//rslib/cargo:serde_repr", - "//rslib/cargo:async_trait", ], rustc_env = _anki_rustc_env, visibility = ["//visibility:public"], @@ -84,6 +84,7 @@ rust_library( "//rslib/cargo:failure", "//rslib/cargo:flate2", "//rslib/cargo:fluent", + "//rslib/cargo:fnv", "//rslib/cargo:futures", "//rslib/cargo:hex", "//rslib/cargo:htmlescape", @@ -112,6 +113,7 @@ rust_library( "//rslib/cargo:slog_async", "//rslib/cargo:slog_envlogger", "//rslib/cargo:slog_term", + "//rslib/cargo:strum", "//rslib/cargo:tempfile", "//rslib/cargo:tokio", "//rslib/cargo:unic_langid", diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index 657cb372c..c7f6c7635 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -23,6 +23,12 @@ env_logger = "0.8.1" [dependencies] # pinned as any changes could invalidate sqlite indexes unicase = "=2.6.0" +# transitive dependency; 0.1.7 is currently broken on Windows (perhaps +# only in Bazel) +proc-macro-nested = "=0.1.6" +# slog-term 2.7+ depends on term 0.7.0, which is currently broken on Windows, +# as cargo-raze doesn't seem to be included the rustversion crate. +slog-term = "=2.6.0" askama = "0.10.1" async-compression = { version = "0.3.5", features = ["stream", "gzip"] } @@ -67,7 +73,6 @@ sha1 = "0.6.0" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_debug"] } slog-async = "2.5.0" slog-envlogger = "2.2.0" -slog-term = "2.6.0" tempfile = "3.1.0" tokio = { version = "0.2.21", features = ["fs", "rt-threaded"] } unic-langid = { version = "0.9", features = ["macros"] } @@ -75,9 +80,7 @@ unicode-normalization = "0.1.13" utime = "0.3.1" zip = { version = "0.5.6", default-features = false, features = ["deflate", "time"] } async-trait = "0.1.42" - -# transitive dependency; 0.1.7 is currently broken on Windows (perhaps -# only in Bazel) -proc-macro-nested = "=0.1.6" ammonia = "3.1.0" pulldown-cmark = "0.8.0" +fnv = "1.0.7" +strum = { version = "0.20.0", features = ["derive"] } diff --git a/rslib/backend.proto b/rslib/backend.proto index 8840cdfde..751c14116 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -124,6 +124,7 @@ service BackendService { rpc StateIsLeech(SchedulingState) returns (Bool); rpc AnswerCard(AnswerCardIn) returns (Empty); rpc UpgradeScheduler(Empty) returns (Empty); + rpc GetQueuedCards(GetQueuedCardsIn) returns (GetQueuedCardsOut); // stats @@ -165,8 +166,7 @@ service BackendService { // cards rpc GetCard(CardID) returns (Card); - rpc UpdateCard(Card) returns (Empty); - rpc AddCard(Card) returns (CardID); + rpc UpdateCard(UpdateCardIn) returns (Empty); rpc RemoveCards(RemoveCardsIn) returns (Empty); rpc SetDeck(SetDeckIn) returns (Empty); @@ -174,7 +174,9 @@ service BackendService { rpc NewNote(NoteTypeID) returns (Note); rpc AddNote(AddNoteIn) returns (NoteID); - rpc UpdateNote(Note) returns (Empty); + rpc DefaultsForAdding(DefaultsForAddingIn) returns (DeckAndNotetype); + rpc DefaultDeckForNotetype(NoteTypeID) returns (DeckID); + rpc UpdateNote(UpdateNoteIn) returns (Empty); rpc GetNote(NoteID) returns (Note); rpc RemoveNotes(RemoveNotesIn) returns (Empty); rpc AddNoteTags(AddNoteTagsIn) returns (UInt32); @@ -200,6 +202,9 @@ service BackendService { rpc OpenCollection(OpenCollectionIn) returns (Empty); rpc CloseCollection(CloseCollectionIn) returns (Empty); rpc CheckDatabase(Empty) returns (CheckDatabaseOut); + rpc GetUndoStatus(Empty) returns (UndoStatus); + rpc Undo(Empty) returns (UndoStatus); + rpc Redo(Empty) returns (UndoStatus); // sync @@ -257,7 +262,17 @@ message DeckConfigInner { NEW_CARD_ORDER_DUE = 0; NEW_CARD_ORDER_RANDOM = 1; } - + enum ReviewCardOrder { + REVIEW_CARD_ORDER_SHUFFLED_BY_DAY = 0; + REVIEW_CARD_ORDER_SHUFFLED = 1; + REVIEW_CARD_ORDER_INTERVALS_ASCENDING = 2; + REVIEW_CARD_ORDER_INTERVALS_DESCENDING = 3; + } + enum ReviewMix { + REVIEW_MIX_MIX_WITH_REVIEWS = 0; + REVIEW_MIX_AFTER_REVIEWS = 1; + REVIEW_MIX_BEFORE_REVIEWS = 2; + } enum LeechAction { LEECH_ACTION_SUSPEND = 0; LEECH_ACTION_TAG_ONLY = 1; @@ -270,6 +285,7 @@ message DeckConfigInner { uint32 new_per_day = 9; uint32 reviews_per_day = 10; + uint32 new_per_day_minimum = 29; float initial_ease = 11; float easy_multiplier = 12; @@ -284,6 +300,10 @@ message DeckConfigInner { uint32 graduating_interval_easy = 19; NewCardOrder new_card_order = 20; + ReviewCardOrder review_order = 32; + + ReviewMix new_mix = 30; + ReviewMix interday_learning_mix = 31; LeechAction leech_action = 21; uint32 leech_threshold = 22; @@ -390,7 +410,7 @@ message NoteTypeConfig { Kind kind = 1; uint32 sort_field_idx = 2; string css = 3; - int64 target_deck_id = 4; + int64 target_deck_id = 4; // moved into config var string latex_pre = 5; string latex_post = 6; bool latex_svg = 7; @@ -934,6 +954,16 @@ message AddNoteIn { int64 deck_id = 2; } +message UpdateNoteIn { + Note note = 1; + bool skip_undo_entry = 2; +} + +message UpdateCardIn { + Card card = 1; + bool skip_undo_entry = 2; +} + message EmptyCardsReport { message NoteWithEmptyCards { int64 note_id = 1; @@ -1248,6 +1278,8 @@ message Config { COLLAPSE_TODAY = 6; COLLAPSE_CARD_STATE = 7; COLLAPSE_FLAGS = 8; + SCHED_2021 = 9; + ADDING_DEFAULTS_TO_CURRENT_DECK = 10; } Key key = 1; } @@ -1305,7 +1337,7 @@ message SchedulingState { } message Preview { uint32 scheduled_secs = 1; - Normal original_state = 2; + bool finished = 2; } message ReschedulingFilter { Normal original_state = 1; @@ -1346,3 +1378,48 @@ message AnswerCardIn { int64 answered_at_millis = 5; uint32 milliseconds_taken = 6; } + +message GetQueuedCardsIn { + uint32 fetch_limit = 1; + bool intraday_learning_only = 2; +} + +message GetQueuedCardsOut { + enum Queue { + New = 0; + Learning = 1; + Review = 2; + } + + message QueuedCard { + Card card = 1; + Queue queue = 2; + NextCardStates next_states = 3; + } + + message QueuedCards { + repeated QueuedCard cards = 1; + uint32 new_count = 2; + uint32 learning_count = 3; + uint32 review_count = 4; + } + + oneof value { + QueuedCards queued_cards = 1; + CongratsInfoOut congrats_info = 2; + } +} + +message UndoStatus { + string undo = 1; + string redo = 2; +} + +message DefaultsForAddingIn { + int64 home_deck_of_current_review_card = 1; +} + +message DeckAndNotetype { + int64 deck_id = 1; + int64 notetype_id = 2; +} diff --git a/rslib/cargo/BUILD.bazel b/rslib/cargo/BUILD.bazel index 3e0b3dae5..2ce7124a9 100644 --- a/rslib/cargo/BUILD.bazel +++ b/rslib/cargo/BUILD.bazel @@ -14,7 +14,7 @@ licenses([ # Aliased targets alias( name = "ammonia", - actual = "@raze__ammonia__3_1_0//:ammonia", + actual = "@raze__ammonia__3_1_1//:ammonia", tags = [ "cargo-raze", "manual", @@ -41,7 +41,7 @@ alias( alias( name = "async_trait", - actual = "@raze__async_trait__0_1_42//:async_trait", + actual = "@raze__async_trait__0_1_48//:async_trait", tags = [ "cargo-raze", "manual", @@ -86,7 +86,7 @@ alias( alias( name = "env_logger", - actual = "@raze__env_logger__0_8_2//:env_logger", + actual = "@raze__env_logger__0_8_3//:env_logger", tags = [ "cargo-raze", "manual", @@ -129,9 +129,18 @@ alias( ], ) +alias( + name = "fnv", + actual = "@raze__fnv__1_0_7//:fnv", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "futures", - actual = "@raze__futures__0_3_12//:futures", + actual = "@raze__futures__0_3_13//:futures", tags = [ "cargo-raze", "manual", @@ -140,7 +149,7 @@ alias( alias( name = "hex", - actual = "@raze__hex__0_4_2//:hex", + actual = "@raze__hex__0_4_3//:hex", tags = [ "cargo-raze", "manual", @@ -185,7 +194,7 @@ alias( alias( name = "nom", - actual = "@raze__nom__6_1_0//:nom", + actual = "@raze__nom__6_1_2//:nom", tags = [ "cargo-raze", "manual", @@ -221,7 +230,7 @@ alias( alias( name = "once_cell", - actual = "@raze__once_cell__1_5_2//:once_cell", + actual = "@raze__once_cell__1_7_2//:once_cell", tags = [ "cargo-raze", "manual", @@ -320,7 +329,7 @@ alias( alias( name = "serde", - actual = "@raze__serde__1_0_123//:serde", + actual = "@raze__serde__1_0_124//:serde", tags = [ "cargo-raze", "manual", @@ -338,7 +347,7 @@ alias( alias( name = "serde_derive", - actual = "@raze__serde_derive__1_0_123//:serde_derive", + actual = "@raze__serde_derive__1_0_124//:serde_derive", tags = [ "cargo-raze", "manual", @@ -347,7 +356,7 @@ alias( alias( name = "serde_json", - actual = "@raze__serde_json__1_0_62//:serde_json", + actual = "@raze__serde_json__1_0_64//:serde_json", tags = [ "cargo-raze", "manual", @@ -417,6 +426,15 @@ alias( ], ) +alias( + name = "strum", + actual = "@raze__strum__0_20_0//:strum", + tags = [ + "cargo-raze", + "manual", + ], +) + alias( name = "tempfile", actual = "@raze__tempfile__3_2_0//:tempfile", @@ -455,7 +473,7 @@ alias( alias( name = "unicode_normalization", - actual = "@raze__unicode_normalization__0_1_16//:unicode_normalization", + actual = "@raze__unicode_normalization__0_1_17//:unicode_normalization", tags = [ "cargo-raze", "manual", diff --git a/rslib/src/adding.rs b/rslib/src/adding.rs new file mode 100644 index 000000000..090c54a2a --- /dev/null +++ b/rslib/src/adding.rs @@ -0,0 +1,113 @@ +// 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::prelude::*; + +pub struct DeckAndNotetype { + pub deck_id: DeckID, + pub notetype_id: NoteTypeID, +} + +impl Collection { + /// An option in the preferences screen governs the behaviour here. + /// + /// - When 'default to the current deck' is enabled, we use the current deck + /// if it's normal, the provided reviewer card's deck as a fallback, and + /// Default as a final fallback. We then fetch the last used notetype stored + /// in the deck, falling back to the global notetype, or the first available one. + /// + /// - Otherwise, each note type remembers the last deck cards were added to, + /// and we use that, defaulting to the current deck if missing, and + /// Default otherwise. + pub fn defaults_for_adding( + &mut self, + home_deck_of_reviewer_card: DeckID, + ) -> Result { + let deck_id; + let notetype_id; + if self.get_bool(BoolKey::AddingDefaultsToCurrentDeck) { + deck_id = self + .get_current_deck_for_adding(home_deck_of_reviewer_card)? + .id; + notetype_id = self.default_notetype_for_deck(deck_id)?.id; + } else { + notetype_id = self.get_current_notetype_for_adding()?.id; + deck_id = if let Some(deck_id) = self.default_deck_for_notetype(notetype_id)? { + deck_id + } else { + // default not set in notetype; fall back to current deck + self.get_current_deck_for_adding(home_deck_of_reviewer_card)? + .id + }; + } + + Ok(DeckAndNotetype { + deck_id, + notetype_id, + }) + } + + /// The currently selected deck, the home deck of the provided card, or the default deck. + fn get_current_deck_for_adding( + &mut self, + home_deck_of_reviewer_card: DeckID, + ) -> Result> { + // current deck, if not filtered + if let Some(current) = self.get_deck(self.get_current_deck_id())? { + if !current.is_filtered() { + return Ok(current); + } + } + // provided reviewer card's home deck + if let Some(home_deck) = self.get_deck(home_deck_of_reviewer_card)? { + return Ok(home_deck); + } + // default deck + self.get_deck(DeckID(1))?.ok_or(AnkiError::NotFound) + } + + fn get_current_notetype_for_adding(&mut self) -> Result> { + // try global 'current' notetype + if let Some(ntid) = self.get_current_notetype_id() { + if let Some(nt) = self.get_notetype(ntid)? { + return Ok(nt); + } + } + // try first available notetype + if let Some((ntid, _)) = self.storage.get_all_notetype_names()?.first() { + Ok(self.get_notetype(*ntid)?.unwrap()) + } else { + Err(AnkiError::NotFound) + } + } + + fn default_notetype_for_deck(&mut self, deck: DeckID) -> Result> { + // try last notetype used by deck + if let Some(ntid) = self.get_last_notetype_for_deck(deck) { + if let Some(nt) = self.get_notetype(ntid)? { + return Ok(nt); + } + } + + // fall back + self.get_current_notetype_for_adding() + } + + /// Returns the last deck added to with this notetype, provided it is valid. + /// This is optional due to the inconsistent handling, where changes in notetype + /// may need to update the current deck, but not vice versa. If a previous deck is + /// not set, we want to keep the current selection, instead of resetting it. + pub(crate) fn default_deck_for_notetype(&mut self, ntid: NoteTypeID) -> Result> { + if let Some(last_deck_id) = self.get_last_deck_added_to_for_notetype(ntid) { + if let Some(deck) = self.get_deck(last_deck_id)? { + if !deck.is_filtered() { + return Ok(Some(deck.id)); + } + } + } + + Ok(None) + } +} diff --git a/rslib/src/backend/adding.rs b/rslib/src/backend/adding.rs new file mode 100644 index 000000000..50d24512c --- /dev/null +++ b/rslib/src/backend/adding.rs @@ -0,0 +1,14 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::adding::DeckAndNotetype; +use crate::backend_proto::DeckAndNotetype as DeckAndNotetypeProto; + +impl From for DeckAndNotetypeProto { + fn from(s: DeckAndNotetype) -> Self { + DeckAndNotetypeProto { + deck_id: s.deck_id.0, + notetype_id: s.notetype_id.0, + } + } +} diff --git a/rslib/src/backend/card.rs b/rslib/src/backend/card.rs new file mode 100644 index 000000000..c859d8312 --- /dev/null +++ b/rslib/src/backend/card.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::convert::TryFrom; + +use crate::prelude::*; +use crate::{ + backend_proto as pb, + card::{CardQueue, CardType}, +}; + +impl TryFrom for Card { + type Error = AnkiError; + + fn try_from(c: pb::Card) -> Result { + let ctype = CardType::try_from(c.ctype as u8) + .map_err(|_| AnkiError::invalid_input("invalid card type"))?; + let queue = CardQueue::try_from(c.queue as i8) + .map_err(|_| AnkiError::invalid_input("invalid card queue"))?; + Ok(Card { + id: CardID(c.id), + note_id: NoteID(c.note_id), + deck_id: DeckID(c.deck_id), + template_idx: c.template_idx as u16, + mtime: TimestampSecs(c.mtime_secs), + usn: Usn(c.usn), + ctype, + queue, + due: c.due, + interval: c.interval, + ease_factor: c.ease_factor as u16, + reps: c.reps, + lapses: c.lapses, + remaining_steps: c.remaining_steps, + original_due: c.original_due, + original_deck_id: DeckID(c.original_deck_id), + flags: c.flags as u8, + data: c.data, + }) + } +} diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs new file mode 100644 index 000000000..fcac7d7cc --- /dev/null +++ b/rslib/src/backend/config.rs @@ -0,0 +1,36 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{ + backend_proto as pb, + config::{BoolKey, StringKey}, +}; +use pb::config::bool::Key as BoolKeyProto; +use pb::config::string::Key as StringKeyProto; + +impl From for BoolKey { + fn from(k: BoolKeyProto) -> Self { + match k { + BoolKeyProto::BrowserSortBackwards => BoolKey::BrowserSortBackwards, + BoolKeyProto::PreviewBothSides => BoolKey::PreviewBothSides, + BoolKeyProto::CollapseTags => BoolKey::CollapseTags, + BoolKeyProto::CollapseNotetypes => BoolKey::CollapseNotetypes, + BoolKeyProto::CollapseDecks => BoolKey::CollapseDecks, + BoolKeyProto::CollapseSavedSearches => BoolKey::CollapseSavedSearches, + BoolKeyProto::CollapseToday => BoolKey::CollapseToday, + BoolKeyProto::CollapseCardState => BoolKey::CollapseCardState, + BoolKeyProto::CollapseFlags => BoolKey::CollapseFlags, + BoolKeyProto::Sched2021 => BoolKey::Sched2021, + BoolKeyProto::AddingDefaultsToCurrentDeck => BoolKey::AddingDefaultsToCurrentDeck, + } + } +} + +impl From for StringKey { + fn from(k: StringKeyProto) -> Self { + match k { + StringKeyProto::SetDueBrowser => StringKey::SetDueBrowser, + StringKeyProto::SetDueReviewer => StringKey::SetDueReviewer, + } + } +} diff --git a/rslib/src/backend/dbproxy.rs b/rslib/src/backend/dbproxy.rs index beb0318b0..49207d334 100644 --- a/rslib/src/backend/dbproxy.rs +++ b/rslib/src/backend/dbproxy.rs @@ -1,8 +1,8 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::err::Result; use crate::storage::SqliteStorage; +use crate::{collection::Collection, err::Result}; use rusqlite::types::{FromSql, FromSqlError, ToSql, ToSqlOutput, ValueRef}; use rusqlite::OptionalExtension; use serde_derive::{Deserialize, Serialize}; @@ -67,7 +67,7 @@ impl FromSql for SqlValue { } } -pub(super) fn db_command_bytes(ctx: &SqliteStorage, input: &[u8]) -> Result> { +pub(super) fn db_command_bytes(col: &mut Collection, input: &[u8]) -> Result> { let req: DBRequest = serde_json::from_slice(input)?; let resp = match req { DBRequest::Query { @@ -75,29 +75,52 @@ pub(super) fn db_command_bytes(ctx: &SqliteStorage, input: &[u8]) -> Result { + maybe_clear_undo(col, &sql); if first_row_only { - db_query_row(ctx, &sql, &args)? + db_query_row(&col.storage, &sql, &args)? } else { - db_query(ctx, &sql, &args)? + db_query(&col.storage, &sql, &args)? } } DBRequest::Begin => { - ctx.begin_trx()?; + col.storage.begin_trx()?; DBResult::None } DBRequest::Commit => { - ctx.commit_trx()?; + col.storage.commit_trx()?; DBResult::None } DBRequest::Rollback => { - ctx.rollback_trx()?; + col.clear_caches(); + col.storage.rollback_trx()?; DBResult::None } - DBRequest::ExecuteMany { sql, args } => db_execute_many(ctx, &sql, &args)?, + DBRequest::ExecuteMany { sql, args } => { + maybe_clear_undo(col, &sql); + db_execute_many(&col.storage, &sql, &args)? + } }; Ok(serde_json::to_vec(&resp)?) } +fn maybe_clear_undo(col: &mut Collection, sql: &str) { + if !is_dql(sql) { + println!("clearing undo+study due to {}", sql); + col.discard_undo_and_study_queues(); + } +} + +/// Anything other than a select statement is false. +fn is_dql(sql: &str) -> bool { + let head: String = sql + .trim_start() + .chars() + .take(10) + .map(|c| c.to_ascii_lowercase()) + .collect(); + head.starts_with("select ") +} + pub(super) fn db_query_row(ctx: &SqliteStorage, sql: &str, args: &[SqlValue]) -> Result { let mut stmt = ctx.db.prepare_cached(sql)?; let columns = stmt.column_count(); diff --git a/rslib/src/backend/err.rs b/rslib/src/backend/err.rs new file mode 100644 index 000000000..3cf310956 --- /dev/null +++ b/rslib/src/backend/err.rs @@ -0,0 +1,70 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{ + backend_proto as pb, + err::{AnkiError, NetworkErrorKind, SyncErrorKind}, + prelude::*, +}; + +/// Convert an Anki error to a protobuf error. +pub(super) fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError { + use pb::backend_error::Value as V; + let localized = err.localized_description(i18n); + let value = match err { + AnkiError::InvalidInput { .. } => V::InvalidInput(pb::Empty {}), + AnkiError::TemplateError { .. } => V::TemplateParse(pb::Empty {}), + AnkiError::IOError { .. } => V::IoError(pb::Empty {}), + AnkiError::DBError { .. } => V::DbError(pb::Empty {}), + AnkiError::NetworkError { kind, .. } => { + V::NetworkError(pb::NetworkError { kind: kind.into() }) + } + AnkiError::SyncError { kind, .. } => V::SyncError(pb::SyncError { kind: kind.into() }), + AnkiError::Interrupted => V::Interrupted(pb::Empty {}), + AnkiError::CollectionNotOpen => V::InvalidInput(pb::Empty {}), + AnkiError::CollectionAlreadyOpen => V::InvalidInput(pb::Empty {}), + AnkiError::JSONError { info } => V::JsonError(info), + AnkiError::ProtoError { info } => V::ProtoError(info), + AnkiError::NotFound => V::NotFoundError(pb::Empty {}), + AnkiError::Existing => V::Exists(pb::Empty {}), + AnkiError::DeckIsFiltered => V::DeckIsFiltered(pb::Empty {}), + AnkiError::SearchError(_) => V::InvalidInput(pb::Empty {}), + AnkiError::TemplateSaveError { .. } => V::TemplateParse(pb::Empty {}), + AnkiError::ParseNumError => V::InvalidInput(pb::Empty {}), + }; + + pb::BackendError { + value: Some(value), + localized, + } +} + +impl std::convert::From for i32 { + fn from(e: NetworkErrorKind) -> Self { + use pb::network_error::NetworkErrorKind as V; + (match e { + NetworkErrorKind::Offline => V::Offline, + NetworkErrorKind::Timeout => V::Timeout, + NetworkErrorKind::ProxyAuth => V::ProxyAuth, + NetworkErrorKind::Other => V::Other, + }) as i32 + } +} + +impl std::convert::From for i32 { + fn from(e: SyncErrorKind) -> Self { + use pb::sync_error::SyncErrorKind as V; + (match e { + SyncErrorKind::Conflict => V::Conflict, + SyncErrorKind::ServerError => V::ServerError, + SyncErrorKind::ClientTooOld => V::ClientTooOld, + SyncErrorKind::AuthFailed => V::AuthFailed, + SyncErrorKind::ServerMessage => V::ServerMessage, + SyncErrorKind::ResyncRequired => V::ResyncRequired, + SyncErrorKind::DatabaseCheckRequired => V::DatabaseCheckRequired, + SyncErrorKind::Other => V::Other, + SyncErrorKind::ClockIncorrect => V::ClockIncorrect, + SyncErrorKind::SyncNotStarted => V::SyncNotStarted, + }) as i32 + } +} diff --git a/rslib/src/backend/generic.rs b/rslib/src/backend/generic.rs index 19edbe04a..e554761f4 100644 --- a/rslib/src/backend/generic.rs +++ b/rslib/src/backend/generic.rs @@ -75,6 +75,12 @@ impl From for Vec { } } +impl From for pb::DeckId { + fn from(did: DeckID) -> Self { + pb::DeckId { did: did.0 } + } +} + impl From for DeckConfID { fn from(dcid: pb::DeckConfigId) -> Self { DeckConfID(dcid.dcid) diff --git a/rslib/src/backend/http_sync_server.rs b/rslib/src/backend/http_sync_server.rs index 971e21ddd..efdc9f3ab 100644 --- a/rslib/src/backend/http_sync_server.rs +++ b/rslib/src/backend/http_sync_server.rs @@ -189,7 +189,7 @@ impl Backend { fn download(&self) -> Result> { let server = Box::new(self.col_into_server()?); let mut rt = Runtime::new().unwrap(); - let file = rt.block_on(server.full_download())?; + let file = rt.block_on(server.full_download(None))?; let path = file.into_temp_path().keep()?; Ok(path.to_str().expect("path was not in utf8").into()) } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 72ca2b7ff..511a9830b 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -1,30 +1,37 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +mod adding; +mod card; +mod config; +mod dbproxy; +mod err; +mod generic; +mod http_sync_server; +mod progress; +mod scheduler; +mod search; +mod sync; + pub use crate::backend_proto::BackendMethod; use crate::{ backend::dbproxy::db_command_bytes, backend_proto as pb, backend_proto::{ - sort_order::builtin::Kind as SortKindProto, sort_order::Value as SortOrderProto, AddOrUpdateDeckConfigLegacyIn, BackendResult, Empty, RenderedTemplateReplacement, }, card::{Card, CardID}, - card::{CardQueue, CardType}, cloze::add_cloze_numbers_in_string, collection::{open_collection, Collection}, - config::SortKind, - dbcheck::DatabaseCheckProgress, deckconf::{DeckConf, DeckConfSchema11}, decks::{Deck, DeckID, DeckSchema11}, - err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind}, - i18n::{tr_args, I18n, TR}, + err::{AnkiError, Result}, + i18n::I18n, latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex}, log, log::default_logger, markdown::render_markdown, media::check::MediaChecker, - media::sync::MediaSyncProgress, media::MediaManager, notes::{Note, NoteID}, notetype::{ @@ -36,31 +43,22 @@ use crate::{ states::{CardState, NextCardStates}, timespan::{answer_button_time, time_span}, }, - search::{ - concatenate_searches, parse_search, replace_search_node, write_nodes, BoolSeparator, Node, - PropertyKind, RatingKind, SearchNode, SortMode, StateKind, TemplateKind, - }, + search::{concatenate_searches, replace_search_node, write_nodes, Node}, stats::studied_today, - sync::{ - get_remote_sync_meta, http::SyncRequest, sync_abort, sync_login, FullSyncProgress, - LocalServer, NormalSyncProgress, SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, - SyncStage, - }, + sync::{http::SyncRequest, LocalServer}, template::RenderedNode, - text::{escape_anki_wildcards, extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag}, + text::{extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag}, timestamp::TimestampSecs, - types::Usn, + undo::UndoableOpKind, }; use fluent::FluentValue; -use futures::future::{AbortHandle, AbortRegistration, Abortable}; -use itertools::Itertools; +use futures::future::AbortHandle; use log::error; use once_cell::sync::OnceCell; -use pb::{sync_status_out, BackendService}; +use pb::BackendService; +use progress::{AbortHandleSlot, Progress}; use prost::Message; use serde_json::Value as JsonValue; -use slog::warn; -use std::convert::TryFrom; use std::{collections::HashSet, convert::TryInto}; use std::{ result, @@ -68,39 +66,11 @@ use std::{ }; use tokio::runtime::{self, Runtime}; -mod dbproxy; -mod generic; -mod http_sync_server; -mod scheduler; - -struct ThrottlingProgressHandler { - state: Arc>, - last_update: coarsetime::Instant, -} - -impl ThrottlingProgressHandler { - /// Returns true if should continue. - fn update(&mut self, progress: impl Into, throttle: bool) -> bool { - let now = coarsetime::Instant::now(); - if throttle && now.duration_since(self.last_update).as_f64() < 0.1 { - return true; - } - self.last_update = now; - let mut guard = self.state.lock().unwrap(); - guard.last_progress.replace(progress.into()); - let want_abort = guard.want_abort; - guard.want_abort = false; - !want_abort - } -} - -struct ProgressState { - want_abort: bool, - last_progress: Option, -} - -// fixme: this should support multiple abort handles. -type AbortHandleSlot = Arc>>; +use self::{ + err::anki_error_to_proto_error, + progress::{progress_to_proto, ProgressState}, + sync::RemoteSyncStatus, +}; pub struct Backend { col: Arc>>, @@ -121,90 +91,6 @@ struct BackendState { http_sync_server: Option, } -#[derive(Default, Debug)] -pub(crate) struct RemoteSyncStatus { - last_check: TimestampSecs, - last_response: sync_status_out::Required, -} - -impl RemoteSyncStatus { - fn update(&mut self, required: sync_status_out::Required) { - self.last_check = TimestampSecs::now(); - self.last_response = required - } -} - -#[derive(Clone, Copy)] -enum Progress { - MediaSync(MediaSyncProgress), - MediaCheck(u32), - FullSync(FullSyncProgress), - NormalSync(NormalSyncProgress), - DatabaseCheck(DatabaseCheckProgress), -} - -/// Convert an Anki error to a protobuf error. -fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError { - use pb::backend_error::Value as V; - let localized = err.localized_description(i18n); - let value = match err { - AnkiError::InvalidInput { .. } => V::InvalidInput(pb::Empty {}), - AnkiError::TemplateError { .. } => V::TemplateParse(pb::Empty {}), - AnkiError::IOError { .. } => V::IoError(pb::Empty {}), - AnkiError::DBError { .. } => V::DbError(pb::Empty {}), - AnkiError::NetworkError { kind, .. } => { - V::NetworkError(pb::NetworkError { kind: kind.into() }) - } - AnkiError::SyncError { kind, .. } => V::SyncError(pb::SyncError { kind: kind.into() }), - AnkiError::Interrupted => V::Interrupted(Empty {}), - AnkiError::CollectionNotOpen => V::InvalidInput(pb::Empty {}), - AnkiError::CollectionAlreadyOpen => V::InvalidInput(pb::Empty {}), - AnkiError::JSONError { info } => V::JsonError(info), - AnkiError::ProtoError { info } => V::ProtoError(info), - AnkiError::NotFound => V::NotFoundError(Empty {}), - AnkiError::Existing => V::Exists(Empty {}), - AnkiError::DeckIsFiltered => V::DeckIsFiltered(Empty {}), - AnkiError::SearchError(_) => V::InvalidInput(pb::Empty {}), - AnkiError::TemplateSaveError { .. } => V::TemplateParse(pb::Empty {}), - AnkiError::ParseNumError => V::InvalidInput(pb::Empty {}), - }; - - pb::BackendError { - value: Some(value), - localized, - } -} - -impl std::convert::From for i32 { - fn from(e: NetworkErrorKind) -> Self { - use pb::network_error::NetworkErrorKind as V; - (match e { - NetworkErrorKind::Offline => V::Offline, - NetworkErrorKind::Timeout => V::Timeout, - NetworkErrorKind::ProxyAuth => V::ProxyAuth, - NetworkErrorKind::Other => V::Other, - }) as i32 - } -} - -impl std::convert::From for i32 { - fn from(e: SyncErrorKind) -> Self { - use pb::sync_error::SyncErrorKind as V; - (match e { - SyncErrorKind::Conflict => V::Conflict, - SyncErrorKind::ServerError => V::ServerError, - SyncErrorKind::ClientTooOld => V::ClientTooOld, - SyncErrorKind::AuthFailed => V::AuthFailed, - SyncErrorKind::ServerMessage => V::ServerMessage, - SyncErrorKind::ResyncRequired => V::ResyncRequired, - SyncErrorKind::DatabaseCheckRequired => V::DatabaseCheckRequired, - SyncErrorKind::Other => V::Other, - SyncErrorKind::ClockIncorrect => V::ClockIncorrect, - SyncErrorKind::SyncNotStarted => V::SyncNotStarted, - }) as i32 - } -} - pub fn init_backend(init_msg: &[u8]) -> std::result::Result { let input: pb::BackendInit = match pb::BackendInit::decode(init_msg) { Ok(req) => req, @@ -220,135 +106,6 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result { Ok(Backend::new(i18n, input.server)) } -impl TryFrom for Node { - type Error = AnkiError; - - fn try_from(msg: pb::SearchNode) -> std::result::Result { - use pb::search_node::group::Joiner; - use pb::search_node::Filter; - use pb::search_node::Flag; - Ok(if let Some(filter) = msg.filter { - match filter { - Filter::Tag(s) => Node::Search(SearchNode::Tag(escape_anki_wildcards(&s))), - Filter::Deck(s) => Node::Search(SearchNode::Deck(if s == "*" { - s - } else { - escape_anki_wildcards(&s) - })), - Filter::Note(s) => Node::Search(SearchNode::NoteType(escape_anki_wildcards(&s))), - Filter::Template(u) => { - Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16))) - } - Filter::Nid(nid) => Node::Search(SearchNode::NoteIDs(nid.to_string())), - Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string())), - Filter::Dupe(dupe) => Node::Search(SearchNode::Duplicates { - note_type_id: dupe.notetype_id.into(), - text: dupe.first_field, - }), - Filter::FieldName(s) => Node::Search(SearchNode::SingleField { - field: escape_anki_wildcards(&s), - text: "*".to_string(), - is_re: false, - }), - Filter::Rated(rated) => Node::Search(SearchNode::Rated { - days: rated.days, - ease: rated.rating().into(), - }), - Filter::AddedInDays(u) => Node::Search(SearchNode::AddedInDays(u)), - Filter::DueInDays(i) => Node::Search(SearchNode::Property { - operator: "<=".to_string(), - kind: PropertyKind::Due(i), - }), - Filter::DueOnDay(i) => Node::Search(SearchNode::Property { - operator: "=".to_string(), - kind: PropertyKind::Due(i), - }), - Filter::EditedInDays(u) => Node::Search(SearchNode::EditedInDays(u)), - Filter::CardState(state) => Node::Search(SearchNode::State( - pb::search_node::CardState::from_i32(state) - .unwrap_or_default() - .into(), - )), - Filter::Flag(flag) => match Flag::from_i32(flag).unwrap_or(Flag::Any) { - Flag::None => Node::Search(SearchNode::Flag(0)), - Flag::Any => Node::Not(Box::new(Node::Search(SearchNode::Flag(0)))), - Flag::Red => Node::Search(SearchNode::Flag(1)), - Flag::Orange => Node::Search(SearchNode::Flag(2)), - Flag::Green => Node::Search(SearchNode::Flag(3)), - Flag::Blue => Node::Search(SearchNode::Flag(4)), - }, - Filter::Negated(term) => Node::try_from(*term)?.negated(), - Filter::Group(mut group) => { - match group.nodes.len() { - 0 => return Err(AnkiError::invalid_input("empty group")), - // a group of 1 doesn't need to be a group - 1 => group.nodes.pop().unwrap().try_into()?, - // 2+ nodes - _ => { - let joiner = match group.joiner() { - Joiner::And => Node::And, - Joiner::Or => Node::Or, - }; - let parsed: Vec<_> = group - .nodes - .into_iter() - .map(TryFrom::try_from) - .collect::>()?; - let joined = parsed.into_iter().intersperse(joiner).collect(); - Node::Group(joined) - } - } - } - Filter::ParsableText(text) => { - let mut nodes = parse_search(&text)?; - if nodes.len() == 1 { - nodes.pop().unwrap() - } else { - Node::Group(nodes) - } - } - } - } else { - Node::Search(SearchNode::WholeCollection) - }) - } -} - -impl From for BoolSeparator { - fn from(sep: pb::search_node::group::Joiner) -> Self { - match sep { - pb::search_node::group::Joiner::And => BoolSeparator::And, - pb::search_node::group::Joiner::Or => BoolSeparator::Or, - } - } -} - -impl From for RatingKind { - fn from(r: pb::search_node::Rating) -> Self { - match r { - pb::search_node::Rating::Again => RatingKind::AnswerButton(1), - pb::search_node::Rating::Hard => RatingKind::AnswerButton(2), - pb::search_node::Rating::Good => RatingKind::AnswerButton(3), - pb::search_node::Rating::Easy => RatingKind::AnswerButton(4), - pb::search_node::Rating::Any => RatingKind::AnyAnswerButton, - pb::search_node::Rating::ByReschedule => RatingKind::ManualReschedule, - } - } -} - -impl From for StateKind { - fn from(k: pb::search_node::CardState) -> Self { - match k { - pb::search_node::CardState::New => StateKind::New, - pb::search_node::CardState::Learn => StateKind::Learning, - pb::search_node::CardState::Review => StateKind::Review, - pb::search_node::CardState::Due => StateKind::Due, - pb::search_node::CardState::Suspended => StateKind::Suspended, - pb::search_node::CardState::Buried => StateKind::Buried, - } - } -} - impl BackendService for Backend { fn latest_progress(&self, _input: Empty) -> BackendResult { let progress = self.progress_state.lock().unwrap().last_progress; @@ -698,6 +455,13 @@ impl BackendService for Backend { .map(Into::into) } + fn get_queued_cards( + &self, + input: pb::GetQueuedCardsIn, + ) -> BackendResult { + self.with_col(|col| col.get_queued_cards(input.fetch_limit, input.intraday_learning_only)) + } + // statistics //----------------------------------------------- @@ -806,7 +570,7 @@ impl BackendService for Backend { if input.preserve_usn_and_mtime { col.transact(None, |col| { let usn = col.usn()?; - col.add_or_update_single_deck(&mut deck, usn) + col.add_or_update_single_deck_with_existing_id(&mut deck, usn) })?; } else { col.add_or_update_deck(&mut deck)?; @@ -977,26 +741,19 @@ impl BackendService for Backend { }) } - fn update_card(&self, input: pb::Card) -> BackendResult { - let mut card = pbcard_to_native(input)?; + fn update_card(&self, input: pb::UpdateCardIn) -> BackendResult { self.with_col(|col| { - col.transact(None, |ctx| { - let orig = ctx - .storage - .get_card(card.id)? - .ok_or_else(|| AnkiError::invalid_input("missing card"))?; - ctx.update_card(&mut card, &orig, ctx.usn()?) - }) + let op = if input.skip_undo_entry { + None + } else { + Some(UndoableOpKind::UpdateCard) + }; + let mut card: Card = input.card.ok_or(AnkiError::NotFound)?.try_into()?; + col.update_card_with_op(&mut card, op) }) .map(Into::into) } - fn add_card(&self, input: pb::Card) -> BackendResult { - let mut card = pbcard_to_native(input)?; - self.with_col(|col| col.transact(None, |ctx| ctx.add_card(&mut card)))?; - Ok(pb::CardId { cid: card.id.0 }) - } - fn remove_cards(&self, input: pb::RemoveCardsIn) -> BackendResult { self.with_col(|col| { col.transact(None, |col| { @@ -1036,10 +793,34 @@ impl BackendService for Backend { }) } - fn update_note(&self, input: pb::Note) -> BackendResult { + fn defaults_for_adding( + &self, + input: pb::DefaultsForAddingIn, + ) -> BackendResult { self.with_col(|col| { - let mut note: Note = input.into(); - col.update_note(&mut note) + let home_deck: DeckID = input.home_deck_of_current_review_card.into(); + col.defaults_for_adding(home_deck).map(Into::into) + }) + } + + fn default_deck_for_notetype(&self, input: pb::NoteTypeId) -> BackendResult { + self.with_col(|col| { + Ok(col + .default_deck_for_notetype(input.into())? + .unwrap_or(DeckID(0)) + .into()) + }) + } + + fn update_note(&self, input: pb::UpdateNoteIn) -> BackendResult { + self.with_col(|col| { + let op = if input.skip_undo_entry { + None + } else { + Some(UndoableOpKind::UpdateNote) + }; + let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into(); + col.update_note_with_op(&mut note, op) }) .map(Into::into) } @@ -1080,7 +861,7 @@ impl BackendService for Backend { fn add_note_tags(&self, input: pb::AddNoteTagsIn) -> BackendResult { self.with_col(|col| { - col.add_tags_for_notes(&to_nids(input.nids), &input.tags) + col.add_tags_to_notes(&to_nids(input.nids), &input.tags) .map(|n| n as u32) }) .map(Into::into) @@ -1301,6 +1082,24 @@ impl BackendService for Backend { }) } + fn get_undo_status(&self, _input: pb::Empty) -> Result { + self.with_col(|col| Ok(col.undo_status())) + } + + fn undo(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + col.undo()?; + Ok(col.undo_status()) + }) + } + + fn redo(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + col.redo()?; + Ok(col.undo_status()) + }) + } + // sync //------------------------------------------------------------------- @@ -1437,7 +1236,7 @@ impl BackendService for Backend { fn clear_tag(&self, tag: pb::String) -> BackendResult { self.with_col(|col| { col.transact(None, |col| { - col.storage.clear_tag(tag.val.as_str())?; + col.storage.clear_tag_and_children(tag.val.as_str())?; Ok(().into()) }) }) @@ -1497,27 +1296,29 @@ impl BackendService for Backend { fn get_config_bool(&self, input: pb::config::Bool) -> BackendResult { self.with_col(|col| { Ok(pb::Bool { - val: col.get_bool(input), + val: col.get_bool(input.key().into()), }) }) } fn set_config_bool(&self, input: pb::SetConfigBoolIn) -> BackendResult { - self.with_col(|col| col.transact(None, |col| col.set_bool(input))) + self.with_col(|col| col.transact(None, |col| col.set_bool(input.key().into(), input.value))) .map(Into::into) } fn get_config_string(&self, input: pb::config::String) -> BackendResult { self.with_col(|col| { Ok(pb::String { - val: col.get_string(input), + val: col.get_string(input.key().into()), }) }) } fn set_config_string(&self, input: pb::SetConfigStringIn) -> BackendResult { - self.with_col(|col| col.transact(None, |col| col.set_string(input))) - .map(Into::into) + self.with_col(|col| { + col.transact(None, |col| col.set_string(input.key().into(), &input.value)) + }) + .map(Into::into) } fn get_preferences(&self, _input: Empty) -> BackendResult { @@ -1575,18 +1376,6 @@ impl Backend { ) } - fn new_progress_handler(&self) -> ThrottlingProgressHandler { - { - let mut guard = self.progress_state.lock().unwrap(); - guard.want_abort = false; - guard.last_progress = None; - } - ThrottlingProgressHandler { - state: Arc::clone(&self.progress_state), - last_update: coarsetime::Instant::now(), - } - } - fn runtime_handle(&self) -> runtime::Handle { self.runtime .get_or_init(|| { @@ -1601,245 +1390,8 @@ impl Backend { .clone() } - fn sync_abort_handle( - &self, - ) -> BackendResult<( - scopeguard::ScopeGuard, - AbortRegistration, - )> { - let (abort_handle, abort_reg) = AbortHandle::new_pair(); - - // Register the new abort_handle. - let old_handle = self.sync_abort.lock().unwrap().replace(abort_handle); - if old_handle.is_some() { - // NOTE: In the future we would ideally be able to handle multiple - // abort handles by just iterating over them all in - // abort_sync). But for now, just log a warning if there was - // already one present -- but don't abort it either. - let log = self.with_col(|col| Ok(col.log.clone()))?; - warn!( - log, - "new sync_abort handle registered, but old one was still present (old sync job might not be cancelled on abort)" - ); - } - // Clear the abort handle after the caller is done and drops the guard. - let guard = scopeguard::guard(Arc::clone(&self.sync_abort), |sync_abort| { - sync_abort.lock().unwrap().take(); - }); - Ok((guard, abort_reg)) - } - - fn sync_media_inner(&self, input: pb::SyncAuth) -> Result<()> { - // mark media sync as active - let (abort_handle, abort_reg) = AbortHandle::new_pair(); - { - let mut guard = self.state.lock().unwrap(); - if guard.media_sync_abort.is_some() { - // media sync is already active - return Ok(()); - } else { - guard.media_sync_abort = Some(abort_handle); - } - } - - // get required info from collection - let mut guard = self.col.lock().unwrap(); - let col = guard.as_mut().unwrap(); - let folder = col.media_folder.clone(); - let db = col.media_db.clone(); - let log = col.log.clone(); - drop(guard); - - // start the sync - let mut handler = self.new_progress_handler(); - let progress_fn = move |progress| handler.update(progress, true); - - let mgr = MediaManager::new(&folder, &db)?; - let rt = self.runtime_handle(); - let sync_fut = mgr.sync_media(progress_fn, input.host_number, &input.hkey, log); - let abortable_sync = Abortable::new(sync_fut, abort_reg); - let result = rt.block_on(abortable_sync); - - // mark inactive - self.state.lock().unwrap().media_sync_abort.take(); - - // return result - match result { - Ok(sync_result) => sync_result, - Err(_) => { - // aborted sync - Err(AnkiError::Interrupted) - } - } - } - - /// Abort the media sync. Won't return until aborted. - fn abort_media_sync_and_wait(&self) { - let guard = self.state.lock().unwrap(); - if let Some(handle) = &guard.media_sync_abort { - handle.abort(); - self.progress_state.lock().unwrap().want_abort = true; - } - drop(guard); - - // block until it aborts - while self.state.lock().unwrap().media_sync_abort.is_some() { - std::thread::sleep(std::time::Duration::from_millis(100)); - self.progress_state.lock().unwrap().want_abort = true; - } - } - - fn sync_login_inner(&self, input: pb::SyncLoginIn) -> BackendResult { - let (_guard, abort_reg) = self.sync_abort_handle()?; - - let rt = self.runtime_handle(); - let sync_fut = sync_login(&input.username, &input.password); - let abortable_sync = Abortable::new(sync_fut, abort_reg); - let ret = match rt.block_on(abortable_sync) { - Ok(sync_result) => sync_result, - Err(_) => Err(AnkiError::Interrupted), - }; - ret.map(|a| pb::SyncAuth { - hkey: a.hkey, - host_number: a.host_number, - }) - } - - fn sync_status_inner(&self, input: pb::SyncAuth) -> BackendResult { - // any local changes mean we can skip the network round-trip - let req = self.with_col(|col| col.get_local_sync_status())?; - if req != pb::sync_status_out::Required::NoChanges { - return Ok(req.into()); - } - - // return cached server response if only a short time has elapsed - { - let guard = self.state.lock().unwrap(); - if guard.remote_sync_status.last_check.elapsed_secs() < 300 { - return Ok(guard.remote_sync_status.last_response.into()); - } - } - - // fetch and cache result - let rt = self.runtime_handle(); - let time_at_check_begin = TimestampSecs::now(); - let remote: SyncMeta = rt.block_on(get_remote_sync_meta(input.into()))?; - let response = self.with_col(|col| col.get_sync_status(remote).map(Into::into))?; - - { - let mut guard = self.state.lock().unwrap(); - // On startup, the sync status check will block on network access, and then automatic syncing begins, - // taking hold of the mutex. By the time we reach here, our network status may be out of date, - // so we discard it if stale. - if guard.remote_sync_status.last_check < time_at_check_begin { - guard.remote_sync_status.last_check = time_at_check_begin; - guard.remote_sync_status.last_response = response; - } - } - - Ok(response.into()) - } - - fn sync_collection_inner(&self, input: pb::SyncAuth) -> BackendResult { - let (_guard, abort_reg) = self.sync_abort_handle()?; - - let rt = self.runtime_handle(); - let input_copy = input.clone(); - - let ret = self.with_col(|col| { - let mut handler = self.new_progress_handler(); - let progress_fn = move |progress: NormalSyncProgress, throttle: bool| { - handler.update(progress, throttle); - }; - - let sync_fut = col.normal_sync(input.into(), progress_fn); - let abortable_sync = Abortable::new(sync_fut, abort_reg); - - match rt.block_on(abortable_sync) { - Ok(sync_result) => sync_result, - Err(_) => { - // if the user aborted, we'll need to clean up the transaction - col.storage.rollback_trx()?; - // and tell AnkiWeb to clean up - let _handle = std::thread::spawn(move || { - let _ = rt.block_on(sync_abort(input_copy.hkey, input_copy.host_number)); - }); - - Err(AnkiError::Interrupted) - } - } - }); - - let output: SyncOutput = ret?; - self.state - .lock() - .unwrap() - .remote_sync_status - .update(output.required.into()); - Ok(output.into()) - } - - fn full_sync_inner(&self, input: pb::SyncAuth, upload: bool) -> Result<()> { - self.abort_media_sync_and_wait(); - - let rt = self.runtime_handle(); - - let mut col = self.col.lock().unwrap(); - if col.is_none() { - return Err(AnkiError::CollectionNotOpen); - } - - let col_inner = col.take().unwrap(); - - let (_guard, abort_reg) = self.sync_abort_handle()?; - - let col_path = col_inner.col_path.clone(); - let media_folder_path = col_inner.media_folder.clone(); - let media_db_path = col_inner.media_db.clone(); - let logger = col_inner.log.clone(); - - let mut handler = self.new_progress_handler(); - let progress_fn = move |progress: FullSyncProgress, throttle: bool| { - handler.update(progress, throttle); - }; - - let result = if upload { - let sync_fut = col_inner.full_upload(input.into(), Box::new(progress_fn)); - let abortable_sync = Abortable::new(sync_fut, abort_reg); - rt.block_on(abortable_sync) - } else { - let sync_fut = col_inner.full_download(input.into(), Box::new(progress_fn)); - let abortable_sync = Abortable::new(sync_fut, abort_reg); - rt.block_on(abortable_sync) - }; - - // ensure re-opened regardless of outcome - col.replace(open_collection( - col_path, - media_folder_path, - media_db_path, - self.server, - self.i18n.clone(), - logger, - )?); - - match result { - Ok(sync_result) => { - if sync_result.is_ok() { - self.state - .lock() - .unwrap() - .remote_sync_status - .update(sync_status_out::Required::NoChanges); - } - sync_result - } - Err(_) => Err(AnkiError::Interrupted), - } - } - pub fn db_command(&self, input: &[u8]) -> Result> { - self.with_col(|col| db_command_bytes(&col.storage, input)) + self.with_col(|col| db_command_bytes(col, input)) } pub fn run_db_command_bytes(&self, input: &[u8]) -> std::result::Result, Vec> { @@ -1900,120 +1452,6 @@ impl From for pb::RenderCardOut { } } -fn progress_to_proto(progress: Option, i18n: &I18n) -> pb::Progress { - let progress = if let Some(progress) = progress { - match progress { - Progress::MediaSync(p) => pb::progress::Value::MediaSync(media_sync_progress(p, i18n)), - Progress::MediaCheck(n) => { - let s = i18n.trn(TR::MediaCheckChecked, tr_args!["count"=>n]); - pb::progress::Value::MediaCheck(s) - } - Progress::FullSync(p) => pb::progress::Value::FullSync(pb::progress::FullSync { - transferred: p.transferred_bytes as u32, - total: p.total_bytes as u32, - }), - Progress::NormalSync(p) => { - let stage = match p.stage { - SyncStage::Connecting => i18n.tr(TR::SyncSyncing), - SyncStage::Syncing => i18n.tr(TR::SyncSyncing), - SyncStage::Finalizing => i18n.tr(TR::SyncChecking), - } - .to_string(); - let added = i18n.trn( - TR::SyncAddedUpdatedCount, - tr_args![ - "up"=>p.local_update, "down"=>p.remote_update], - ); - let removed = i18n.trn( - TR::SyncMediaRemovedCount, - tr_args![ - "up"=>p.local_remove, "down"=>p.remote_remove], - ); - pb::progress::Value::NormalSync(pb::progress::NormalSync { - stage, - added, - removed, - }) - } - Progress::DatabaseCheck(p) => { - let mut stage_total = 0; - let mut stage_current = 0; - let stage = match p { - DatabaseCheckProgress::Integrity => i18n.tr(TR::DatabaseCheckCheckingIntegrity), - DatabaseCheckProgress::Optimize => i18n.tr(TR::DatabaseCheckRebuilding), - DatabaseCheckProgress::Cards => i18n.tr(TR::DatabaseCheckCheckingCards), - DatabaseCheckProgress::Notes { current, total } => { - stage_total = total; - stage_current = current; - i18n.tr(TR::DatabaseCheckCheckingNotes) - } - DatabaseCheckProgress::History => i18n.tr(TR::DatabaseCheckCheckingHistory), - } - .to_string(); - pb::progress::Value::DatabaseCheck(pb::progress::DatabaseCheck { - stage, - stage_current, - stage_total, - }) - } - } - } else { - pb::progress::Value::None(pb::Empty {}) - }; - pb::Progress { - value: Some(progress), - } -} - -fn media_sync_progress(p: MediaSyncProgress, i18n: &I18n) -> pb::progress::MediaSync { - pb::progress::MediaSync { - checked: i18n.trn(TR::SyncMediaCheckedCount, tr_args!["count"=>p.checked]), - added: i18n.trn( - TR::SyncMediaAddedCount, - tr_args!["up"=>p.uploaded_files,"down"=>p.downloaded_files], - ), - removed: i18n.trn( - TR::SyncMediaRemovedCount, - tr_args!["up"=>p.uploaded_deletions,"down"=>p.downloaded_deletions], - ), - } -} - -impl From for SortKind { - fn from(kind: SortKindProto) -> Self { - match kind { - SortKindProto::NoteCreation => SortKind::NoteCreation, - SortKindProto::NoteMod => SortKind::NoteMod, - SortKindProto::NoteField => SortKind::NoteField, - SortKindProto::NoteTags => SortKind::NoteTags, - SortKindProto::NoteType => SortKind::NoteType, - SortKindProto::CardMod => SortKind::CardMod, - SortKindProto::CardReps => SortKind::CardReps, - SortKindProto::CardDue => SortKind::CardDue, - SortKindProto::CardEase => SortKind::CardEase, - SortKindProto::CardLapses => SortKind::CardLapses, - SortKindProto::CardInterval => SortKind::CardInterval, - SortKindProto::CardDeck => SortKind::CardDeck, - SortKindProto::CardTemplate => SortKind::CardTemplate, - } - } -} - -impl From> for SortMode { - fn from(order: Option) -> Self { - use pb::sort_order::Value as V; - match order.unwrap_or(V::FromConfig(pb::Empty {})) { - V::None(_) => SortMode::NoOrder, - V::Custom(s) => SortMode::Custom(s), - V::FromConfig(_) => SortMode::FromConfig, - V::Builtin(b) => SortMode::Builtin { - kind: b.kind().into(), - reverse: b.reverse, - }, - } - } -} - impl From for pb::Card { fn from(c: Card) -> Self { pb::Card { @@ -2039,104 +1477,11 @@ impl From for pb::Card { } } -fn pbcard_to_native(c: pb::Card) -> Result { - let ctype = CardType::try_from(c.ctype as u8) - .map_err(|_| AnkiError::invalid_input("invalid card type"))?; - let queue = CardQueue::try_from(c.queue as i8) - .map_err(|_| AnkiError::invalid_input("invalid card queue"))?; - Ok(Card { - id: CardID(c.id), - note_id: NoteID(c.note_id), - deck_id: DeckID(c.deck_id), - template_idx: c.template_idx as u16, - mtime: TimestampSecs(c.mtime_secs), - usn: Usn(c.usn), - ctype, - queue, - due: c.due, - interval: c.interval, - ease_factor: c.ease_factor as u16, - reps: c.reps, - lapses: c.lapses, - remaining_steps: c.remaining_steps, - original_due: c.original_due, - original_deck_id: DeckID(c.original_deck_id), - flags: c.flags as u8, - data: c.data, - }) -} - -impl From for pb::SchedTimingTodayOut { - fn from(t: crate::scheduler::cutoff::SchedTimingToday) -> pb::SchedTimingTodayOut { +impl From for pb::SchedTimingTodayOut { + fn from(t: crate::scheduler::timing::SchedTimingToday) -> pb::SchedTimingTodayOut { pb::SchedTimingTodayOut { days_elapsed: t.days_elapsed, next_day_at: t.next_day_at, } } } - -impl From for pb::SyncCollectionOut { - fn from(o: SyncOutput) -> Self { - pb::SyncCollectionOut { - host_number: o.host_number, - server_message: o.server_message, - required: match o.required { - SyncActionRequired::NoChanges => { - pb::sync_collection_out::ChangesRequired::NoChanges as i32 - } - SyncActionRequired::FullSyncRequired { - upload_ok, - download_ok, - } => { - if !upload_ok { - pb::sync_collection_out::ChangesRequired::FullDownload as i32 - } else if !download_ok { - pb::sync_collection_out::ChangesRequired::FullUpload as i32 - } else { - pb::sync_collection_out::ChangesRequired::FullSync as i32 - } - } - SyncActionRequired::NormalSyncRequired => { - pb::sync_collection_out::ChangesRequired::NormalSync as i32 - } - }, - } - } -} - -impl From for SyncAuth { - fn from(a: pb::SyncAuth) -> Self { - SyncAuth { - hkey: a.hkey, - host_number: a.host_number, - } - } -} - -impl From for Progress { - fn from(p: FullSyncProgress) -> Self { - Progress::FullSync(p) - } -} - -impl From for Progress { - fn from(p: MediaSyncProgress) -> Self { - Progress::MediaSync(p) - } -} - -impl From for Progress { - fn from(p: NormalSyncProgress) -> Self { - Progress::NormalSync(p) - } -} - -impl pb::search_node::IdList { - fn into_id_string(self) -> String { - self.ids - .iter() - .map(|i| i.to_string()) - .collect::>() - .join(",") - } -} diff --git a/rslib/src/backend/progress.rs b/rslib/src/backend/progress.rs new file mode 100644 index 000000000..557915cb6 --- /dev/null +++ b/rslib/src/backend/progress.rs @@ -0,0 +1,164 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use futures::future::AbortHandle; +use std::sync::{Arc, Mutex}; + +use crate::{ + backend_proto as pb, + dbcheck::DatabaseCheckProgress, + i18n::{tr_args, I18n, TR}, + media::sync::MediaSyncProgress, + sync::{FullSyncProgress, NormalSyncProgress, SyncStage}, +}; + +use super::Backend; + +pub(super) struct ThrottlingProgressHandler { + pub state: Arc>, + pub last_update: coarsetime::Instant, +} + +impl ThrottlingProgressHandler { + /// Returns true if should continue. + pub(super) fn update(&mut self, progress: impl Into, throttle: bool) -> bool { + let now = coarsetime::Instant::now(); + if throttle && now.duration_since(self.last_update).as_f64() < 0.1 { + return true; + } + self.last_update = now; + let mut guard = self.state.lock().unwrap(); + guard.last_progress.replace(progress.into()); + let want_abort = guard.want_abort; + guard.want_abort = false; + !want_abort + } +} + +pub(super) struct ProgressState { + pub want_abort: bool, + pub last_progress: Option, +} + +// fixme: this should support multiple abort handles. +pub(super) type AbortHandleSlot = Arc>>; + +#[derive(Clone, Copy)] +pub(super) enum Progress { + MediaSync(MediaSyncProgress), + MediaCheck(u32), + FullSync(FullSyncProgress), + NormalSync(NormalSyncProgress), + DatabaseCheck(DatabaseCheckProgress), +} + +pub(super) fn progress_to_proto(progress: Option, i18n: &I18n) -> pb::Progress { + let progress = if let Some(progress) = progress { + match progress { + Progress::MediaSync(p) => pb::progress::Value::MediaSync(media_sync_progress(p, i18n)), + Progress::MediaCheck(n) => { + let s = i18n.trn(TR::MediaCheckChecked, tr_args!["count"=>n]); + pb::progress::Value::MediaCheck(s) + } + Progress::FullSync(p) => pb::progress::Value::FullSync(pb::progress::FullSync { + transferred: p.transferred_bytes as u32, + total: p.total_bytes as u32, + }), + Progress::NormalSync(p) => { + let stage = match p.stage { + SyncStage::Connecting => i18n.tr(TR::SyncSyncing), + SyncStage::Syncing => i18n.tr(TR::SyncSyncing), + SyncStage::Finalizing => i18n.tr(TR::SyncChecking), + } + .to_string(); + let added = i18n.trn( + TR::SyncAddedUpdatedCount, + tr_args![ + "up"=>p.local_update, "down"=>p.remote_update], + ); + let removed = i18n.trn( + TR::SyncMediaRemovedCount, + tr_args![ + "up"=>p.local_remove, "down"=>p.remote_remove], + ); + pb::progress::Value::NormalSync(pb::progress::NormalSync { + stage, + added, + removed, + }) + } + Progress::DatabaseCheck(p) => { + let mut stage_total = 0; + let mut stage_current = 0; + let stage = match p { + DatabaseCheckProgress::Integrity => i18n.tr(TR::DatabaseCheckCheckingIntegrity), + DatabaseCheckProgress::Optimize => i18n.tr(TR::DatabaseCheckRebuilding), + DatabaseCheckProgress::Cards => i18n.tr(TR::DatabaseCheckCheckingCards), + DatabaseCheckProgress::Notes { current, total } => { + stage_total = total; + stage_current = current; + i18n.tr(TR::DatabaseCheckCheckingNotes) + } + DatabaseCheckProgress::History => i18n.tr(TR::DatabaseCheckCheckingHistory), + } + .to_string(); + pb::progress::Value::DatabaseCheck(pb::progress::DatabaseCheck { + stage, + stage_current, + stage_total, + }) + } + } + } else { + pb::progress::Value::None(pb::Empty {}) + }; + pb::Progress { + value: Some(progress), + } +} + +fn media_sync_progress(p: MediaSyncProgress, i18n: &I18n) -> pb::progress::MediaSync { + pb::progress::MediaSync { + checked: i18n.trn(TR::SyncMediaCheckedCount, tr_args!["count"=>p.checked]), + added: i18n.trn( + TR::SyncMediaAddedCount, + tr_args!["up"=>p.uploaded_files,"down"=>p.downloaded_files], + ), + removed: i18n.trn( + TR::SyncMediaRemovedCount, + tr_args!["up"=>p.uploaded_deletions,"down"=>p.downloaded_deletions], + ), + } +} + +impl From for Progress { + fn from(p: FullSyncProgress) -> Self { + Progress::FullSync(p) + } +} + +impl From for Progress { + fn from(p: MediaSyncProgress) -> Self { + Progress::MediaSync(p) + } +} + +impl From for Progress { + fn from(p: NormalSyncProgress) -> Self { + Progress::NormalSync(p) + } +} + +impl Backend { + pub(super) fn new_progress_handler(&self) -> ThrottlingProgressHandler { + { + let mut guard = self.progress_state.lock().unwrap(); + guard.want_abort = false; + guard.last_progress = None; + } + ThrottlingProgressHandler { + state: Arc::clone(&self.progress_state), + last_update: coarsetime::Instant::now(), + } + } +} diff --git a/rslib/src/backend/scheduler/answering.rs b/rslib/src/backend/scheduler/answering.rs index 8d6942bf0..d8f15a900 100644 --- a/rslib/src/backend/scheduler/answering.rs +++ b/rslib/src/backend/scheduler/answering.rs @@ -4,7 +4,10 @@ use crate::{ backend_proto as pb, prelude::*, - scheduler::answering::{CardAnswer, Rating}, + scheduler::{ + answering::{CardAnswer, Rating}, + queue::{QueuedCard, QueuedCards}, + }, }; impl From for CardAnswer { @@ -30,3 +33,24 @@ impl From for Rating { } } } + +impl From for pb::get_queued_cards_out::QueuedCard { + fn from(queued_card: QueuedCard) -> Self { + Self { + card: Some(queued_card.card.into()), + next_states: Some(queued_card.next_states.into()), + queue: queued_card.kind as i32, + } + } +} + +impl From for pb::get_queued_cards_out::QueuedCards { + fn from(queued_cards: QueuedCards) -> Self { + Self { + cards: queued_cards.cards.into_iter().map(Into::into).collect(), + new_count: queued_cards.new_count as u32, + learning_count: queued_cards.learning_count as u32, + review_count: queued_cards.review_count as u32, + } + } +} diff --git a/rslib/src/backend/scheduler/states/preview.rs b/rslib/src/backend/scheduler/states/preview.rs index c09876820..24d3c2877 100644 --- a/rslib/src/backend/scheduler/states/preview.rs +++ b/rslib/src/backend/scheduler/states/preview.rs @@ -7,7 +7,7 @@ impl From for PreviewState { fn from(state: pb::scheduling_state::Preview) -> Self { PreviewState { scheduled_secs: state.scheduled_secs, - original_state: state.original_state.unwrap_or_default().into(), + finished: state.finished, } } } @@ -16,7 +16,7 @@ impl From for pb::scheduling_state::Preview { fn from(state: PreviewState) -> Self { pb::scheduling_state::Preview { scheduled_secs: state.scheduled_secs, - original_state: Some(state.original_state.into()), + finished: state.finished, } } } diff --git a/rslib/src/backend/search.rs b/rslib/src/backend/search.rs new file mode 100644 index 000000000..d0b134c2a --- /dev/null +++ b/rslib/src/backend/search.rs @@ -0,0 +1,193 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use itertools::Itertools; +use std::convert::{TryFrom, TryInto}; + +use crate::{ + backend_proto as pb, + backend_proto::{ + sort_order::builtin::Kind as SortKindProto, sort_order::Value as SortOrderProto, + }, + config::SortKind, + prelude::*, + search::{ + parse_search, BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, + StateKind, TemplateKind, + }, + text::escape_anki_wildcards, +}; + +impl TryFrom for Node { + type Error = AnkiError; + + fn try_from(msg: pb::SearchNode) -> std::result::Result { + use pb::search_node::group::Joiner; + use pb::search_node::Filter; + use pb::search_node::Flag; + Ok(if let Some(filter) = msg.filter { + match filter { + Filter::Tag(s) => Node::Search(SearchNode::Tag(escape_anki_wildcards(&s))), + Filter::Deck(s) => Node::Search(SearchNode::Deck(if s == "*" { + s + } else { + escape_anki_wildcards(&s) + })), + Filter::Note(s) => Node::Search(SearchNode::NoteType(escape_anki_wildcards(&s))), + Filter::Template(u) => { + Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16))) + } + Filter::Nid(nid) => Node::Search(SearchNode::NoteIDs(nid.to_string())), + Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string())), + Filter::Dupe(dupe) => Node::Search(SearchNode::Duplicates { + note_type_id: dupe.notetype_id.into(), + text: dupe.first_field, + }), + Filter::FieldName(s) => Node::Search(SearchNode::SingleField { + field: escape_anki_wildcards(&s), + text: "*".to_string(), + is_re: false, + }), + Filter::Rated(rated) => Node::Search(SearchNode::Rated { + days: rated.days, + ease: rated.rating().into(), + }), + Filter::AddedInDays(u) => Node::Search(SearchNode::AddedInDays(u)), + Filter::DueInDays(i) => Node::Search(SearchNode::Property { + operator: "<=".to_string(), + kind: PropertyKind::Due(i), + }), + Filter::DueOnDay(i) => Node::Search(SearchNode::Property { + operator: "=".to_string(), + kind: PropertyKind::Due(i), + }), + Filter::EditedInDays(u) => Node::Search(SearchNode::EditedInDays(u)), + Filter::CardState(state) => Node::Search(SearchNode::State( + pb::search_node::CardState::from_i32(state) + .unwrap_or_default() + .into(), + )), + Filter::Flag(flag) => match Flag::from_i32(flag).unwrap_or(Flag::Any) { + Flag::None => Node::Search(SearchNode::Flag(0)), + Flag::Any => Node::Not(Box::new(Node::Search(SearchNode::Flag(0)))), + Flag::Red => Node::Search(SearchNode::Flag(1)), + Flag::Orange => Node::Search(SearchNode::Flag(2)), + Flag::Green => Node::Search(SearchNode::Flag(3)), + Flag::Blue => Node::Search(SearchNode::Flag(4)), + }, + Filter::Negated(term) => Node::try_from(*term)?.negated(), + Filter::Group(mut group) => { + match group.nodes.len() { + 0 => return Err(AnkiError::invalid_input("empty group")), + // a group of 1 doesn't need to be a group + 1 => group.nodes.pop().unwrap().try_into()?, + // 2+ nodes + _ => { + let joiner = match group.joiner() { + Joiner::And => Node::And, + Joiner::Or => Node::Or, + }; + let parsed: Vec<_> = group + .nodes + .into_iter() + .map(TryFrom::try_from) + .collect::>()?; + let joined = parsed.into_iter().intersperse(joiner).collect(); + Node::Group(joined) + } + } + } + Filter::ParsableText(text) => { + let mut nodes = parse_search(&text)?; + if nodes.len() == 1 { + nodes.pop().unwrap() + } else { + Node::Group(nodes) + } + } + } + } else { + Node::Search(SearchNode::WholeCollection) + }) + } +} + +impl From for BoolSeparator { + fn from(sep: pb::search_node::group::Joiner) -> Self { + match sep { + pb::search_node::group::Joiner::And => BoolSeparator::And, + pb::search_node::group::Joiner::Or => BoolSeparator::Or, + } + } +} + +impl From for RatingKind { + fn from(r: pb::search_node::Rating) -> Self { + match r { + pb::search_node::Rating::Again => RatingKind::AnswerButton(1), + pb::search_node::Rating::Hard => RatingKind::AnswerButton(2), + pb::search_node::Rating::Good => RatingKind::AnswerButton(3), + pb::search_node::Rating::Easy => RatingKind::AnswerButton(4), + pb::search_node::Rating::Any => RatingKind::AnyAnswerButton, + pb::search_node::Rating::ByReschedule => RatingKind::ManualReschedule, + } + } +} + +impl From for StateKind { + fn from(k: pb::search_node::CardState) -> Self { + match k { + pb::search_node::CardState::New => StateKind::New, + pb::search_node::CardState::Learn => StateKind::Learning, + pb::search_node::CardState::Review => StateKind::Review, + pb::search_node::CardState::Due => StateKind::Due, + pb::search_node::CardState::Suspended => StateKind::Suspended, + pb::search_node::CardState::Buried => StateKind::Buried, + } + } +} + +impl pb::search_node::IdList { + fn into_id_string(self) -> String { + self.ids + .iter() + .map(|i| i.to_string()) + .collect::>() + .join(",") + } +} + +impl From for SortKind { + fn from(kind: SortKindProto) -> Self { + match kind { + SortKindProto::NoteCreation => SortKind::NoteCreation, + SortKindProto::NoteMod => SortKind::NoteMod, + SortKindProto::NoteField => SortKind::NoteField, + SortKindProto::NoteTags => SortKind::NoteTags, + SortKindProto::NoteType => SortKind::NoteType, + SortKindProto::CardMod => SortKind::CardMod, + SortKindProto::CardReps => SortKind::CardReps, + SortKindProto::CardDue => SortKind::CardDue, + SortKindProto::CardEase => SortKind::CardEase, + SortKindProto::CardLapses => SortKind::CardLapses, + SortKindProto::CardInterval => SortKind::CardInterval, + SortKindProto::CardDeck => SortKind::CardDeck, + SortKindProto::CardTemplate => SortKind::CardTemplate, + } + } +} + +impl From> for SortMode { + fn from(order: Option) -> Self { + use pb::sort_order::Value as V; + match order.unwrap_or(V::FromConfig(pb::Empty {})) { + V::None(_) => SortMode::NoOrder, + V::Custom(s) => SortMode::Custom(s), + V::FromConfig(_) => SortMode::FromConfig, + V::Builtin(b) => SortMode::Builtin { + kind: b.kind().into(), + reverse: b.reverse, + }, + } + } +} diff --git a/rslib/src/backend/sync.rs b/rslib/src/backend/sync.rs new file mode 100644 index 000000000..17c1fd5b0 --- /dev/null +++ b/rslib/src/backend/sync.rs @@ -0,0 +1,313 @@ +// 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 futures::future::{AbortHandle, AbortRegistration, Abortable}; +use slog::warn; + +use crate::{ + backend_proto as pb, + collection::open_collection, + media::MediaManager, + prelude::*, + sync::{ + get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress, + SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, + }, +}; + +use super::{progress::AbortHandleSlot, Backend}; + +#[derive(Default, Debug)] +pub(super) struct RemoteSyncStatus { + pub last_check: TimestampSecs, + pub last_response: pb::sync_status_out::Required, +} + +impl RemoteSyncStatus { + pub(super) fn update(&mut self, required: pb::sync_status_out::Required) { + self.last_check = TimestampSecs::now(); + self.last_response = required + } +} + +impl From for pb::SyncCollectionOut { + fn from(o: SyncOutput) -> Self { + pb::SyncCollectionOut { + host_number: o.host_number, + server_message: o.server_message, + required: match o.required { + SyncActionRequired::NoChanges => { + pb::sync_collection_out::ChangesRequired::NoChanges as i32 + } + SyncActionRequired::FullSyncRequired { + upload_ok, + download_ok, + } => { + if !upload_ok { + pb::sync_collection_out::ChangesRequired::FullDownload as i32 + } else if !download_ok { + pb::sync_collection_out::ChangesRequired::FullUpload as i32 + } else { + pb::sync_collection_out::ChangesRequired::FullSync as i32 + } + } + SyncActionRequired::NormalSyncRequired => { + pb::sync_collection_out::ChangesRequired::NormalSync as i32 + } + }, + } + } +} + +impl From for SyncAuth { + fn from(a: pb::SyncAuth) -> Self { + SyncAuth { + hkey: a.hkey, + host_number: a.host_number, + } + } +} + +impl Backend { + fn sync_abort_handle( + &self, + ) -> Result<( + scopeguard::ScopeGuard, + AbortRegistration, + )> { + let (abort_handle, abort_reg) = AbortHandle::new_pair(); + + // Register the new abort_handle. + let old_handle = self.sync_abort.lock().unwrap().replace(abort_handle); + if old_handle.is_some() { + // NOTE: In the future we would ideally be able to handle multiple + // abort handles by just iterating over them all in + // abort_sync). But for now, just log a warning if there was + // already one present -- but don't abort it either. + let log = self.with_col(|col| Ok(col.log.clone()))?; + warn!( + log, + "new sync_abort handle registered, but old one was still present (old sync job might not be cancelled on abort)" + ); + } + // Clear the abort handle after the caller is done and drops the guard. + let guard = scopeguard::guard(Arc::clone(&self.sync_abort), |sync_abort| { + sync_abort.lock().unwrap().take(); + }); + Ok((guard, abort_reg)) + } + + pub(super) fn sync_media_inner(&self, input: pb::SyncAuth) -> Result<()> { + // mark media sync as active + let (abort_handle, abort_reg) = AbortHandle::new_pair(); + { + let mut guard = self.state.lock().unwrap(); + if guard.media_sync_abort.is_some() { + // media sync is already active + return Ok(()); + } else { + guard.media_sync_abort = Some(abort_handle); + } + } + + // get required info from collection + let mut guard = self.col.lock().unwrap(); + let col = guard.as_mut().unwrap(); + let folder = col.media_folder.clone(); + let db = col.media_db.clone(); + let log = col.log.clone(); + drop(guard); + + // start the sync + let mut handler = self.new_progress_handler(); + let progress_fn = move |progress| handler.update(progress, true); + + let mgr = MediaManager::new(&folder, &db)?; + let rt = self.runtime_handle(); + let sync_fut = mgr.sync_media(progress_fn, input.host_number, &input.hkey, log); + let abortable_sync = Abortable::new(sync_fut, abort_reg); + let result = rt.block_on(abortable_sync); + + // mark inactive + self.state.lock().unwrap().media_sync_abort.take(); + + // return result + match result { + Ok(sync_result) => sync_result, + Err(_) => { + // aborted sync + Err(AnkiError::Interrupted) + } + } + } + + /// Abort the media sync. Won't return until aborted. + pub(super) fn abort_media_sync_and_wait(&self) { + let guard = self.state.lock().unwrap(); + if let Some(handle) = &guard.media_sync_abort { + handle.abort(); + self.progress_state.lock().unwrap().want_abort = true; + } + drop(guard); + + // block until it aborts + while self.state.lock().unwrap().media_sync_abort.is_some() { + std::thread::sleep(std::time::Duration::from_millis(100)); + self.progress_state.lock().unwrap().want_abort = true; + } + } + + pub(super) fn sync_login_inner(&self, input: pb::SyncLoginIn) -> Result { + let (_guard, abort_reg) = self.sync_abort_handle()?; + + let rt = self.runtime_handle(); + let sync_fut = sync_login(&input.username, &input.password); + let abortable_sync = Abortable::new(sync_fut, abort_reg); + let ret = match rt.block_on(abortable_sync) { + Ok(sync_result) => sync_result, + Err(_) => Err(AnkiError::Interrupted), + }; + ret.map(|a| pb::SyncAuth { + hkey: a.hkey, + host_number: a.host_number, + }) + } + + pub(super) fn sync_status_inner(&self, input: pb::SyncAuth) -> Result { + // any local changes mean we can skip the network round-trip + let req = self.with_col(|col| col.get_local_sync_status())?; + if req != pb::sync_status_out::Required::NoChanges { + return Ok(req.into()); + } + + // return cached server response if only a short time has elapsed + { + let guard = self.state.lock().unwrap(); + if guard.remote_sync_status.last_check.elapsed_secs() < 300 { + return Ok(guard.remote_sync_status.last_response.into()); + } + } + + // fetch and cache result + let rt = self.runtime_handle(); + let time_at_check_begin = TimestampSecs::now(); + let remote: SyncMeta = rt.block_on(get_remote_sync_meta(input.into()))?; + let response = self.with_col(|col| col.get_sync_status(remote).map(Into::into))?; + + { + let mut guard = self.state.lock().unwrap(); + // On startup, the sync status check will block on network access, and then automatic syncing begins, + // taking hold of the mutex. By the time we reach here, our network status may be out of date, + // so we discard it if stale. + if guard.remote_sync_status.last_check < time_at_check_begin { + guard.remote_sync_status.last_check = time_at_check_begin; + guard.remote_sync_status.last_response = response; + } + } + + Ok(response.into()) + } + + pub(super) fn sync_collection_inner( + &self, + input: pb::SyncAuth, + ) -> Result { + let (_guard, abort_reg) = self.sync_abort_handle()?; + + let rt = self.runtime_handle(); + let input_copy = input.clone(); + + let ret = self.with_col(|col| { + let mut handler = self.new_progress_handler(); + let progress_fn = move |progress: NormalSyncProgress, throttle: bool| { + handler.update(progress, throttle); + }; + + let sync_fut = col.normal_sync(input.into(), progress_fn); + let abortable_sync = Abortable::new(sync_fut, abort_reg); + + match rt.block_on(abortable_sync) { + Ok(sync_result) => sync_result, + Err(_) => { + // if the user aborted, we'll need to clean up the transaction + col.storage.rollback_trx()?; + // and tell AnkiWeb to clean up + let _handle = std::thread::spawn(move || { + let _ = rt.block_on(sync_abort(input_copy.hkey, input_copy.host_number)); + }); + + Err(AnkiError::Interrupted) + } + } + }); + + let output: SyncOutput = ret?; + self.state + .lock() + .unwrap() + .remote_sync_status + .update(output.required.into()); + Ok(output.into()) + } + + pub(super) fn full_sync_inner(&self, input: pb::SyncAuth, upload: bool) -> Result<()> { + self.abort_media_sync_and_wait(); + + let rt = self.runtime_handle(); + + let mut col = self.col.lock().unwrap(); + if col.is_none() { + return Err(AnkiError::CollectionNotOpen); + } + + let col_inner = col.take().unwrap(); + + let (_guard, abort_reg) = self.sync_abort_handle()?; + + let col_path = col_inner.col_path.clone(); + let media_folder_path = col_inner.media_folder.clone(); + let media_db_path = col_inner.media_db.clone(); + let logger = col_inner.log.clone(); + + let mut handler = self.new_progress_handler(); + let progress_fn = move |progress: FullSyncProgress, throttle: bool| { + handler.update(progress, throttle); + }; + + let result = if upload { + let sync_fut = col_inner.full_upload(input.into(), Box::new(progress_fn)); + let abortable_sync = Abortable::new(sync_fut, abort_reg); + rt.block_on(abortable_sync) + } else { + let sync_fut = col_inner.full_download(input.into(), Box::new(progress_fn)); + let abortable_sync = Abortable::new(sync_fut, abort_reg); + rt.block_on(abortable_sync) + }; + + // ensure re-opened regardless of outcome + col.replace(open_collection( + col_path, + media_folder_path, + media_db_path, + self.server, + self.i18n.clone(), + logger, + )?); + + match result { + Ok(sync_result) => { + if sync_result.is_ok() { + self.state + .lock() + .unwrap() + .remote_sync_status + .update(pb::sync_status_out::Required::NoChanges); + } + sync_result + } + Err(_) => Err(AnkiError::Interrupted), + } + } +} diff --git a/rslib/src/card.rs b/rslib/src/card/mod.rs similarity index 56% rename from rslib/src/card.rs rename to rslib/src/card/mod.rs index dc8062a9a..1cbec18af 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card/mod.rs @@ -1,13 +1,15 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::define_newtype; +pub(crate) mod undo; + use crate::err::{AnkiError, Result}; use crate::notes::NoteID; use crate::{ collection::Collection, config::SchedulerVersion, timestamp::TimestampSecs, types::Usn, - undo::Undoable, }; +use crate::{define_newtype, undo::UndoableOpKind}; + use crate::{deckconf::DeckConf, decks::DeckID}; use num_enum::TryFromPrimitive; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -118,17 +120,9 @@ impl Card { pub fn ease_factor(&self) -> f32 { (self.ease_factor as f32) / 1000.0 } -} -#[derive(Debug)] -pub(crate) struct UpdateCardUndo(Card); -impl Undoable for UpdateCardUndo { - fn apply(&self, col: &mut crate::collection::Collection, usn: Usn) -> Result<()> { - let current = col - .storage - .get_card(self.0.id)? - .ok_or_else(|| AnkiError::invalid_input("card disappeared"))?; - col.update_card(&mut self.0.clone(), ¤t, usn) + pub fn is_intraday_learning(&self) -> bool { + matches!(self.queue, CardQueue::Learn | CardQueue::PreviewRepeat) } } @@ -145,6 +139,15 @@ impl Card { } impl Collection { + pub(crate) fn update_card_with_op( + &mut self, + card: &mut Card, + op: Option, + ) -> Result<()> { + let existing = self.storage.get_card(card.id)?.ok_or(AnkiError::NotFound)?; + self.transact(op, |col| col.update_card_inner(card, &existing, col.usn()?)) + } + #[cfg(test)] pub(crate) fn get_and_update_card(&mut self, cid: CardID, func: F) -> Result where @@ -156,19 +159,19 @@ impl Collection { .ok_or_else(|| AnkiError::invalid_input("no such card"))?; let mut card = orig.clone(); func(&mut card)?; - self.update_card(&mut card, &orig, self.usn()?)?; + self.update_card_inner(&mut card, &orig, self.usn()?)?; Ok(card) } - pub(crate) fn update_card(&mut self, card: &mut Card, original: &Card, usn: Usn) -> Result<()> { - if card.id.0 == 0 { - return Err(AnkiError::invalid_input("card id not set")); - } - self.state - .undo - .save_undoable(Box::new(UpdateCardUndo(original.clone()))); + /// Marks the card as modified, then saves it. + pub(crate) fn update_card_inner( + &mut self, + card: &mut Card, + original: &Card, + usn: Usn, + ) -> Result<()> { card.set_modified(usn); - self.storage.update_card(card) + self.update_card_undoable(card, original) } pub(crate) fn add_card(&mut self, card: &mut Card) -> Result<()> { @@ -177,7 +180,7 @@ impl Collection { } card.mtime = TimestampSecs::now(); card.usn = self.usn()?; - self.storage.add_card(card) + self.add_card_undoable(card) } /// Remove cards and any resulting orphaned notes. @@ -187,29 +190,19 @@ impl Collection { let mut nids = HashSet::new(); for cid in cids { if let Some(card) = self.storage.get_card(*cid)? { - // fixme: undo nids.insert(card.note_id); - self.storage.remove_card(*cid)?; - self.storage.add_card_grave(*cid, usn)?; + self.remove_card_and_add_grave_undoable(card, usn)?; } } for nid in nids { if self.storage.note_is_orphaned(nid)? { - self.remove_note_only(nid, usn)?; + self.remove_note_only_undoable(nid, usn)?; } } Ok(()) } - pub(crate) fn remove_card_only(&mut self, card: Card, usn: Usn) -> Result<()> { - // fixme: undo - self.storage.remove_card(card.id)?; - self.storage.add_card_grave(card.id, usn)?; - - Ok(()) - } - pub fn set_deck(&mut self, cards: &[CardID], deck_id: DeckID) -> Result<()> { let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?; if deck.is_filtered() { @@ -225,7 +218,7 @@ impl Collection { } let original = card.clone(); card.set_deck(deck_id, sched); - col.update_card(&mut card, &original, usn)?; + col.update_card_inner(&mut card, &original, usn)?; } Ok(()) }) @@ -243,99 +236,3 @@ impl Collection { Ok(DeckConf::default()) } } - -#[cfg(test)] -mod test { - use super::Card; - use crate::collection::{open_test_collection, CollectionOp}; - - #[test] - fn undo() { - let mut col = open_test_collection(); - - let mut card = Card::default(); - card.interval = 1; - col.add_card(&mut card).unwrap(); - let cid = card.id; - - assert_eq!(col.can_undo(), None); - assert_eq!(col.can_redo(), None); - - // outside of a transaction, no undo info recorded - let card = col - .get_and_update_card(cid, |card| { - card.interval = 2; - Ok(()) - }) - .unwrap(); - assert_eq!(card.interval, 2); - assert_eq!(col.can_undo(), None); - assert_eq!(col.can_redo(), None); - - // record a few undo steps - for i in 3..=4 { - col.transact(Some(CollectionOp::UpdateCard), |col| { - col.get_and_update_card(cid, |card| { - card.interval = i; - Ok(()) - }) - .unwrap(); - Ok(()) - }) - .unwrap(); - } - - assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 4); - assert_eq!(col.can_undo(), Some(CollectionOp::UpdateCard)); - assert_eq!(col.can_redo(), None); - - // undo a step - col.undo().unwrap(); - assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 3); - assert_eq!(col.can_undo(), Some(CollectionOp::UpdateCard)); - assert_eq!(col.can_redo(), Some(CollectionOp::UpdateCard)); - - // and again - col.undo().unwrap(); - assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 2); - assert_eq!(col.can_undo(), None); - assert_eq!(col.can_redo(), Some(CollectionOp::UpdateCard)); - - // redo a step - col.redo().unwrap(); - assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 3); - assert_eq!(col.can_undo(), Some(CollectionOp::UpdateCard)); - assert_eq!(col.can_redo(), Some(CollectionOp::UpdateCard)); - - // and another - col.redo().unwrap(); - assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 4); - assert_eq!(col.can_undo(), Some(CollectionOp::UpdateCard)); - assert_eq!(col.can_redo(), None); - - // and undo the redo - col.undo().unwrap(); - assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 3); - assert_eq!(col.can_undo(), Some(CollectionOp::UpdateCard)); - assert_eq!(col.can_redo(), Some(CollectionOp::UpdateCard)); - - // if any action is performed, it should clear the redo queue - col.transact(Some(CollectionOp::UpdateCard), |col| { - col.get_and_update_card(cid, |card| { - card.interval = 5; - Ok(()) - }) - .unwrap(); - Ok(()) - }) - .unwrap(); - assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 5); - assert_eq!(col.can_undo(), Some(CollectionOp::UpdateCard)); - assert_eq!(col.can_redo(), None); - - // and any action that doesn't support undoing will clear both queues - col.transact(None, |_col| Ok(())).unwrap(); - assert_eq!(col.can_undo(), None); - assert_eq!(col.can_redo(), None); - } -} diff --git a/rslib/src/card/undo.rs b/rslib/src/card/undo.rs new file mode 100644 index 000000000..b6375c53a --- /dev/null +++ b/rslib/src/card/undo.rs @@ -0,0 +1,78 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::prelude::*; + +#[derive(Debug)] +pub(crate) enum UndoableCardChange { + Added(Box), + Updated(Box), + Removed(Box), + GraveAdded(Box<(CardID, Usn)>), + GraveRemoved(Box<(CardID, Usn)>), +} + +impl Collection { + pub(crate) fn undo_card_change(&mut self, change: UndoableCardChange) -> Result<()> { + match change { + UndoableCardChange::Added(card) => self.remove_card_only(*card), + UndoableCardChange::Updated(mut card) => { + let current = self + .storage + .get_card(card.id)? + .ok_or_else(|| AnkiError::invalid_input("card disappeared"))?; + self.update_card_undoable(&mut *card, ¤t) + } + UndoableCardChange::Removed(card) => self.restore_deleted_card(*card), + UndoableCardChange::GraveAdded(e) => self.remove_card_grave(e.0, e.1), + UndoableCardChange::GraveRemoved(e) => self.add_card_grave_undoable(e.0, e.1), + } + } + + pub(super) fn add_card_undoable(&mut self, card: &mut Card) -> Result<(), AnkiError> { + self.storage.add_card(card)?; + self.save_undo(UndoableCardChange::Added(Box::new(card.clone()))); + Ok(()) + } + + pub(super) fn update_card_undoable(&mut self, card: &mut Card, original: &Card) -> Result<()> { + if card.id.0 == 0 { + return Err(AnkiError::invalid_input("card id not set")); + } + self.save_undo(UndoableCardChange::Updated(Box::new(original.clone()))); + self.storage.update_card(card) + } + + pub(crate) fn remove_card_and_add_grave_undoable( + &mut self, + card: Card, + usn: Usn, + ) -> Result<()> { + self.add_card_grave_undoable(card.id, usn)?; + self.storage.remove_card(card.id)?; + self.save_undo(UndoableCardChange::Removed(Box::new(card))); + Ok(()) + } + + fn restore_deleted_card(&mut self, card: Card) -> Result<()> { + self.storage.add_or_update_card(&card)?; + self.save_undo(UndoableCardChange::Added(Box::new(card))); + Ok(()) + } + + fn remove_card_only(&mut self, card: Card) -> Result<()> { + self.storage.remove_card(card.id)?; + self.save_undo(UndoableCardChange::Removed(Box::new(card))); + Ok(()) + } + + fn add_card_grave_undoable(&mut self, cid: CardID, usn: Usn) -> Result<()> { + self.save_undo(UndoableCardChange::GraveAdded(Box::new((cid, usn)))); + self.storage.add_card_grave(cid, usn) + } + + fn remove_card_grave(&mut self, cid: CardID, usn: Usn) -> Result<()> { + self.save_undo(UndoableCardChange::GraveRemoved(Box::new((cid, usn)))); + self.storage.remove_card_grave(cid) + } +} diff --git a/rslib/src/collection.rs b/rslib/src/collection.rs index 7b652a7b8..bb701f9b6 100644 --- a/rslib/src/collection.rs +++ b/rslib/src/collection.rs @@ -1,16 +1,17 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::err::Result; use crate::i18n::I18n; use crate::log::Logger; use crate::types::Usn; use crate::{ decks::{Deck, DeckID}, notetype::{NoteType, NoteTypeID}, + prelude::*, storage::SqliteStorage, undo::UndoManager, }; +use crate::{err::Result, scheduler::queue::CardQueues}; use std::{collections::HashMap, path::PathBuf, sync::Arc}; pub fn open_collection>( @@ -43,7 +44,7 @@ pub fn open_collection>( #[cfg(test)] pub fn open_test_collection() -> Collection { use crate::config::SchedulerVersion; - let col = open_test_collection_with_server(false); + let mut col = open_test_collection_with_server(false); // our unit tests assume v2 is the default, but at the time of writing v1 // is still the default col.set_scheduler_version_config_key(SchedulerVersion::V2) @@ -63,6 +64,7 @@ pub struct CollectionState { pub(crate) undo: UndoManager, pub(crate) notetype_cache: HashMap>, pub(crate) deck_cache: HashMap>, + pub(crate) card_queues: Option, } pub struct Collection { @@ -77,20 +79,15 @@ pub struct Collection { pub(crate) state: CollectionState, } -#[derive(Debug, Clone, PartialEq)] -pub enum CollectionOp { - UpdateCard, -} - impl Collection { /// Execute the provided closure in a transaction, rolling back if /// an error is returned. - pub(crate) fn transact(&mut self, op: Option, func: F) -> Result + pub(crate) fn transact(&mut self, op: Option, func: F) -> Result where F: FnOnce(&mut Collection) -> Result, { self.storage.begin_rust_trx()?; - self.state.undo.begin_step(op); + self.begin_undoable_operation(op); let mut res = func(self); @@ -103,10 +100,10 @@ impl Collection { } if res.is_err() { - self.state.undo.discard_step(); + self.discard_undo_and_study_queues(); self.storage.rollback_rust_trx()?; } else { - self.state.undo.end_step(); + self.end_undoable_operation(); } res @@ -138,4 +135,9 @@ impl Collection { })?; self.storage.optimize() } + + pub(crate) fn clear_caches(&mut self) { + self.state.deck_cache.clear(); + self.state.notetype_cache.clear(); + } } diff --git a/rslib/src/config.rs b/rslib/src/config.rs deleted file mode 100644 index 14ebf3fef..000000000 --- a/rslib/src/config.rs +++ /dev/null @@ -1,479 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -use crate::{ - backend_proto as pb, collection::Collection, decks::DeckID, err::Result, notetype::NoteTypeID, - timestamp::TimestampSecs, -}; -use pb::config::bool::Key as BoolKey; -use pb::config::string::Key as StringKey; -use serde::{de::DeserializeOwned, Serialize}; -use serde_aux::field_attributes::deserialize_bool_from_anything; -use serde_derive::Deserialize; -use serde_json::json; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use slog::warn; - -/// These items are expected to exist in schema 11. When adding -/// new config variables, you do not need to add them here - -/// just create an accessor function below with an appropriate -/// default on missing/invalid values instead. -pub(crate) fn schema11_config_as_string() -> String { - let obj = json!({ - "activeDecks": [1], - "curDeck": 1, - "newSpread": 0, - "collapseTime": 1200, - "timeLim": 0, - "estTimes": true, - "dueCounts": true, - "curModel": null, - "nextPos": 1, - "sortType": "noteFld", - "sortBackwards": false, - "addToCur": true, - "dayLearnFirst": false, - "schedVer": 1, - }); - serde_json::to_string(&obj).unwrap() -} - -pub(crate) enum ConfigKey { - AnswerTimeLimitSecs, - BrowserSortKind, - BrowserSortReverse, - CardCountsSeparateInactive, - CollapseCardState, - CollapseDecks, - CollapseFlags, - CollapseNotetypes, - CollapseSavedSearches, - CollapseTags, - CollapseToday, - CreationOffset, - CurrentDeckID, - CurrentNoteTypeID, - FirstDayOfWeek, - FutureDueShowBacklog, - LastUnburiedDay, - LearnAheadSecs, - LocalOffset, - NewReviewMix, - NextNewCardPosition, - NormalizeNoteText, - PreviewBothSides, - Rollover, - SchedulerVersion, - SetDueBrowser, - SetDueReviewer, - ShowDayLearningCardsFirst, - ShowIntervalsAboveAnswerButtons, - ShowRemainingDueCountsInStudy, -} -#[derive(PartialEq, Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] -#[repr(u8)] -pub(crate) enum SchedulerVersion { - V1 = 1, - V2 = 2, -} - -impl From for &'static str { - fn from(c: ConfigKey) -> Self { - match c { - ConfigKey::AnswerTimeLimitSecs => "timeLim", - ConfigKey::BrowserSortKind => "sortType", - ConfigKey::BrowserSortReverse => "sortBackwards", - ConfigKey::CardCountsSeparateInactive => "cardCountsSeparateInactive", - ConfigKey::CollapseCardState => "collapseCardState", - ConfigKey::CollapseDecks => "collapseDecks", - ConfigKey::CollapseFlags => "collapseFlags", - ConfigKey::CollapseNotetypes => "collapseNotetypes", - ConfigKey::CollapseSavedSearches => "collapseSavedSearches", - ConfigKey::CollapseTags => "collapseTags", - ConfigKey::CollapseToday => "collapseToday", - ConfigKey::CreationOffset => "creationOffset", - ConfigKey::CurrentDeckID => "curDeck", - ConfigKey::CurrentNoteTypeID => "curModel", - ConfigKey::FirstDayOfWeek => "firstDayOfWeek", - ConfigKey::FutureDueShowBacklog => "futureDueShowBacklog", - ConfigKey::LastUnburiedDay => "lastUnburied", - ConfigKey::LearnAheadSecs => "collapseTime", - ConfigKey::LocalOffset => "localOffset", - ConfigKey::NewReviewMix => "newSpread", - ConfigKey::NextNewCardPosition => "nextPos", - ConfigKey::NormalizeNoteText => "normalize_note_text", - ConfigKey::PreviewBothSides => "previewBothSides", - ConfigKey::Rollover => "rollover", - ConfigKey::SchedulerVersion => "schedVer", - ConfigKey::SetDueBrowser => "setDueBrowser", - ConfigKey::SetDueReviewer => "setDueReviewer", - ConfigKey::ShowDayLearningCardsFirst => "dayLearnFirst", - ConfigKey::ShowIntervalsAboveAnswerButtons => "estTimes", - ConfigKey::ShowRemainingDueCountsInStudy => "dueCounts", - } - } -} - -impl From for ConfigKey { - fn from(key: BoolKey) -> Self { - match key { - BoolKey::BrowserSortBackwards => ConfigKey::BrowserSortReverse, - BoolKey::CollapseCardState => ConfigKey::CollapseCardState, - BoolKey::CollapseDecks => ConfigKey::CollapseDecks, - BoolKey::CollapseFlags => ConfigKey::CollapseFlags, - BoolKey::CollapseNotetypes => ConfigKey::CollapseNotetypes, - BoolKey::CollapseSavedSearches => ConfigKey::CollapseSavedSearches, - BoolKey::CollapseTags => ConfigKey::CollapseTags, - BoolKey::CollapseToday => ConfigKey::CollapseToday, - BoolKey::PreviewBothSides => ConfigKey::PreviewBothSides, - } - } -} - -impl From for ConfigKey { - fn from(key: StringKey) -> Self { - match key { - StringKey::SetDueBrowser => ConfigKey::SetDueBrowser, - StringKey::SetDueReviewer => ConfigKey::SetDueReviewer, - } - } -} - -/// This is a workaround for old clients that used ints to represent boolean -/// values. For new config items, prefer using a bool directly. -#[derive(Deserialize, Default)] -struct BoolLike(#[serde(deserialize_with = "deserialize_bool_from_anything")] bool); - -impl Collection { - /// Get config item, returning None if missing/invalid. - pub(crate) fn get_config_optional<'a, T, K>(&self, key: K) -> Option - where - T: DeserializeOwned, - K: Into<&'a str>, - { - let key = key.into(); - match self.storage.get_config_value(key) { - Ok(Some(val)) => Some(val), - Ok(None) => None, - Err(e) => { - warn!(self.log, "error accessing config key"; "key"=>key, "err"=>?e); - None - } - } - } - - // /// Get config item, returning default value if missing/invalid. - pub(crate) fn get_config_default(&self, key: K) -> T - where - T: DeserializeOwned + Default, - K: Into<&'static str>, - { - self.get_config_optional(key).unwrap_or_default() - } - - pub(crate) fn set_config<'a, T: Serialize, K>(&self, key: K, val: &T) -> Result<()> - where - K: Into<&'a str>, - { - self.storage - .set_config_value(key.into(), val, self.usn()?, TimestampSecs::now()) - } - - pub(crate) fn remove_config<'a, K>(&self, key: K) -> Result<()> - where - K: Into<&'a str>, - { - self.storage.remove_config(key.into()) - } - - pub(crate) fn get_browser_sort_kind(&self) -> SortKind { - self.get_config_default(ConfigKey::BrowserSortKind) - } - - pub(crate) fn get_browser_sort_reverse(&self) -> bool { - let b: BoolLike = self.get_config_default(ConfigKey::BrowserSortReverse); - b.0 - } - - pub(crate) fn get_current_deck_id(&self) -> DeckID { - self.get_config_optional(ConfigKey::CurrentDeckID) - .unwrap_or(DeckID(1)) - } - - pub(crate) fn get_creation_utc_offset(&self) -> Option { - self.get_config_optional(ConfigKey::CreationOffset) - } - - pub(crate) fn set_creation_utc_offset(&self, mins: Option) -> Result<()> { - if let Some(mins) = mins { - self.set_config(ConfigKey::CreationOffset, &mins) - } else { - self.remove_config(ConfigKey::CreationOffset) - } - } - - pub(crate) fn get_configured_utc_offset(&self) -> Option { - self.get_config_optional(ConfigKey::LocalOffset) - } - - pub(crate) fn set_configured_utc_offset(&self, mins: i32) -> Result<()> { - self.set_config(ConfigKey::LocalOffset, &mins) - } - - pub(crate) fn get_v2_rollover(&self) -> Option { - self.get_config_optional::(ConfigKey::Rollover) - .map(|r| r.min(23)) - } - - pub(crate) fn set_v2_rollover(&self, hour: u32) -> Result<()> { - self.set_config(ConfigKey::Rollover, &hour) - } - - #[allow(dead_code)] - pub(crate) fn get_current_notetype_id(&self) -> Option { - self.get_config_optional(ConfigKey::CurrentNoteTypeID) - } - - pub(crate) fn set_current_notetype_id(&self, id: NoteTypeID) -> Result<()> { - self.set_config(ConfigKey::CurrentNoteTypeID, &id) - } - - pub(crate) fn get_next_card_position(&self) -> u32 { - self.get_config_default(ConfigKey::NextNewCardPosition) - } - - pub(crate) fn get_and_update_next_card_position(&self) -> Result { - let pos: u32 = self - .get_config_optional(ConfigKey::NextNewCardPosition) - .unwrap_or_default(); - self.set_config(ConfigKey::NextNewCardPosition, &pos.wrapping_add(1))?; - Ok(pos) - } - - pub(crate) fn set_next_card_position(&self, pos: u32) -> Result<()> { - self.set_config(ConfigKey::NextNewCardPosition, &pos) - } - - pub(crate) fn scheduler_version(&self) -> SchedulerVersion { - self.get_config_optional(ConfigKey::SchedulerVersion) - .unwrap_or(SchedulerVersion::V1) - } - - /// Caution: this only updates the config setting. - pub(crate) fn set_scheduler_version_config_key(&self, ver: SchedulerVersion) -> Result<()> { - self.set_config(ConfigKey::SchedulerVersion, &ver) - } - - pub(crate) fn learn_ahead_secs(&self) -> u32 { - self.get_config_optional(ConfigKey::LearnAheadSecs) - .unwrap_or(1200) - } - - pub(crate) fn set_learn_ahead_secs(&self, secs: u32) -> Result<()> { - self.set_config(ConfigKey::LearnAheadSecs, &secs) - } - - /// This is a stop-gap solution until we can decouple searching from canonical storage. - pub(crate) fn normalize_note_text(&self) -> bool { - self.get_config_optional(ConfigKey::NormalizeNoteText) - .unwrap_or(true) - } - - pub(crate) fn get_new_review_mix(&self) -> NewReviewMix { - match self.get_config_default::(ConfigKey::NewReviewMix) { - 1 => NewReviewMix::ReviewsFirst, - 2 => NewReviewMix::NewFirst, - _ => NewReviewMix::Mix, - } - } - - pub(crate) fn set_new_review_mix(&self, mix: NewReviewMix) -> Result<()> { - self.set_config(ConfigKey::NewReviewMix, &(mix as u8)) - } - - pub(crate) fn get_first_day_of_week(&self) -> Weekday { - self.get_config_optional(ConfigKey::FirstDayOfWeek) - .unwrap_or(Weekday::Sunday) - } - - pub(crate) fn set_first_day_of_week(&self, weekday: Weekday) -> Result<()> { - self.set_config(ConfigKey::FirstDayOfWeek, &weekday) - } - - pub(crate) fn get_card_counts_separate_inactive(&self) -> bool { - self.get_config_optional(ConfigKey::CardCountsSeparateInactive) - .unwrap_or(true) - } - - pub(crate) fn set_card_counts_separate_inactive(&self, separate: bool) -> Result<()> { - self.set_config(ConfigKey::CardCountsSeparateInactive, &separate) - } - - pub(crate) fn get_future_due_show_backlog(&self) -> bool { - self.get_config_optional(ConfigKey::FutureDueShowBacklog) - .unwrap_or(true) - } - - pub(crate) fn set_future_due_show_backlog(&self, show: bool) -> Result<()> { - self.set_config(ConfigKey::FutureDueShowBacklog, &show) - } - - pub(crate) fn get_show_due_counts(&self) -> bool { - self.get_config_optional(ConfigKey::ShowRemainingDueCountsInStudy) - .unwrap_or(true) - } - - pub(crate) fn set_show_due_counts(&self, on: bool) -> Result<()> { - self.set_config(ConfigKey::ShowRemainingDueCountsInStudy, &on) - } - - pub(crate) fn get_show_intervals_above_buttons(&self) -> bool { - self.get_config_optional(ConfigKey::ShowIntervalsAboveAnswerButtons) - .unwrap_or(true) - } - - pub(crate) fn set_show_intervals_above_buttons(&self, on: bool) -> Result<()> { - self.set_config(ConfigKey::ShowIntervalsAboveAnswerButtons, &on) - } - - pub(crate) fn get_answer_time_limit_secs(&self) -> u32 { - self.get_config_optional(ConfigKey::AnswerTimeLimitSecs) - .unwrap_or_default() - } - - pub(crate) fn set_answer_time_limit_secs(&self, secs: u32) -> Result<()> { - self.set_config(ConfigKey::AnswerTimeLimitSecs, &secs) - } - - pub(crate) fn get_day_learn_first(&self) -> bool { - self.get_config_optional(ConfigKey::ShowDayLearningCardsFirst) - .unwrap_or_default() - } - - pub(crate) fn set_day_learn_first(&self, on: bool) -> Result<()> { - self.set_config(ConfigKey::ShowDayLearningCardsFirst, &on) - } - - pub(crate) fn get_last_unburied_day(&self) -> u32 { - self.get_config_optional(ConfigKey::LastUnburiedDay) - .unwrap_or_default() - } - - pub(crate) fn set_last_unburied_day(&self, day: u32) -> Result<()> { - self.set_config(ConfigKey::LastUnburiedDay, &day) - } - - #[allow(clippy::match_single_binding)] - pub(crate) fn get_bool(&self, config: pb::config::Bool) -> bool { - match config.key() { - // all options default to false at the moment - other => self.get_config_default(ConfigKey::from(other)), - } - } - - pub(crate) fn set_bool(&self, input: pb::SetConfigBoolIn) -> Result<()> { - self.set_config(ConfigKey::from(input.key()), &input.value) - } - - pub(crate) fn get_string(&self, config: pb::config::String) -> String { - let key = config.key(); - let default = match key { - StringKey::SetDueBrowser => "0", - StringKey::SetDueReviewer => "1", - // other => "", - }; - self.get_config_optional(ConfigKey::from(key)) - .unwrap_or_else(|| default.to_string()) - } - - pub(crate) fn set_string(&self, input: pb::SetConfigStringIn) -> Result<()> { - self.set_config(ConfigKey::from(input.key()), &input.value) - } -} - -#[derive(Deserialize, PartialEq, Debug, Clone, Copy)] -#[serde(rename_all = "camelCase")] -pub enum SortKind { - #[serde(rename = "noteCrt")] - NoteCreation, - NoteMod, - #[serde(rename = "noteFld")] - NoteField, - #[serde(rename = "note")] - NoteType, - NoteTags, - CardMod, - CardReps, - CardDue, - CardEase, - CardLapses, - #[serde(rename = "cardIvl")] - CardInterval, - #[serde(rename = "deck")] - CardDeck, - #[serde(rename = "template")] - CardTemplate, -} - -impl Default for SortKind { - fn default() -> Self { - Self::NoteCreation - } -} - -pub(crate) enum NewReviewMix { - Mix = 0, - ReviewsFirst = 1, - NewFirst = 2, -} - -#[derive(PartialEq, Serialize_repr, Deserialize_repr, Clone, Copy)] -#[repr(u8)] -pub(crate) enum Weekday { - Sunday = 0, - Monday = 1, - Friday = 5, - Saturday = 6, -} - -#[cfg(test)] -mod test { - use super::SortKind; - use crate::collection::open_test_collection; - use crate::decks::DeckID; - - #[test] - fn defaults() { - let col = open_test_collection(); - assert_eq!(col.get_current_deck_id(), DeckID(1)); - assert_eq!(col.get_browser_sort_kind(), SortKind::NoteField); - } - - #[test] - fn get_set() { - let col = open_test_collection(); - - // missing key - assert_eq!(col.get_config_optional::, _>("test"), None); - - // normal retrieval - col.set_config("test", &vec![1, 2]).unwrap(); - assert_eq!( - col.get_config_optional::, _>("test"), - Some(vec![1, 2]) - ); - - // invalid type conversion - assert_eq!(col.get_config_optional::("test"), None,); - - // invalid json - col.storage - .db - .execute( - "update config set val=? where key='test'", - &[b"xx".as_ref()], - ) - .unwrap(); - assert_eq!(col.get_config_optional::("test"), None,); - } -} diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs new file mode 100644 index 000000000..06f13e31e --- /dev/null +++ b/rslib/src/config/bool.rs @@ -0,0 +1,67 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::prelude::*; +use serde_aux::field_attributes::deserialize_bool_from_anything; +use serde_derive::Deserialize; +use strum::IntoStaticStr; + +#[derive(Debug, Clone, Copy, IntoStaticStr)] +#[strum(serialize_all = "camelCase")] +pub enum BoolKey { + CardCountsSeparateInactive, + CollapseCardState, + CollapseDecks, + CollapseFlags, + CollapseNotetypes, + CollapseSavedSearches, + CollapseTags, + CollapseToday, + FutureDueShowBacklog, + PreviewBothSides, + Sched2021, + + #[strum(to_string = "sortBackwards")] + BrowserSortBackwards, + #[strum(to_string = "normalize_note_text")] + NormalizeNoteText, + #[strum(to_string = "dayLearnFirst")] + ShowDayLearningCardsFirst, + #[strum(to_string = "estTimes")] + ShowIntervalsAboveAnswerButtons, + #[strum(to_string = "dueCounts")] + ShowRemainingDueCountsInStudy, + #[strum(to_string = "addToCur")] + AddingDefaultsToCurrentDeck, +} + +/// This is a workaround for old clients that used ints to represent boolean +/// values. For new config items, prefer using a bool directly. +#[derive(Deserialize, Default)] +struct BoolLike(#[serde(deserialize_with = "deserialize_bool_from_anything")] bool); + +impl Collection { + pub(crate) fn get_bool(&self, key: BoolKey) -> bool { + match key { + BoolKey::BrowserSortBackwards => { + // older clients were storing this as an int + self.get_config_default::(BoolKey::BrowserSortBackwards) + .0 + } + + // some keys default to true + BoolKey::AddingDefaultsToCurrentDeck + | BoolKey::FutureDueShowBacklog + | BoolKey::ShowRemainingDueCountsInStudy + | BoolKey::CardCountsSeparateInactive + | BoolKey::NormalizeNoteText => self.get_config_optional(key).unwrap_or(true), + + // other options default to false + other => self.get_config_default(other), + } + } + + 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 new file mode 100644 index 000000000..22c7b8694 --- /dev/null +++ b/rslib/src/config/deck.rs @@ -0,0 +1,49 @@ +// 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; + +/// Auxillary deck state, stored in the config table. +#[derive(Debug, Clone, Copy, IntoStaticStr)] +#[strum(serialize_all = "camelCase")] +enum DeckConfigKey { + LastNotetype, +} + +impl DeckConfigKey { + fn for_deck(self, did: DeckID) -> String { + build_aux_deck_key(did, <&'static str>::from(self)) + } +} + +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, "")) + } + + pub(crate) fn get_last_notetype_for_deck(&self, id: DeckID) -> Option { + let key = DeckConfigKey::LastNotetype.for_deck(id); + self.get_config_optional(key.as_str()) + } + + pub(crate) fn set_last_notetype_for_deck( + &mut self, + did: DeckID, + ntid: NoteTypeID, + ) -> Result<()> { + let key = DeckConfigKey::LastNotetype.for_deck(did); + self.set_config(key.as_str(), &ntid) + } +} + +fn build_aux_deck_key(deck: DeckID, key: &str) -> String { + format!("_deck_{deck}_{key}", deck = deck, key = key) +} diff --git a/rslib/src/config/mod.rs b/rslib/src/config/mod.rs new file mode 100644 index 000000000..12c0ced95 --- /dev/null +++ b/rslib/src/config/mod.rs @@ -0,0 +1,328 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +mod bool; +mod deck; +mod notetype; +pub(crate) mod schema11; +mod string; +pub(crate) mod undo; + +pub use self::{bool::BoolKey, string::StringKey}; +use crate::prelude::*; +use serde::{de::DeserializeOwned, Serialize}; +use serde_derive::Deserialize; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use slog::warn; +use strum::IntoStaticStr; + +/// Only used when updating/undoing. +#[derive(Debug)] +pub(crate) struct ConfigEntry { + pub key: String, + pub value: Vec, + pub usn: Usn, + pub mtime: TimestampSecs, +} + +impl ConfigEntry { + pub(crate) fn boxed(key: &str, value: Vec, usn: Usn, mtime: TimestampSecs) -> Box { + Box::new(Self { + key: key.into(), + value, + usn, + mtime, + }) + } +} + +#[derive(IntoStaticStr)] +#[strum(serialize_all = "camelCase")] +pub(crate) enum ConfigKey { + CreationOffset, + FirstDayOfWeek, + LocalOffset, + Rollover, + + #[strum(to_string = "timeLim")] + AnswerTimeLimitSecs, + #[strum(to_string = "sortType")] + BrowserSortKind, + #[strum(to_string = "curDeck")] + CurrentDeckID, + #[strum(to_string = "curModel")] + CurrentNoteTypeID, + #[strum(to_string = "lastUnburied")] + LastUnburiedDay, + #[strum(to_string = "collapseTime")] + LearnAheadSecs, + #[strum(to_string = "newSpread")] + NewReviewMix, + #[strum(to_string = "nextPos")] + NextNewCardPosition, + #[strum(to_string = "schedVer")] + SchedulerVersion, +} + +#[derive(PartialEq, Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] +#[repr(u8)] +pub(crate) enum SchedulerVersion { + V1 = 1, + V2 = 2, +} +impl Collection { + /// Get config item, returning None if missing/invalid. + pub(crate) fn get_config_optional<'a, T, K>(&self, key: K) -> Option + where + T: DeserializeOwned, + K: Into<&'a str>, + { + let key = key.into(); + match self.storage.get_config_value(key) { + Ok(Some(val)) => Some(val), + Ok(None) => None, + Err(e) => { + warn!(self.log, "error accessing config key"; "key"=>key, "err"=>?e); + None + } + } + } + + // /// Get config item, returning default value if missing/invalid. + pub(crate) fn get_config_default(&self, key: K) -> T + where + T: DeserializeOwned + Default, + K: Into<&'static str>, + { + self.get_config_optional(key).unwrap_or_default() + } + + pub(crate) fn set_config<'a, T: Serialize, K>(&mut self, key: K, val: &T) -> Result<()> + where + K: Into<&'a str>, + { + let entry = ConfigEntry::boxed( + key.into(), + serde_json::to_vec(val)?, + self.usn()?, + TimestampSecs::now(), + ); + self.set_config_undoable(entry) + } + + pub(crate) fn remove_config<'a, K>(&mut self, key: K) -> Result<()> + where + K: Into<&'a str>, + { + self.remove_config_undoable(key.into()) + } + + /// Remove all keys starting with provided prefix, which must end with '_'. + pub(crate) fn remove_config_prefix(&self, key: &str) -> Result<()> { + for (key, _val) in self.storage.get_config_prefix(key)? { + self.storage.remove_config(&key)?; + } + Ok(()) + } + + pub(crate) fn get_browser_sort_kind(&self) -> SortKind { + self.get_config_default(ConfigKey::BrowserSortKind) + } + + pub(crate) fn get_creation_utc_offset(&self) -> Option { + self.get_config_optional(ConfigKey::CreationOffset) + } + + pub(crate) fn set_creation_utc_offset(&mut self, mins: Option) -> Result<()> { + if let Some(mins) = mins { + self.set_config(ConfigKey::CreationOffset, &mins) + } else { + self.remove_config(ConfigKey::CreationOffset) + } + } + + pub(crate) fn get_configured_utc_offset(&self) -> Option { + self.get_config_optional(ConfigKey::LocalOffset) + } + + pub(crate) fn set_configured_utc_offset(&mut self, mins: i32) -> Result<()> { + self.set_config(ConfigKey::LocalOffset, &mins) + } + + pub(crate) fn get_v2_rollover(&self) -> Option { + self.get_config_optional::(ConfigKey::Rollover) + .map(|r| r.min(23)) + } + + pub(crate) fn set_v2_rollover(&mut self, hour: u32) -> Result<()> { + self.set_config(ConfigKey::Rollover, &hour) + } + + pub(crate) fn get_next_card_position(&self) -> u32 { + self.get_config_default(ConfigKey::NextNewCardPosition) + } + + pub(crate) fn get_and_update_next_card_position(&mut self) -> Result { + let pos: u32 = self + .get_config_optional(ConfigKey::NextNewCardPosition) + .unwrap_or_default(); + self.set_config(ConfigKey::NextNewCardPosition, &pos.wrapping_add(1))?; + Ok(pos) + } + + pub(crate) fn set_next_card_position(&mut self, pos: u32) -> Result<()> { + self.set_config(ConfigKey::NextNewCardPosition, &pos) + } + + pub(crate) fn scheduler_version(&self) -> SchedulerVersion { + self.get_config_optional(ConfigKey::SchedulerVersion) + .unwrap_or(SchedulerVersion::V1) + } + + /// Caution: this only updates the config setting. + pub(crate) fn set_scheduler_version_config_key(&mut self, ver: SchedulerVersion) -> Result<()> { + self.set_config(ConfigKey::SchedulerVersion, &ver) + } + + pub(crate) fn learn_ahead_secs(&self) -> u32 { + self.get_config_optional(ConfigKey::LearnAheadSecs) + .unwrap_or(1200) + } + + pub(crate) fn set_learn_ahead_secs(&mut self, secs: u32) -> Result<()> { + self.set_config(ConfigKey::LearnAheadSecs, &secs) + } + + pub(crate) fn get_new_review_mix(&self) -> NewReviewMix { + match self.get_config_default::(ConfigKey::NewReviewMix) { + 1 => NewReviewMix::ReviewsFirst, + 2 => NewReviewMix::NewFirst, + _ => NewReviewMix::Mix, + } + } + + pub(crate) fn set_new_review_mix(&mut self, mix: NewReviewMix) -> Result<()> { + self.set_config(ConfigKey::NewReviewMix, &(mix as u8)) + } + + pub(crate) fn get_first_day_of_week(&self) -> Weekday { + self.get_config_optional(ConfigKey::FirstDayOfWeek) + .unwrap_or(Weekday::Sunday) + } + + pub(crate) fn set_first_day_of_week(&mut self, weekday: Weekday) -> Result<()> { + self.set_config(ConfigKey::FirstDayOfWeek, &weekday) + } + + pub(crate) fn get_answer_time_limit_secs(&self) -> u32 { + self.get_config_optional(ConfigKey::AnswerTimeLimitSecs) + .unwrap_or_default() + } + + pub(crate) fn set_answer_time_limit_secs(&mut self, secs: u32) -> Result<()> { + self.set_config(ConfigKey::AnswerTimeLimitSecs, &secs) + } + + pub(crate) fn get_last_unburied_day(&self) -> u32 { + self.get_config_optional(ConfigKey::LastUnburiedDay) + .unwrap_or_default() + } + + pub(crate) fn set_last_unburied_day(&mut self, day: u32) -> Result<()> { + self.set_config(ConfigKey::LastUnburiedDay, &day) + } +} + +#[derive(Deserialize, PartialEq, Debug, Clone, Copy)] +#[serde(rename_all = "camelCase")] +pub enum SortKind { + #[serde(rename = "noteCrt")] + NoteCreation, + NoteMod, + #[serde(rename = "noteFld")] + NoteField, + #[serde(rename = "note")] + NoteType, + NoteTags, + CardMod, + CardReps, + CardDue, + CardEase, + CardLapses, + #[serde(rename = "cardIvl")] + CardInterval, + #[serde(rename = "deck")] + CardDeck, + #[serde(rename = "template")] + CardTemplate, +} + +impl Default for SortKind { + fn default() -> Self { + Self::NoteCreation + } +} + +// 2021 scheduler moves this into deck config +pub(crate) enum NewReviewMix { + Mix = 0, + ReviewsFirst = 1, + NewFirst = 2, +} + +impl Default for NewReviewMix { + fn default() -> Self { + NewReviewMix::Mix + } +} + +#[derive(PartialEq, Serialize_repr, Deserialize_repr, Clone, Copy)] +#[repr(u8)] +pub(crate) enum Weekday { + Sunday = 0, + Monday = 1, + Friday = 5, + Saturday = 6, +} + +#[cfg(test)] +mod test { + use super::SortKind; + use crate::collection::open_test_collection; + use crate::decks::DeckID; + + #[test] + fn defaults() { + let col = open_test_collection(); + assert_eq!(col.get_current_deck_id(), DeckID(1)); + assert_eq!(col.get_browser_sort_kind(), SortKind::NoteField); + } + + #[test] + fn get_set() { + let mut col = open_test_collection(); + + // missing key + assert_eq!(col.get_config_optional::, _>("test"), None); + + // normal retrieval + col.set_config("test", &vec![1, 2]).unwrap(); + assert_eq!( + col.get_config_optional::, _>("test"), + Some(vec![1, 2]) + ); + + // invalid type conversion + assert_eq!(col.get_config_optional::("test"), None,); + + // invalid json + col.storage + .db + .execute( + "update config set val=? where key='test'", + &[b"xx".as_ref()], + ) + .unwrap(); + assert_eq!(col.get_config_optional::("test"), None,); + } +} diff --git a/rslib/src/config/notetype.rs b/rslib/src/config/notetype.rs new file mode 100644 index 000000000..acda970da --- /dev/null +++ b/rslib/src/config/notetype.rs @@ -0,0 +1,52 @@ +// 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; + +/// Notetype config packed into a collection config key. This may change +/// frequently, and we want to avoid the potentially expensive notetype +/// write/sync. +#[derive(Debug, Clone, Copy, IntoStaticStr)] +#[strum(serialize_all = "camelCase")] +enum NoteTypeConfigKey { + #[strum(to_string = "lastDeck")] + LastDeckAddedTo, +} + +impl NoteTypeConfigKey { + fn for_notetype(self, ntid: NoteTypeID) -> String { + build_aux_notetype_key(ntid, <&'static str>::from(self)) + } +} + +impl Collection { + #[allow(dead_code)] + pub(crate) fn get_current_notetype_id(&self) -> Option { + self.get_config_optional(ConfigKey::CurrentNoteTypeID) + } + + pub(crate) fn set_current_notetype_id(&mut self, ntid: NoteTypeID) -> Result<()> { + self.set_config(ConfigKey::CurrentNoteTypeID, &ntid) + } + + pub(crate) fn clear_aux_config_for_notetype(&self, ntid: NoteTypeID) -> Result<()> { + self.remove_config_prefix(&build_aux_notetype_key(ntid, "")) + } + + pub(crate) fn get_last_deck_added_to_for_notetype(&self, id: NoteTypeID) -> Option { + let key = NoteTypeConfigKey::LastDeckAddedTo.for_notetype(id); + self.get_config_optional(key.as_str()) + } + + 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) + } +} + +fn build_aux_notetype_key(ntid: NoteTypeID, key: &str) -> String { + format!("_nt_{ntid}_{key}", ntid = ntid, key = key) +} diff --git a/rslib/src/config/schema11.rs b/rslib/src/config/schema11.rs new file mode 100644 index 000000000..e210a5d12 --- /dev/null +++ b/rslib/src/config/schema11.rs @@ -0,0 +1,28 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use serde_json::json; + +/// These items are expected to exist in schema 11. When adding +/// new config variables, you do not need to add them here - +/// just create an accessor function below with an appropriate +/// default on missing/invalid values instead. +pub(crate) fn schema11_config_as_string() -> String { + let obj = json!({ + "activeDecks": [1], + "curDeck": 1, + "newSpread": 0, + "collapseTime": 1200, + "timeLim": 0, + "estTimes": true, + "dueCounts": true, + "curModel": null, + "nextPos": 1, + "sortType": "noteFld", + "sortBackwards": false, + "addToCur": true, + "dayLearnFirst": false, + "schedVer": 1, + }); + serde_json::to_string(&obj).unwrap() +} diff --git a/rslib/src/config/string.rs b/rslib/src/config/string.rs new file mode 100644 index 000000000..d5aa918f8 --- /dev/null +++ b/rslib/src/config/string.rs @@ -0,0 +1,28 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::prelude::*; +use strum::IntoStaticStr; + +#[derive(Debug, Clone, Copy, IntoStaticStr)] +#[strum(serialize_all = "camelCase")] +pub enum StringKey { + SetDueBrowser, + SetDueReviewer, +} + +impl Collection { + pub(crate) fn get_string(&self, key: StringKey) -> String { + let default = match key { + StringKey::SetDueBrowser => "0", + StringKey::SetDueReviewer => "1", + // other => "", + }; + self.get_config_optional(key) + .unwrap_or_else(|| default.to_string()) + } + + 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 new file mode 100644 index 000000000..1aec6d075 --- /dev/null +++ b/rslib/src/config/undo.rs @@ -0,0 +1,111 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::ConfigEntry; +use crate::prelude::*; + +#[derive(Debug)] +pub(crate) enum UndoableConfigChange { + Added(Box), + Updated(Box), + Removed(Box), +} + +impl Collection { + pub(crate) fn undo_config_change(&mut self, change: UndoableConfigChange) -> Result<()> { + match change { + UndoableConfigChange::Added(entry) => self.remove_config_undoable(&entry.key), + UndoableConfigChange::Updated(entry) => { + let current = self + .storage + .get_config_entry(&entry.key)? + .ok_or_else(|| AnkiError::invalid_input("config disappeared"))?; + self.update_config_entry_undoable(entry, current) + } + UndoableConfigChange::Removed(entry) => self.add_config_entry_undoable(entry), + } + } + + 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) + } + } + + pub(super) fn remove_config_undoable(&mut self, key: &str) -> Result<()> { + if let Some(current) = self.storage.get_config_entry(key)? { + self.save_undo(UndoableConfigChange::Removed(current)); + self.storage.remove_config(key)?; + } + + Ok(()) + } + + fn add_config_entry_undoable(&mut self, entry: Box) -> Result<()> { + self.storage.set_config_entry(&entry)?; + self.save_undo(UndoableConfigChange::Added(entry)); + Ok(()) + } + + fn update_config_entry_undoable( + &mut self, + entry: Box, + original: Box, + ) -> Result<()> { + if entry.value != original.value { + self.save_undo(UndoableConfigChange::Updated(original)); + self.storage.set_config_entry(&entry)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::collection::open_test_collection; + + #[test] + fn undo() -> Result<()> { + let mut col = open_test_collection(); + // the op kind doesn't matter, we just need undo enabled + let op = Some(UndoableOpKind::Bury); + // test key + let key = BoolKey::NormalizeNoteText; + + // not set by default, but defaults to true + assert_eq!(col.get_bool(key), true); + + // first set adds the key + col.transact(op, |col| col.set_bool(key, false))?; + assert_eq!(col.get_bool(key), false); + + // mutate it twice + col.transact(op, |col| col.set_bool(key, true))?; + assert_eq!(col.get_bool(key), true); + col.transact(op, |col| col.set_bool(key, false))?; + assert_eq!(col.get_bool(key), false); + + // when we remove it, it goes back to its default + col.transact(op, |col| col.remove_config(key))?; + assert_eq!(col.get_bool(key), true); + + // undo the removal + col.undo()?; + assert_eq!(col.get_bool(key), false); + + // undo the mutations + col.undo()?; + assert_eq!(col.get_bool(key), true); + col.undo()?; + assert_eq!(col.get_bool(key), false); + + // and undo the initial add + col.undo()?; + assert_eq!(col.get_bool(key), true); + + Ok(()) + } +} diff --git a/rslib/src/dbcheck.rs b/rslib/src/dbcheck.rs index 95b1bbe24..d5668d3a0 100644 --- a/rslib/src/dbcheck.rs +++ b/rslib/src/dbcheck.rs @@ -230,12 +230,12 @@ impl Collection { F: FnMut(DatabaseCheckProgress, bool), { let nids_by_notetype = self.storage.all_note_ids_by_notetype()?; - let norm = self.normalize_note_text(); + let norm = self.get_bool(BoolKey::NormalizeNoteText); let usn = self.usn()?; let stamp = TimestampMillis::now(); let expanded_tags = self.storage.expanded_tags()?; - self.storage.clear_tags()?; + self.storage.clear_all_tags()?; let total_notes = self.storage.total_notes()?; let mut checked_notes = 0; @@ -247,7 +247,7 @@ impl Collection { None => { let first_note = self.storage.get_note(group.peek().unwrap().1)?.unwrap(); out.notetypes_recovered += 1; - self.recover_notetype(stamp, first_note.fields.len(), ntid)? + self.recover_notetype(stamp, first_note.fields().len(), ntid)? } Some(nt) => nt, }; @@ -264,6 +264,7 @@ impl Collection { checked_notes += 1; let mut note = self.get_note_fixing_invalid_utf8(nid, out)?; + let original = note.clone(); let cards = self.storage.existing_cards_for_note(nid)?; @@ -271,7 +272,7 @@ impl Collection { out.templates_missing += self.remove_cards_without_template(&nt, &cards)?; // fix fields - if note.fields.len() != nt.fields.len() { + if note.fields().len() != nt.fields.len() { note.fix_field_count(&nt); note.tags.push("db-check".into()); out.field_count_mismatch += 1; @@ -282,7 +283,7 @@ impl Collection { // write note, updating tags and generating missing cards let ctx = genctx.get_or_insert_with(|| CardGenContext::new(&nt, usn)); - self.update_note_inner_generating_cards(&ctx, &mut note, false, norm)?; + self.update_note_inner_generating_cards(&ctx, &mut note, &original, false, norm)?; } } @@ -408,7 +409,7 @@ impl Collection { Ok(()) } - fn update_next_new_position(&self) -> Result<()> { + fn update_next_new_position(&mut self) -> Result<()> { let pos = self.storage.max_new_card_position().unwrap_or(0); self.set_next_card_position(pos) } @@ -575,7 +576,7 @@ mod test { } ); let note = col.storage.get_note(note.id)?.unwrap(); - assert_eq!(¬e.fields, &["a", "b; c; d"]); + assert_eq!(¬e.fields()[..], &["a", "b; c; d"]); // missing fields get filled with blanks col.storage @@ -590,7 +591,7 @@ mod test { } ); let note = col.storage.get_note(note.id)?.unwrap(); - assert_eq!(¬e.fields, &["a", ""]); + assert_eq!(¬e.fields()[..], &["a", ""]); Ok(()) } diff --git a/rslib/src/deckconf/mod.rs b/rslib/src/deckconf/mod.rs index f15203309..23c13abeb 100644 --- a/rslib/src/deckconf/mod.rs +++ b/rslib/src/deckconf/mod.rs @@ -11,7 +11,7 @@ use crate::{ }; pub use crate::backend_proto::{ - deck_config_inner::{LeechAction, NewCardOrder}, + deck_config_inner::{LeechAction, NewCardOrder, ReviewCardOrder, ReviewMix}, DeckConfigInner, }; pub use schema11::{DeckConfSchema11, NewCardOrderSchema11}; @@ -41,14 +41,9 @@ impl Default for DeckConf { inner: DeckConfigInner { learn_steps: vec![1.0, 10.0], relearn_steps: vec![10.0], - disable_autoplay: false, - cap_answer_time_to_secs: 60, - visible_timer_secs: 0, - skip_question_when_replaying_answer: false, new_per_day: 20, reviews_per_day: 200, - bury_new: false, - bury_reviews: false, + new_per_day_minimum: 0, initial_ease: 2.5, easy_multiplier: 1.3, hard_multiplier: 1.2, @@ -59,8 +54,17 @@ impl Default for DeckConf { graduating_interval_good: 1, graduating_interval_easy: 4, new_card_order: NewCardOrder::Due as i32, + review_order: ReviewCardOrder::ShuffledByDay as i32, + new_mix: ReviewMix::MixWithReviews as i32, + interday_learning_mix: ReviewMix::MixWithReviews as i32, leech_action: LeechAction::TagOnly as i32, leech_threshold: 8, + disable_autoplay: false, + cap_answer_time_to_secs: 60, + visible_timer_secs: 0, + skip_question_when_replaying_answer: false, + bury_new: false, + bury_reviews: false, other: vec![], }, } diff --git a/rslib/src/deckconf/schema11.rs b/rslib/src/deckconf/schema11.rs index 057dba532..7ed5fedab 100644 --- a/rslib/src/deckconf/schema11.rs +++ b/rslib/src/deckconf/schema11.rs @@ -32,6 +32,19 @@ pub struct DeckConfSchema11 { pub(crate) lapse: LapseConfSchema11, #[serde(rename = "dyn", default, deserialize_with = "default_on_invalid")] dynamic: bool, + + // 2021 scheduler options: these were not in schema 11, but we need to persist them + // so the settings are not lost on upgrade/downgrade. + // NOTE: if adding new ones, make sure to update clear_other_duplicates() + #[serde(default)] + new_mix: i32, + #[serde(default)] + new_per_day_minimum: u32, + #[serde(default)] + interday_learning_mix: i32, + #[serde(default)] + review_order: i32, + #[serde(flatten)] other: HashMap, } @@ -191,6 +204,10 @@ impl Default for DeckConfSchema11 { rev: Default::default(), lapse: Default::default(), other: Default::default(), + new_mix: 0, + new_per_day_minimum: 0, + interday_learning_mix: 0, + review_order: 0, } } } @@ -229,14 +246,9 @@ impl From for DeckConf { inner: DeckConfigInner { learn_steps: c.new.delays, relearn_steps: c.lapse.delays, - disable_autoplay: !c.autoplay, - cap_answer_time_to_secs: c.max_taken.max(0) as u32, - visible_timer_secs: c.timer as u32, - skip_question_when_replaying_answer: !c.replayq, new_per_day: c.new.per_day, reviews_per_day: c.rev.per_day, - bury_new: c.new.bury, - bury_reviews: c.rev.bury, + new_per_day_minimum: c.new_per_day_minimum, initial_ease: (c.new.initial_factor as f32) / 1000.0, easy_multiplier: c.rev.ease4, hard_multiplier: c.rev.hard_factor, @@ -250,15 +262,24 @@ impl From for DeckConf { NewCardOrderSchema11::Random => NewCardOrder::Random, NewCardOrderSchema11::Due => NewCardOrder::Due, } as i32, + review_order: c.review_order, + new_mix: c.new_mix, + interday_learning_mix: c.interday_learning_mix, leech_action: c.lapse.leech_action as i32, leech_threshold: c.lapse.leech_fails, + disable_autoplay: !c.autoplay, + cap_answer_time_to_secs: c.max_taken.max(0) as u32, + visible_timer_secs: c.timer as u32, + skip_question_when_replaying_answer: !c.replayq, + bury_new: c.new.bury, + bury_reviews: c.rev.bury, other: other_bytes, }, } } } -// schema 15 -> schema 11 +// latest schema -> schema 11 impl From for DeckConfSchema11 { fn from(c: DeckConf) -> DeckConfSchema11 { // split extra json up @@ -270,6 +291,7 @@ impl From for DeckConfSchema11 { top_other = Default::default(); } else { top_other = serde_json::from_slice(&c.inner.other).unwrap_or_default(); + clear_other_duplicates(&mut top_other); if let Some(new) = top_other.remove("new") { let val: HashMap = serde_json::from_value(new).unwrap_or_default(); new_other = val; @@ -332,6 +354,28 @@ impl From for DeckConfSchema11 { other: lapse_other, }, other: top_other, + new_mix: i.new_mix, + new_per_day_minimum: i.new_per_day_minimum, + interday_learning_mix: i.interday_learning_mix, + review_order: i.review_order, } } } + +fn clear_other_duplicates(top_other: &mut HashMap) { + // Older clients may have received keys from a newer client when + // syncing, which get bundled into `other`. If they then upgrade, then + // downgrade their collection to schema11, serde will serialize the + // new default keys, but then add them again from `other`, leading + // to the keys being duplicated in the resulting json - which older + // clients then can't read. So we need to strip out any new keys we + // add. + for key in &[ + "newMix", + "newPerDayMinimum", + "interdayLearningMix", + "reviewOrder", + ] { + top_other.remove(*key); + } +} diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index ffd0b6a6f..0aeccf8a0 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -1,6 +1,11 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +mod counts; +mod schema11; +mod tree; +pub(crate) mod undo; + pub use crate::backend_proto::{ deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto, DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck, @@ -16,9 +21,6 @@ use crate::{ timestamp::TimestampSecs, types::Usn, }; -mod counts; -mod schema11; -mod tree; pub(crate) use counts::DueCounts; pub use schema11::DeckSchema11; use std::{borrow::Cow, sync::Arc}; @@ -251,7 +253,7 @@ impl Collection { } /// Normalize deck name and rename if not unique. Bumps mtime and usn if - /// deck was modified. + /// name was changed, but otherwise leaves it the same. fn prepare_deck_for_update(&mut self, deck: &mut Deck, usn: Usn) -> Result<()> { if let Cow::Owned(name) = normalize_native_name(&deck.name) { deck.name = name; @@ -268,18 +270,28 @@ impl Collection { self.transact(None, |col| { let usn = col.usn()?; + col.prepare_deck_for_update(deck, usn)?; deck.set_modified(usn); if deck.id.0 == 0 { - col.prepare_deck_for_update(deck, usn)?; + // TODO: undo support col.match_or_create_parents(deck, usn)?; col.storage.add_deck(deck) } else if let Some(existing_deck) = col.storage.get_deck(deck.id)? { - if existing_deck.name != deck.name { - col.update_renamed_deck(existing_deck, deck, usn) - } else { - col.add_or_update_single_deck(deck, usn) + let name_changed = existing_deck.name != deck.name; + if name_changed { + // match closest parent name + col.match_or_create_parents(deck, usn)?; + // rename children + col.rename_child_decks(&existing_deck, &deck.name, usn)?; } + col.update_single_deck_undoable(deck, &existing_deck)?; + if name_changed { + // after updating, we need to ensure all grandparents exist, which may not be the case + // in the parent->child case + col.create_missing_parents(&deck.name, usn)?; + } + Ok(()) } else { Err(AnkiError::invalid_input("updating non-existent deck")) } @@ -289,10 +301,15 @@ impl Collection { /// Add/update a single deck when syncing/importing. Ensures name is unique /// & normalized, but does not check parents/children or update mtime /// (unless the name was changed). Caller must set up transaction. - pub(crate) fn add_or_update_single_deck(&mut self, deck: &mut Deck, usn: Usn) -> Result<()> { + /// TODO: undo support + pub(crate) fn add_or_update_single_deck_with_existing_id( + &mut self, + deck: &mut Deck, + usn: Usn, + ) -> Result<()> { self.state.deck_cache.clear(); self.prepare_deck_for_update(deck, usn)?; - self.storage.update_deck(deck) + self.storage.add_or_update_deck_with_existing_id(deck) } pub(crate) fn ensure_deck_name_unique(&self, deck: &mut Deck, usn: Usn) -> Result<()> { @@ -316,7 +333,7 @@ impl Collection { deck.id = did; deck.name = format!("recovered{}", did); deck.set_modified(usn); - self.add_or_update_single_deck(&mut deck, usn) + self.add_or_update_single_deck_with_existing_id(&mut deck, usn) } pub fn get_or_create_normal_deck(&mut self, human_name: &str) -> Result { @@ -331,37 +348,18 @@ impl Collection { } } - fn update_renamed_deck(&mut self, existing: Deck, updated: &mut Deck, usn: Usn) -> Result<()> { - self.state.deck_cache.clear(); - // ensure name normalized - if let Cow::Owned(name) = normalize_native_name(&updated.name) { - updated.name = name; - } - // match closest parent name - self.match_or_create_parents(updated, usn)?; - // ensure new name is unique - self.ensure_deck_name_unique(updated, usn)?; - // rename children - self.rename_child_decks(&existing, &updated.name, usn)?; - // save deck - updated.set_modified(usn); - self.storage.update_deck(updated)?; - // after updating, we need to ensure all grandparents exist, which may not be the case - // in the parent->child case - self.create_missing_parents(&updated.name, usn) - } - fn rename_child_decks(&mut self, old: &Deck, new_name: &str, usn: Usn) -> Result<()> { let children = self.storage.child_decks(old)?; let old_component_count = old.name.matches('\x1f').count() + 1; for mut child in children { + let original = child.clone(); let child_components: Vec<_> = child.name.split('\x1f').collect(); let child_only = &child_components[old_component_count..]; let new_name = format!("{}\x1f{}", new_name, child_only.join("\x1f")); child.name = new_name; child.set_modified(usn); - self.storage.update_deck(&child)?; + self.update_single_deck_undoable(&mut child, &original)?; } Ok(()) @@ -471,12 +469,13 @@ impl Collection { DeckKind::Normal(_) => self.delete_all_cards_in_normal_deck(deck.id)?, DeckKind::Filtered(_) => self.return_all_cards_in_filtered_deck(deck.id)?, } + self.clear_aux_config_for_deck(deck.id)?; if deck.id.0 == 1 { let mut deck = deck.to_owned(); // fixme: separate key deck.name = self.i18n.tr(TR::DeckConfigDefaultName).into(); deck.set_modified(usn); - self.add_or_update_single_deck(&mut deck, usn)?; + self.add_or_update_single_deck_with_existing_id(&mut deck, usn)?; } else { self.storage.remove_deck(deck.id)?; self.storage.add_deck_grave(deck.id, usn)?; @@ -588,10 +587,11 @@ impl Collection { where F: FnOnce(&mut DeckCommon), { + let original = deck.clone(); deck.reset_stats_if_day_changed(today); mutator(&mut deck.common); deck.set_modified(usn); - self.add_or_update_single_deck(deck, usn) + self.update_single_deck_undoable(deck, &original) } pub fn drag_drop_decks( @@ -606,6 +606,9 @@ impl Collection { let mut target_name = None; if let Some(target) = target { if let Some(target) = col.storage.get_deck(target)? { + if target.is_filtered() { + return Err(AnkiError::DeckIsFiltered); + } target_deck = target; target_name = Some(target_deck.name.as_str()); } diff --git a/rslib/src/decks/tree.rs b/rslib/src/decks/tree.rs index 4280dd647..683e18f9b 100644 --- a/rslib/src/decks/tree.rs +++ b/rslib/src/decks/tree.rs @@ -5,7 +5,7 @@ use super::{Deck, DeckKind, DueCounts}; use crate::{ backend_proto::DeckTreeNode, collection::Collection, - config::SchedulerVersion, + config::{BoolKey, SchedulerVersion}, deckconf::{DeckConf, DeckConfID}, decks::DeckID, err::Result, @@ -123,12 +123,11 @@ fn apply_limits( node.review_count = (node.review_count + child_rev_total).min(remaining_rev); } -/// Apply parent new limits to children, and add child counts to parents. -/// Unlike v1, reviews are not capped by their parents, and we return the -/// uncapped review amount to add to the parent. This is a bit of a hack, and -/// just tides us over until the v2 queue building code can be reworked. +/// Apply parent new limits to children, and add child counts to parents. Unlike +/// v1 and the 2021 scheduler, reviews are not capped by their parents, and we +/// return the uncapped review amount to add to the parent. /// Counts are (new, review). -fn apply_limits_v2( +fn apply_limits_v2_old( node: &mut DeckTreeNode, today: u32, decks: &HashMap, @@ -148,7 +147,7 @@ fn apply_limits_v2( let mut child_rev_total = 0; for child in &mut node.children { child_rev_total += - apply_limits_v2(child, today, decks, dconf, (remaining_new, remaining_rev)); + apply_limits_v2_old(child, today, decks, dconf, (remaining_new, remaining_rev)); child_new_total += child.new_count; // no limit on learning cards node.learn_count += child.learn_count; @@ -283,8 +282,10 @@ impl Collection { let counts = self.due_counts(days_elapsed, learn_cutoff, limit)?; let dconf = self.storage.get_deck_config_map()?; add_counts(&mut tree, &counts); - if self.scheduler_version() == SchedulerVersion::V2 { - apply_limits_v2( + if self.scheduler_version() == SchedulerVersion::V2 + && !self.get_bool(BoolKey::Sched2021) + { + apply_limits_v2_old( &mut tree, days_elapsed, &decks_map, @@ -390,7 +391,7 @@ mod test { // add some new cards let nt = col.get_notetype_by_name("Cloze")?.unwrap(); let mut note = nt.new_note(); - note.fields[0] = "{{c1::}} {{c2::}} {{c3::}} {{c4::}}".into(); + note.set_field(0, "{{c1::}} {{c2::}} {{c3::}} {{c4::}}")?; col.add_note(&mut note, child_deck.id)?; let tree = col.deck_tree(Some(TimestampSecs::now()), None)?; diff --git a/rslib/src/decks/undo.rs b/rslib/src/decks/undo.rs new file mode 100644 index 000000000..3251a0a91 --- /dev/null +++ b/rslib/src/decks/undo.rs @@ -0,0 +1,37 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::prelude::*; + +#[derive(Debug)] + +pub(crate) enum UndoableDeckChange { + Updated(Box), +} + +impl Collection { + pub(crate) fn undo_deck_change(&mut self, change: UndoableDeckChange) -> Result<()> { + match change { + UndoableDeckChange::Updated(mut deck) => { + let current = self + .storage + .get_deck(deck.id)? + .ok_or_else(|| AnkiError::invalid_input("deck disappeared"))?; + self.update_single_deck_undoable(&mut *deck, ¤t) + } + } + } + + /// Update an individual, existing deck. Caller is responsible for ensuring deck + /// is normalized, matches parents, is not a duplicate name, and bumping mtime. + /// Clears deck cache. + pub(super) fn update_single_deck_undoable( + &mut self, + deck: &mut Deck, + original: &Deck, + ) -> Result<()> { + self.state.deck_cache.clear(); + self.save_undo(UndoableDeckChange::Updated(Box::new(original.clone()))); + self.storage.update_deck(deck) + } +} diff --git a/rslib/src/err.rs b/rslib/src/err.rs index 015750197..0849adb59 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -208,6 +208,7 @@ impl AnkiError { } } AnkiError::ParseNumError => i18n.tr(TR::ErrorsParseNumberFail).into(), + AnkiError::DeckIsFiltered => i18n.tr(TR::ErrorsFilteredParentDeck).into(), _ => format!("{:?}", self), } } diff --git a/rslib/src/filtered.rs b/rslib/src/filtered.rs index eecf7731d..37bfae449 100644 --- a/rslib/src/filtered.rs +++ b/rslib/src/filtered.rs @@ -18,6 +18,22 @@ use crate::{ }; impl Card { + pub(crate) fn restore_queue_from_type(&mut self) { + self.queue = match self.ctype { + CardType::Learn | CardType::Relearn => { + if self.due > 1_000_000_000 { + // unix timestamp + CardQueue::Learn + } else { + // day number + CardQueue::DayLearn + } + } + CardType::New => CardQueue::New, + CardType::Review => CardQueue::Review, + } + } + pub(crate) fn move_into_filtered_deck(&mut self, ctx: &DeckFilterContext, position: i32) { // filtered and v1 learning cards are excluded, so odue should be guaranteed to be zero if self.original_due != 0 { @@ -64,17 +80,6 @@ impl Card { } } - /// Returns original_due if set, else due. - /// original_due will be set in filtered decks, and in relearning in - /// the old scheduler. - pub(crate) fn original_or_current_due(&self) -> i32 { - if self.original_due > 0 { - self.original_due - } else { - self.due - } - } - pub(crate) fn original_or_current_deck_id(&self) -> DeckID { if self.original_deck_id.0 > 0 { self.original_deck_id @@ -116,19 +121,7 @@ impl Card { } if (self.queue as i8) >= 0 { - self.queue = match self.ctype { - CardType::Learn | CardType::Relearn => { - if self.due > 1_000_000_000 { - // unix timestamp - CardQueue::Learn - } else { - // day number - CardQueue::DayLearn - } - } - CardType::New => CardQueue::New, - CardType::Review => CardQueue::Review, - } + self.restore_queue_from_type(); } } } @@ -191,7 +184,7 @@ impl Collection { if let Some(mut card) = self.storage.get_card(*cid)? { let original = card.clone(); card.remove_from_filtered_deck_restoring_queue(sched); - self.update_card(&mut card, &original, usn)?; + self.update_card_inner(&mut card, &original, usn)?; } } Ok(()) @@ -256,7 +249,7 @@ impl Collection { for mut card in self.storage.all_searched_cards_in_search_order()? { let original = card.clone(); card.move_into_filtered_deck(ctx, position); - self.update_card(&mut card, &original, ctx.usn)?; + self.update_card_inner(&mut card, &original, ctx.usn)?; position += 1; } diff --git a/rslib/src/findreplace.rs b/rslib/src/findreplace.rs index 43ee636d4..8d97eb1ec 100644 --- a/rslib/src/findreplace.rs +++ b/rslib/src/findreplace.rs @@ -5,6 +5,7 @@ use crate::{ collection::Collection, err::{AnkiError, Result}, notes::{NoteID, TransformNoteOutput}, + prelude::*, text::normalize_to_nfc, }; use regex::Regex; @@ -46,7 +47,7 @@ impl Collection { field_name: Option, ) -> Result { self.transact(None, |col| { - let norm = col.normalize_note_text(); + let norm = col.get_bool(BoolKey::NormalizeNoteText); let search = if norm { normalize_to_nfc(search_re) } else { @@ -70,7 +71,7 @@ impl Collection { match field_ord { None => { // all fields - for txt in &mut note.fields { + for txt in note.fields_mut() { if let Cow::Owned(otxt) = ctx.replace_text(txt) { changed = true; *txt = otxt; @@ -79,7 +80,7 @@ impl Collection { } Some(ord) => { // single field - if let Some(txt) = note.fields.get_mut(ord) { + if let Some(txt) = note.fields_mut().get_mut(ord) { if let Cow::Owned(otxt) = ctx.replace_text(txt) { changed = true; *txt = otxt; @@ -108,13 +109,13 @@ mod test { let nt = col.get_notetype_by_name("Basic")?.unwrap(); let mut note = nt.new_note(); - note.fields[0] = "one aaa".into(); - note.fields[1] = "two aaa".into(); + note.set_field(0, "one aaa")?; + note.set_field(1, "two aaa")?; col.add_note(&mut note, DeckID(1))?; let nt = col.get_notetype_by_name("Cloze")?.unwrap(); let mut note2 = nt.new_note(); - note2.fields[0] = "three aaa".into(); + note2.set_field(0, "three aaa")?; col.add_note(&mut note2, DeckID(1))?; let nids = col.search_notes("")?; @@ -123,10 +124,10 @@ mod test { let note = col.storage.get_note(note.id)?.unwrap(); // but the update should be limited to the specified field when it was available - assert_eq!(¬e.fields, &["one BBB", "two BBB"]); + assert_eq!(¬e.fields()[..], &["one BBB", "two BBB"]); let note2 = col.storage.get_note(note2.id)?.unwrap(); - assert_eq!(¬e2.fields, &["three BBB", ""]); + assert_eq!(¬e2.fields()[..], &["three BBB", ""]); assert_eq!( col.storage.field_names_for_notes(&nids)?, @@ -144,7 +145,7 @@ mod test { let note = col.storage.get_note(note.id)?.unwrap(); // but the update should be limited to the specified field when it was available - assert_eq!(¬e.fields, &["one ccc", "two BBB"]); + assert_eq!(¬e.fields()[..], &["one ccc", "two BBB"]); Ok(()) } diff --git a/rslib/src/lib.rs b/rslib/src/lib.rs index 59ba18842..c3a138ad0 100644 --- a/rslib/src/lib.rs +++ b/rslib/src/lib.rs @@ -3,6 +3,7 @@ #![deny(unused_must_use)] +pub mod adding; pub mod backend; mod backend_proto; pub mod card; diff --git a/rslib/src/notes.rs b/rslib/src/notes/mod.rs similarity index 75% rename from rslib/src/notes.rs rename to rslib/src/notes/mod.rs index f60e61d55..18dee8f3f 100644 --- a/rslib/src/notes.rs +++ b/rslib/src/notes/mod.rs @@ -1,14 +1,16 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +pub(crate) mod undo; + use crate::backend_proto::note_is_duplicate_or_empty_out::State as DuplicateState; use crate::{ backend_proto as pb, - collection::Collection, decks::DeckID, define_newtype, err::{AnkiError, Result}, notetype::{CardGenContext, NoteField, NoteType, NoteTypeID}, + prelude::*, template::field_is_empty, text::{ensure_string_in_nfc, normalize_to_nfc, strip_html_preserving_media_filenames}, timestamp::TimestampSecs, @@ -40,7 +42,7 @@ pub struct Note { pub mtime: TimestampSecs, pub usn: Usn, pub tags: Vec, - pub(crate) fields: Vec, + fields: Vec, pub(crate) sort_field: Option, pub(crate) checksum: Option, } @@ -60,10 +62,46 @@ impl Note { } } + #[allow(clippy::clippy::too_many_arguments)] + pub(crate) fn new_from_storage( + id: NoteID, + guid: String, + notetype_id: NoteTypeID, + mtime: TimestampSecs, + usn: Usn, + tags: Vec, + fields: Vec, + sort_field: Option, + checksum: Option, + ) -> Self { + Self { + id, + guid, + notetype_id, + mtime, + usn, + tags, + fields, + sort_field, + checksum, + } + } + pub fn fields(&self) -> &Vec { &self.fields } + pub(crate) fn fields_mut(&mut self) -> &mut Vec { + self.mark_dirty(); + &mut self.fields + } + + // Ensure we get an error if caller forgets to call prepare_for_update(). + fn mark_dirty(&mut self) { + self.sort_field = None; + self.checksum = None; + } + pub fn set_field(&mut self, idx: usize, text: impl Into) -> Result<()> { if idx >= self.fields.len() { return Err(AnkiError::invalid_input( @@ -72,6 +110,7 @@ impl Note { } self.fields[idx] = text.into(); + self.mark_dirty(); Ok(()) } @@ -258,7 +297,7 @@ fn invalid_char_for_field(c: char) -> bool { } impl Collection { - fn canonify_note_tags(&self, note: &mut Note, usn: Usn) -> Result<()> { + fn canonify_note_tags(&mut self, note: &mut Note, usn: Usn) -> Result<()> { if !note.tags.is_empty() { let tags = std::mem::replace(&mut note.tags, vec![]); note.tags = self.canonify_tags(tags, usn)?.0; @@ -267,12 +306,12 @@ impl Collection { } pub fn add_note(&mut self, note: &mut Note, did: DeckID) -> Result<()> { - self.transact(None, |col| { + self.transact(Some(UndoableOpKind::AddNote), |col| { let nt = col .get_notetype(note.notetype_id)? .ok_or_else(|| AnkiError::invalid_input("missing note type"))?; let ctx = CardGenContext::new(&nt, col.usn()?); - let norm = col.normalize_note_text(); + let norm = col.get_bool(BoolKey::NormalizeNoteText); col.add_note_inner(&ctx, note, did, norm) }) } @@ -287,27 +326,36 @@ impl Collection { self.canonify_note_tags(note, ctx.usn)?; note.prepare_for_update(&ctx.notetype, normalize_text)?; note.set_modified(ctx.usn); - self.storage.add_note(note)?; - self.generate_cards_for_new_note(ctx, note, did) + self.add_note_only_undoable(note)?; + self.generate_cards_for_new_note(ctx, note, did)?; + self.set_last_deck_for_notetype(note.notetype_id, did)?; + self.set_last_notetype_for_deck(did, note.notetype_id)?; + self.set_current_notetype_id(note.notetype_id) } - pub fn update_note(&mut self, note: &mut Note) -> Result<()> { - if let Some(existing_note) = self.storage.get_note(note.id)? { - if &existing_note == note { - // nothing to do - return Ok(()); - } - } else { - return Err(AnkiError::NotFound); + #[cfg(test)] + pub(crate) fn update_note(&mut self, note: &mut Note) -> Result<()> { + self.update_note_with_op(note, Some(UndoableOpKind::UpdateNote)) + } + + pub(crate) fn update_note_with_op( + &mut self, + note: &mut Note, + op: Option, + ) -> Result<()> { + let mut existing_note = self.storage.get_note(note.id)?.ok_or(AnkiError::NotFound)?; + if !note_differs_from_db(&mut existing_note, note) { + // nothing to do + return Ok(()); } - self.transact(None, |col| { + self.transact(op, |col| { let nt = col .get_notetype(note.notetype_id)? .ok_or_else(|| AnkiError::invalid_input("missing note type"))?; let ctx = CardGenContext::new(&nt, col.usn()?); - let norm = col.normalize_note_text(); - col.update_note_inner_generating_cards(&ctx, note, true, norm) + let norm = col.get_bool(BoolKey::NormalizeNoteText); + col.update_note_inner_generating_cards(&ctx, note, &existing_note, true, norm) }) } @@ -315,11 +363,13 @@ impl Collection { &mut self, ctx: &CardGenContext, note: &mut Note, + original: &Note, mark_note_modified: bool, normalize_text: bool, ) -> Result<()> { self.update_note_inner_without_cards( note, + original, ctx.notetype, ctx.usn, mark_note_modified, @@ -331,6 +381,7 @@ impl Collection { pub(crate) fn update_note_inner_without_cards( &mut self, note: &mut Note, + original: &Note, nt: &NoteType, usn: Usn, mark_note_modified: bool, @@ -341,31 +392,20 @@ impl Collection { if mark_note_modified { note.set_modified(usn); } - self.storage.update_note(note) - } - - /// Remove a note. Cards must already have been deleted. - pub(crate) fn remove_note_only(&mut self, nid: NoteID, usn: Usn) -> Result<()> { - if let Some(_note) = self.storage.get_note(nid)? { - // fixme: undo - self.storage.remove_note(nid)?; - self.storage.add_note_grave(nid, usn)?; - } - Ok(()) + self.update_note_undoable(note, original, true) } /// Remove provided notes, and any cards that use them. pub(crate) fn remove_notes(&mut self, nids: &[NoteID]) -> Result<()> { let usn = self.usn()?; - self.transact(None, |col| { + self.transact(Some(UndoableOpKind::RemoveNote), |col| { for nid in nids { let nid = *nid; if let Some(_existing_note) = col.storage.get_note(nid)? { - // fixme: undo for card in col.storage.all_cards_of_note(nid)? { - col.remove_card_only(card, usn)?; + col.remove_card_and_add_grave_undoable(card, usn)?; } - col.remove_note_only(nid, usn)?; + col.remove_note_only_undoable(nid, usn)?; } } @@ -400,7 +440,7 @@ impl Collection { F: FnMut(&mut Note, &NoteType) -> Result, { let nids_by_notetype = self.storage.note_ids_by_notetype(nids)?; - let norm = self.normalize_note_text(); + let norm = self.get_bool(BoolKey::NormalizeNoteText); let mut changed_notes = 0; let usn = self.usn()?; @@ -413,6 +453,7 @@ impl Collection { for (_, nid) in group { // grab the note and transform it let mut note = self.storage.get_note(nid)?.unwrap(); + let original = note.clone(); let out = transformer(&mut note, &nt)?; if !out.changed { continue; @@ -423,12 +464,14 @@ impl Collection { self.update_note_inner_generating_cards( &ctx, &mut note, + &original, out.mark_modified, norm, )?; } else { self.update_note_inner_without_cards( &mut note, + &original, &nt, usn, out.mark_modified, @@ -445,7 +488,7 @@ impl Collection { pub(crate) fn note_is_duplicate_or_empty(&self, note: &Note) -> Result { if let Some(field1) = note.fields.get(0) { - let field1 = if self.normalize_note_text() { + let field1 = if self.get_bool(BoolKey::NormalizeNoteText) { normalize_to_nfc(field1) } else { field1.into() @@ -501,11 +544,26 @@ impl Collection { } } +/// The existing note pulled from the DB will have sfld and csum set, but the +/// note we receive from the frontend won't. Temporarily zero them out and +/// compare, then restore them again. +/// Also set mtime to existing, since the frontend may have a stale mtime, and +/// we'll bump it as we save in any case. +fn note_differs_from_db(existing_note: &mut Note, note: &mut Note) -> bool { + let sort_field = existing_note.sort_field.take(); + let checksum = existing_note.checksum.take(); + note.mtime = existing_note.mtime; + let notes_differ = existing_note != note; + existing_note.sort_field = sort_field; + existing_note.checksum = checksum; + notes_differ +} + #[cfg(test)] mod test { use super::{anki_base91, field_checksum}; use crate::{ - collection::open_test_collection, config::ConfigKey, decks::DeckID, err::Result, + collection::open_test_collection, config::BoolKey, decks::DeckID, err::Result, prelude::*, search::SortMode, }; @@ -593,8 +651,7 @@ mod test { // if normalization turned off, note text is entered as-is let mut note = nt.new_note(); note.fields[0] = "\u{fa47}".into(); - col.set_config(ConfigKey::NormalizeNoteText, &false) - .unwrap(); + col.set_config(BoolKey::NormalizeNoteText, &false).unwrap(); col.add_note(&mut note, DeckID(1))?; assert_eq!(note.fields[0], "\u{fa47}"); // normalized searches won't match @@ -604,4 +661,74 @@ mod test { Ok(()) } + + #[test] + fn undo() -> Result<()> { + let mut col = open_test_collection(); + let nt = col + .get_notetype_by_name("basic (and reversed card)")? + .unwrap(); + + let assert_initial = |col: &mut Collection| -> Result<()> { + assert_eq!(col.search_notes("")?.len(), 0); + assert_eq!(col.search_cards("", SortMode::NoOrder)?.len(), 0); + assert_eq!( + col.storage.db_scalar::("select count() from graves")?, + 0 + ); + assert_eq!(col.next_card()?.is_some(), false); + Ok(()) + }; + + let assert_after_add = |col: &mut Collection| -> Result<()> { + assert_eq!(col.search_notes("")?.len(), 1); + assert_eq!(col.search_cards("", SortMode::NoOrder)?.len(), 2); + assert_eq!( + col.storage.db_scalar::("select count() from graves")?, + 0 + ); + assert_eq!(col.next_card()?.is_some(), true); + Ok(()) + }; + + assert_initial(&mut col)?; + + let mut note = nt.new_note(); + note.set_field(0, "a")?; + note.set_field(1, "b")?; + + col.add_note(&mut note, DeckID(1)).unwrap(); + + assert_after_add(&mut col)?; + col.undo()?; + assert_initial(&mut col)?; + col.redo()?; + assert_after_add(&mut col)?; + col.undo()?; + assert_initial(&mut col)?; + + let assert_after_remove = |col: &mut Collection| -> Result<()> { + assert_eq!(col.search_notes("")?.len(), 0); + assert_eq!(col.search_cards("", SortMode::NoOrder)?.len(), 0); + // 1 note + 2 cards + assert_eq!( + col.storage.db_scalar::("select count() from graves")?, + 3 + ); + assert_eq!(col.next_card()?.is_some(), false); + Ok(()) + }; + + col.redo()?; + assert_after_add(&mut col)?; + let nids = col.search_notes("")?; + col.remove_notes(&nids)?; + assert_after_remove(&mut col)?; + col.undo()?; + assert_after_add(&mut col)?; + col.redo()?; + assert_after_remove(&mut col)?; + + Ok(()) + } } diff --git a/rslib/src/notes/undo.rs b/rslib/src/notes/undo.rs new file mode 100644 index 000000000..02bec7443 --- /dev/null +++ b/rslib/src/notes/undo.rs @@ -0,0 +1,107 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{prelude::*, undo::UndoableChange}; + +#[derive(Debug)] +pub(crate) enum UndoableNoteChange { + Added(Box), + Updated(Box), + Removed(Box), + GraveAdded(Box<(NoteID, Usn)>), + GraveRemoved(Box<(NoteID, Usn)>), +} + +impl Collection { + pub(crate) fn undo_note_change(&mut self, change: UndoableNoteChange) -> Result<()> { + match change { + UndoableNoteChange::Added(note) => self.remove_note_without_grave(*note), + UndoableNoteChange::Updated(note) => { + let current = self + .storage + .get_note(note.id)? + .ok_or_else(|| AnkiError::invalid_input("note disappeared"))?; + self.update_note_undoable(¬e, ¤t, false) + } + UndoableNoteChange::Removed(note) => self.restore_deleted_note(*note), + UndoableNoteChange::GraveAdded(e) => self.remove_note_grave(e.0, e.1), + UndoableNoteChange::GraveRemoved(e) => self.add_note_grave(e.0, e.1), + } + } + + /// Saves in the undo queue, and commits to DB. + /// No validation, card generation or normalization is done. + /// If `coalesce_updates` is true, successive updates within a 1 minute + /// period will not result in further undo entries. + pub(super) fn update_note_undoable( + &mut self, + note: &Note, + original: &Note, + coalesce_updates: bool, + ) -> Result<()> { + if !coalesce_updates || !self.note_was_just_updated(note) { + self.save_undo(UndoableNoteChange::Updated(Box::new(original.clone()))); + } + self.storage.update_note(note)?; + + Ok(()) + } + + /// Remove a note. Cards must already have been deleted. + pub(crate) fn remove_note_only_undoable(&mut self, nid: NoteID, usn: Usn) -> Result<()> { + if let Some(note) = self.storage.get_note(nid)? { + self.save_undo(UndoableNoteChange::Removed(Box::new(note))); + self.storage.remove_note(nid)?; + self.add_note_grave(nid, usn)?; + } + Ok(()) + } + + /// Add a note, not adding any cards. + pub(super) fn add_note_only_undoable(&mut self, note: &mut Note) -> Result<(), AnkiError> { + self.storage.add_note(note)?; + self.save_undo(UndoableNoteChange::Added(Box::new(note.clone()))); + + Ok(()) + } + + fn remove_note_without_grave(&mut self, note: Note) -> Result<()> { + self.storage.remove_note(note.id)?; + self.save_undo(UndoableNoteChange::Removed(Box::new(note))); + Ok(()) + } + + fn restore_deleted_note(&mut self, note: Note) -> Result<()> { + self.storage.add_or_update_note(¬e)?; + self.save_undo(UndoableNoteChange::Added(Box::new(note))); + Ok(()) + } + + fn add_note_grave(&mut self, nid: NoteID, usn: Usn) -> Result<()> { + self.save_undo(UndoableNoteChange::GraveAdded(Box::new((nid, usn)))); + self.storage.add_note_grave(nid, usn) + } + + fn remove_note_grave(&mut self, nid: NoteID, usn: Usn) -> Result<()> { + self.save_undo(UndoableNoteChange::GraveRemoved(Box::new((nid, usn)))); + self.storage.remove_note_grave(nid) + } + + /// True only if the last operation was UpdateNote, and the same note was just updated less than + /// a minute ago. + fn note_was_just_updated(&self, before_change: &Note) -> bool { + self.previous_undo_op() + .map(|op| { + if let Some(UndoableChange::Note(UndoableNoteChange::Updated(note))) = + op.changes.last() + { + note.id == before_change.id + && op.kind == UndoableOpKind::UpdateNote + && op.timestamp.elapsed_secs() < 60 + } else { + false + } + }) + .unwrap_or(false) + } +} diff --git a/rslib/src/notetype/cardgen.rs b/rslib/src/notetype/cardgen.rs index 2e1993794..e89e37ffa 100644 --- a/rslib/src/notetype/cardgen.rs +++ b/rslib/src/notetype/cardgen.rs @@ -299,7 +299,12 @@ impl Collection { // not sure if entry() can be used due to get_deck_config() returning a result #[allow(clippy::map_entry)] - fn due_for_deck(&self, did: DeckID, dcid: DeckConfID, cache: &mut CardGenCache) -> Result { + fn due_for_deck( + &mut self, + did: DeckID, + dcid: DeckConfID, + cache: &mut CardGenCache, + ) -> Result { if !cache.deck_configs.contains_key(&did) { let conf = self.get_deck_config(dcid, true)?.unwrap(); cache.deck_configs.insert(did, conf); diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 6c19e94d8..e0477f8a6 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -28,6 +28,7 @@ use crate::{ define_newtype, err::{AnkiError, Result}, notes::Note, + prelude::*, template::{FieldRequirements, ParsedTemplate}, text::ensure_string_in_nfc, timestamp::TimestampSecs, @@ -424,7 +425,7 @@ impl Collection { /// or fields have been added/removed/reordered. pub fn update_notetype(&mut self, nt: &mut NoteType, preserve_usn: bool) -> Result<()> { let existing = self.get_notetype(nt.id)?; - let norm = self.normalize_note_text(); + let norm = self.get_bool(BoolKey::NormalizeNoteText); nt.prepare_for_update(existing.as_ref().map(AsRef::as_ref))?; self.transact(None, |col| { if let Some(existing_notetype) = existing { @@ -498,6 +499,7 @@ impl Collection { self.transact(None, |col| { col.storage.set_schema_modified()?; col.state.notetype_cache.remove(&ntid); + col.clear_aux_config_for_notetype(ntid)?; col.storage.remove_notetype(ntid)?; let all = col.storage.get_all_notetype_names()?; if all.is_empty() { diff --git a/rslib/src/notetype/render.rs b/rslib/src/notetype/render.rs index 689405a09..f4683f10f 100644 --- a/rslib/src/notetype/render.rs +++ b/rslib/src/notetype/render.rs @@ -169,7 +169,7 @@ fn fill_empty_fields(note: &mut Note, qfmt: &str, nt: &NoteType, i18n: &I18n) { if let Ok(tmpl) = ParsedTemplate::from_text(qfmt) { let cloze_fields = tmpl.cloze_fields(); - for (val, field) in note.fields.iter_mut().zip(nt.fields.iter()) { + for (val, field) in note.fields_mut().iter_mut().zip(nt.fields.iter()) { if field_is_empty(val) { if cloze_fields.contains(&field.name.as_str()) { *val = i18n.tr(TR::CardTemplatesSampleCloze).into(); diff --git a/rslib/src/notetype/schemachange.rs b/rslib/src/notetype/schemachange.rs index 4f87c280c..2eba11818 100644 --- a/rslib/src/notetype/schemachange.rs +++ b/rslib/src/notetype/schemachange.rs @@ -67,6 +67,7 @@ impl Collection { note.prepare_for_update(nt, normalize_text)?; self.storage.update_note(¬e)?; } + return Ok(()); } else { // nothing to do return Ok(()); @@ -79,11 +80,11 @@ impl Collection { let usn = self.usn()?; for nid in nids { let mut note = self.storage.get_note(nid)?.unwrap(); - note.fields = ords + *note.fields_mut() = ords .iter() .map(|f| { if let Some(idx) = f { - note.fields + note.fields() .get(*idx as usize) .map(AsRef::as_ref) .unwrap_or("") @@ -201,24 +202,22 @@ mod test { .get_notetype(col.get_current_notetype_id().unwrap())? .unwrap(); let mut note = nt.new_note(); - assert_eq!(note.fields.len(), 2); - note.fields = vec!["one".into(), "two".into()]; + assert_eq!(note.fields().len(), 2); + note.set_field(0, "one")?; + note.set_field(1, "two")?; col.add_note(&mut note, DeckID(1))?; nt.add_field("three"); col.update_notetype(&mut nt, false)?; let note = col.storage.get_note(note.id)?.unwrap(); - assert_eq!( - note.fields, - vec!["one".to_string(), "two".into(), "".into()] - ); + assert_eq!(note.fields(), &["one".to_string(), "two".into(), "".into()]); nt.fields.remove(1); col.update_notetype(&mut nt, false)?; let note = col.storage.get_note(note.id)?.unwrap(); - assert_eq!(note.fields, vec!["one".to_string(), "".into()]); + assert_eq!(note.fields(), &["one".to_string(), "".into()]); Ok(()) } @@ -252,8 +251,9 @@ mod test { .get_notetype(col.get_current_notetype_id().unwrap())? .unwrap(); let mut note = nt.new_note(); - assert_eq!(note.fields.len(), 2); - note.fields = vec!["one".into(), "two".into()]; + assert_eq!(note.fields().len(), 2); + note.set_field(0, "one")?; + note.set_field(1, "two")?; col.add_note(&mut note, DeckID(1))?; assert_eq!( diff --git a/rslib/src/notetype/stock.rs b/rslib/src/notetype/stock.rs index b124a398b..420cc1b5f 100644 --- a/rslib/src/notetype/stock.rs +++ b/rslib/src/notetype/stock.rs @@ -3,8 +3,13 @@ use super::NoteTypeKind; use crate::{ - config::ConfigKey, err::Result, i18n::I18n, i18n::TR, notetype::NoteType, - storage::SqliteStorage, timestamp::TimestampSecs, + config::{ConfigEntry, ConfigKey}, + err::Result, + i18n::I18n, + i18n::TR, + notetype::NoteType, + storage::SqliteStorage, + timestamp::TimestampSecs, }; use crate::backend_proto::stock_note_type::Kind; @@ -14,12 +19,12 @@ impl SqliteStorage { for (idx, mut nt) in all_stock_notetypes(i18n).into_iter().enumerate() { self.add_new_notetype(&mut nt)?; if idx == Kind::Basic as usize { - self.set_config_value( + self.set_config_entry(&ConfigEntry::boxed( ConfigKey::CurrentNoteTypeID.into(), - &nt.id, + serde_json::to_vec(&nt.id)?, self.usn(false)?, TimestampSecs::now(), - )?; + ))?; } } Ok(()) diff --git a/rslib/src/preferences.rs b/rslib/src/preferences.rs index 1ec03c9e5..ae45fb9b9 100644 --- a/rslib/src/preferences.rs +++ b/rslib/src/preferences.rs @@ -7,8 +7,9 @@ use crate::{ Preferences, }, collection::Collection, + config::BoolKey, err::Result, - scheduler::cutoff::local_minutes_west_for_stamp, + scheduler::timing::local_minutes_west_for_stamp, }; impl Collection { @@ -39,11 +40,11 @@ impl Collection { crate::config::NewReviewMix::ReviewsFirst => NewRevMixPB::ReviewsFirst, crate::config::NewReviewMix::NewFirst => NewRevMixPB::NewFirst, } as i32, - show_remaining_due_counts: self.get_show_due_counts(), - show_intervals_on_buttons: self.get_show_intervals_above_buttons(), + show_remaining_due_counts: self.get_bool(BoolKey::ShowRemainingDueCountsInStudy), + show_intervals_on_buttons: self.get_bool(BoolKey::ShowIntervalsAboveAnswerButtons), time_limit_secs: self.get_answer_time_limit_secs(), new_timezone: self.get_creation_utc_offset().is_some(), - day_learn_first: self.get_day_learn_first(), + day_learn_first: self.get_bool(BoolKey::ShowDayLearningCardsFirst), }) } @@ -53,10 +54,16 @@ impl Collection { ) -> Result<()> { let s = settings; - self.set_day_learn_first(s.day_learn_first)?; + self.set_bool(BoolKey::ShowDayLearningCardsFirst, s.day_learn_first)?; + self.set_bool( + BoolKey::ShowRemainingDueCountsInStudy, + s.show_remaining_due_counts, + )?; + self.set_bool( + BoolKey::ShowIntervalsAboveAnswerButtons, + s.show_intervals_on_buttons, + )?; self.set_answer_time_limit_secs(s.time_limit_secs)?; - self.set_show_due_counts(s.show_remaining_due_counts)?; - self.set_show_intervals_above_buttons(s.show_intervals_on_buttons)?; self.set_learn_ahead_secs(s.learn_ahead_secs)?; self.set_new_review_mix(match s.new_review_mix() { diff --git a/rslib/src/prelude.rs b/rslib/src/prelude.rs index 54a5ca204..2e0d589ac 100644 --- a/rslib/src/prelude.rs +++ b/rslib/src/prelude.rs @@ -4,14 +4,16 @@ pub use crate::{ card::{Card, CardID}, collection::Collection, + config::BoolKey, deckconf::{DeckConf, DeckConfID}, decks::{Deck, DeckID, DeckKind}, err::{AnkiError, Result}, - i18n::{tr_args, tr_strs, TR}, + i18n::{tr_args, tr_strs, I18n, TR}, notes::{Note, NoteID}, - notetype::NoteTypeID, + notetype::{NoteType, NoteTypeID}, revlog::RevlogID, timestamp::{TimestampMillis, TimestampSecs}, types::Usn, + undo::UndoableOpKind, }; pub use slog::{debug, Logger}; diff --git a/rslib/src/revlog.rs b/rslib/src/revlog/mod.rs similarity index 85% rename from rslib/src/revlog.rs rename to rslib/src/revlog/mod.rs index 33fad146d..79f73f99f 100644 --- a/rslib/src/revlog.rs +++ b/rslib/src/revlog/mod.rs @@ -1,6 +1,8 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +pub(crate) mod undo; + use crate::serde::{default_on_invalid, deserialize_int_from_number}; use crate::{define_newtype, prelude::*}; use num_enum::TryFromPrimitive; @@ -10,9 +12,25 @@ use serde_tuple::Serialize_tuple; define_newtype!(RevlogID, i64); +impl RevlogID { + pub fn new() -> Self { + RevlogID(TimestampMillis::now().0) + } + + pub fn as_secs(self) -> TimestampSecs { + TimestampSecs(self.0 / 1000) + } +} + +impl From for RevlogID { + fn from(m: TimestampMillis) -> Self { + RevlogID(m.0) + } +} + #[derive(Serialize_tuple, Deserialize, Debug, Default, PartialEq)] pub struct RevlogEntry { - pub id: TimestampMillis, + pub id: RevlogID, pub cid: CardID, pub usn: Usn, /// - In the V1 scheduler, 3 represents easy in the learning case. @@ -70,7 +88,7 @@ impl Collection { usn: Usn, ) -> Result<()> { let entry = RevlogEntry { - id: TimestampMillis::now(), + id: RevlogID::new(), cid: card.id, usn, button_chosen: 0, @@ -80,6 +98,7 @@ impl Collection { taken_millis: 0, review_kind: RevlogReviewKind::Manual, }; - self.storage.add_revlog_entry(&entry) + self.add_revlog_entry_undoable(entry)?; + Ok(()) } } diff --git a/rslib/src/revlog/undo.rs b/rslib/src/revlog/undo.rs new file mode 100644 index 000000000..daa3ce50f --- /dev/null +++ b/rslib/src/revlog/undo.rs @@ -0,0 +1,36 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::RevlogEntry; +use crate::prelude::*; + +#[derive(Debug)] +pub(crate) enum UndoableRevlogChange { + Added(Box), + Removed(Box), +} + +impl Collection { + pub(crate) fn undo_revlog_change(&mut self, change: UndoableRevlogChange) -> Result<()> { + match change { + UndoableRevlogChange::Added(revlog) => { + self.storage.remove_revlog_entry(revlog.id)?; + self.save_undo(UndoableRevlogChange::Removed(revlog)); + Ok(()) + } + UndoableRevlogChange::Removed(revlog) => { + self.storage.add_revlog_entry(&revlog, false)?; + self.save_undo(UndoableRevlogChange::Added(revlog)); + Ok(()) + } + } + } + + /// Add the provided revlog entry, modifying the ID if it is not unique. + pub(crate) fn add_revlog_entry_undoable(&mut self, mut entry: RevlogEntry) -> Result { + entry.id = self.storage.add_revlog_entry(&entry, true)?; + let id = entry.id; + self.save_undo(UndoableRevlogChange::Added(Box::new(entry))); + Ok(id) + } +} diff --git a/rslib/src/scheduler/answering/current.rs b/rslib/src/scheduler/answering/current.rs index 2b62024a4..ff2d3bf46 100644 --- a/rslib/src/scheduler/answering/current.rs +++ b/rslib/src/scheduler/answering/current.rs @@ -49,7 +49,7 @@ impl CardStateUpdater { } else { PreviewState { scheduled_secs: filtered.preview_delay * 60, - original_state: normal_state, + finished: false, } .into() } diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 46c4d924e..3913bee97 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -5,9 +5,9 @@ mod current; mod learning; mod preview; mod relearning; -mod rescheduling_filter; mod review; mod revlog; +mod undo; use crate::{ backend_proto, @@ -20,11 +20,11 @@ use crate::{ use revlog::RevlogEntryPartial; use super::{ - cutoff::SchedTimingToday, states::{ steps::LearningSteps, CardState, FilteredState, NextCardStates, NormalState, StateContext, }, timespan::answer_button_time_collapsible, + timing::SchedTimingToday, }; #[derive(Copy, Clone)] @@ -44,9 +44,7 @@ pub struct CardAnswer { pub milliseconds_taken: u32, } -// fixme: 4 buttons for previewing // fixme: log preview review -// fixme: undo /// Holds the information required to determine a given card's /// current state, and to apply a state change to it. @@ -107,25 +105,52 @@ impl CardStateUpdater { current: CardState, next: CardState, ) -> Result> { - // any non-preview answer resets card.odue and increases reps - if !matches!(current, CardState::Filtered(FilteredState::Preview(_))) { - self.card.reps += 1; - self.card.original_due = 0; - } + let revlog = match next { + CardState::Normal(normal) => { + // transitioning from filtered state? + if let CardState::Filtered(filtered) = ¤t { + match filtered { + FilteredState::Preview(_) => { + return Err(AnkiError::invalid_input( + "should set finished=true, not return different state", + )); + } + FilteredState::Rescheduling(_) => { + // card needs to be removed from normal filtered deck, then scheduled normally + self.card.remove_from_filtered_deck_before_reschedule(); + } + } + } + // apply normal scheduling + self.apply_normal_study_state(current, normal) + } + CardState::Filtered(filtered) => { + self.ensure_filtered()?; + match filtered { + FilteredState::Preview(next) => self.apply_preview_state(current, next), + FilteredState::Rescheduling(next) => { + self.apply_normal_study_state(current, next.original_state) + } + } + } + }?; + + Ok(revlog) + } + + fn apply_normal_study_state( + &mut self, + current: CardState, + next: NormalState, + ) -> Result> { + self.card.reps += 1; + self.card.original_due = 0; let revlog = match next { - CardState::Normal(normal) => match normal { - NormalState::New(next) => self.apply_new_state(current, next), - NormalState::Learning(next) => self.apply_learning_state(current, next), - NormalState::Review(next) => self.apply_review_state(current, next), - NormalState::Relearning(next) => self.apply_relearning_state(current, next), - }, - CardState::Filtered(filtered) => match filtered { - FilteredState::Preview(next) => self.apply_preview_state(current, next), - FilteredState::Rescheduling(next) => { - self.apply_rescheduling_filter_state(current, next) - } - }, + NormalState::New(next) => self.apply_new_state(current, next), + NormalState::Learning(next) => self.apply_learning_state(current, next), + NormalState::Review(next) => self.apply_review_state(current, next), + NormalState::Relearning(next) => self.apply_relearning_state(current, next), }?; if next.leeched() && self.config.inner.leech_action() == LeechAction::Suspend { @@ -168,11 +193,12 @@ impl Collection { } /// Describe the next intervals, to display on the answer buttons. - pub fn describe_next_states(&self, choices: NextCardStates) -> Result> { + pub fn describe_next_states(&mut self, choices: NextCardStates) -> Result> { let collapse_time = self.learn_ahead_secs(); let now = TimestampSecs::now(); let timing = self.timing_for_timestamp(now)?; let secs_until_rollover = (timing.next_day_at - now.0).max(0) as u32; + Ok(vec![ answer_button_time_collapsible( choices @@ -215,7 +241,9 @@ impl Collection { /// Answer card, writing its new state to the database. pub fn answer_card(&mut self, answer: &CardAnswer) -> Result<()> { - self.transact(None, |col| col.answer_card_inner(answer)) + self.transact(Some(UndoableOpKind::AnswerCard), |col| { + col.answer_card_inner(answer) + }) } fn answer_card_inner(&mut self, answer: &CardAnswer) -> Result<()> { @@ -234,23 +262,38 @@ impl Collection { current_state, answer.current_state, ))); } - if let Some(revlog_partial) = updater.apply_study_state(current_state, answer.new_state)? { self.add_partial_revlog(revlog_partial, usn, &answer)?; } self.update_deck_stats_from_answer(usn, &answer, &updater)?; - + self.maybe_bury_siblings(&original, &updater.config)?; + let timing = updater.timing; let mut card = updater.into_card(); - self.update_card(&mut card, &original, usn)?; + self.update_card_inner(&mut card, &original, usn)?; if answer.new_state.leeched() { self.add_leech_tag(card.note_id)?; } + self.update_queues_after_answering_card(&card, timing)?; + + Ok(()) + } + + fn maybe_bury_siblings(&mut self, card: &Card, config: &DeckConf) -> Result<()> { + if config.inner.bury_new || config.inner.bury_reviews { + self.bury_siblings( + card.id, + card.note_id, + config.inner.bury_new, + config.inner.bury_reviews, + )?; + } + Ok(()) } fn add_partial_revlog( - &self, + &mut self, partial: RevlogEntryPartial, usn: Usn, answer: &CardAnswer, @@ -262,7 +305,8 @@ impl Collection { answer.answered_at, answer.milliseconds_taken, ); - self.storage.add_revlog_entry(&revlog) + self.add_revlog_entry_undoable(revlog)?; + Ok(()) } fn update_deck_stats_from_answer( @@ -338,7 +382,7 @@ impl Collection { /// Return a consistent seed for a given card at a given number of reps. /// If in test environment, disable fuzzing. fn get_fuzz_seed(card: &Card) -> Option { - if *crate::timestamp::TESTING { + if *crate::timestamp::TESTING || cfg!(test) { None } else { Some((card.id.0 as u64).wrapping_add(card.reps as u64)) diff --git a/rslib/src/scheduler/answering/preview.rs b/rslib/src/scheduler/answering/preview.rs index 9bf385477..4dd1e9df8 100644 --- a/rslib/src/scheduler/answering/preview.rs +++ b/rslib/src/scheduler/answering/preview.rs @@ -3,6 +3,7 @@ use crate::{ card::CardQueue, + config::SchedulerVersion, prelude::*, scheduler::states::{CardState, IntervalKind, PreviewState}, }; @@ -17,7 +18,12 @@ impl CardStateUpdater { current: CardState, next: PreviewState, ) -> Result> { - self.ensure_filtered()?; + if next.finished { + self.card + .remove_from_filtered_deck_restoring_queue(SchedulerVersion::V2); + return Ok(None); + } + self.card.queue = CardQueue::PreviewRepeat; let interval = next.interval_kind(); @@ -48,7 +54,7 @@ mod test { card::CardType, scheduler::{ answering::{CardAnswer, Rating}, - states::{CardState, FilteredState, LearnState, NormalState}, + states::{CardState, FilteredState}, }, timestamp::TimestampMillis, }; @@ -56,42 +62,35 @@ mod test { #[test] fn preview() -> Result<()> { let mut col = open_test_collection(); - dbg!(col.scheduler_version()); let mut c = Card { deck_id: DeckID(1), ctype: CardType::Learn, - queue: CardQueue::Learn, + queue: CardQueue::DayLearn, remaining_steps: 2, + due: 123, ..Default::default() }; col.add_card(&mut c)?; - // set the first (current) step to a day - let deck = col.storage.get_deck(DeckID(1))?.unwrap(); - let mut conf = col - .get_deck_config(DeckConfID(deck.normal()?.config_id), false)? - .unwrap(); - *conf.inner.learn_steps.get_mut(0).unwrap() = 24.0 * 60.0; - col.add_or_update_deck_config(&mut conf, false)?; - // pull the card into a preview deck let mut filtered_deck = Deck::new_filtered(); filtered_deck.filtered_mut()?.reschedule = false; col.add_or_update_deck(&mut filtered_deck)?; assert_eq!(col.rebuild_filtered_deck(filtered_deck.id)?, 1); - // the original state reflects the learning steps, not the card properties let next = col.get_next_card_states(c.id)?; - assert_eq!( + assert!(matches!( next.current, + CardState::Filtered(FilteredState::Preview(_)) + )); + // the exit state should have a 0 second interval, which will show up as (end) + assert!(matches!( + next.easy, CardState::Filtered(FilteredState::Preview(PreviewState { - scheduled_secs: 600, - original_state: NormalState::Learning(LearnState { - remaining_steps: 2, - scheduled_secs: 86_400, - }), + scheduled_secs: 0, + finished: true })) - ); + )); // use Again on the preview col.answer_card(&CardAnswer { @@ -106,8 +105,7 @@ mod test { c = col.storage.get_card(c.id)?.unwrap(); assert_eq!(c.queue, CardQueue::PreviewRepeat); - // and then it should return to its old state once passed - // (based on learning steps) + // hard let next = col.get_next_card_states(c.id)?; col.answer_card(&CardAnswer { card_id: c.id, @@ -118,8 +116,34 @@ mod test { milliseconds_taken: 0, })?; c = col.storage.get_card(c.id)?.unwrap(); + assert_eq!(c.queue, CardQueue::PreviewRepeat); + + // good + let next = col.get_next_card_states(c.id)?; + col.answer_card(&CardAnswer { + card_id: c.id, + current_state: next.current, + new_state: next.good, + rating: Rating::Good, + answered_at: TimestampMillis::now(), + milliseconds_taken: 0, + })?; + c = col.storage.get_card(c.id)?.unwrap(); + assert_eq!(c.queue, CardQueue::PreviewRepeat); + + // and then it should return to its old state once easy selected + let next = col.get_next_card_states(c.id)?; + col.answer_card(&CardAnswer { + card_id: c.id, + current_state: next.current, + new_state: next.easy, + rating: Rating::Easy, + answered_at: TimestampMillis::now(), + milliseconds_taken: 0, + })?; + c = col.storage.get_card(c.id)?.unwrap(); assert_eq!(c.queue, CardQueue::DayLearn); - assert_eq!(c.due, 1); + assert_eq!(c.due, 123); Ok(()) } diff --git a/rslib/src/scheduler/answering/relearning.rs b/rslib/src/scheduler/answering/relearning.rs index 77e701adf..69378b804 100644 --- a/rslib/src/scheduler/answering/relearning.rs +++ b/rslib/src/scheduler/answering/relearning.rs @@ -18,6 +18,7 @@ impl CardStateUpdater { self.card.interval = next.review.scheduled_days; self.card.remaining_steps = next.learning.remaining_steps; self.card.ctype = CardType::Relearn; + self.card.lapses = next.review.lapses; let interval = next .interval_kind() diff --git a/rslib/src/scheduler/answering/rescheduling_filter.rs b/rslib/src/scheduler/answering/rescheduling_filter.rs deleted file mode 100644 index e5b331cfc..000000000 --- a/rslib/src/scheduler/answering/rescheduling_filter.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -use crate::{ - prelude::*, - scheduler::states::{CardState, ReschedulingFilterState}, -}; - -use super::{CardStateUpdater, RevlogEntryPartial}; - -impl CardStateUpdater { - pub(super) fn apply_rescheduling_filter_state( - &mut self, - current: CardState, - next: ReschedulingFilterState, - ) -> Result> { - self.ensure_filtered()?; - self.apply_study_state(current, next.original_state.into()) - } -} diff --git a/rslib/src/scheduler/answering/review.rs b/rslib/src/scheduler/answering/review.rs index 6df913c29..b948b43c2 100644 --- a/rslib/src/scheduler/answering/review.rs +++ b/rslib/src/scheduler/answering/review.rs @@ -15,7 +15,6 @@ impl CardStateUpdater { current: CardState, next: ReviewState, ) -> Result> { - self.card.remove_from_filtered_deck_before_reschedule(); self.card.queue = CardQueue::Review; self.card.ctype = CardType::Review; self.card.interval = next.scheduled_days; diff --git a/rslib/src/scheduler/answering/revlog.rs b/rslib/src/scheduler/answering/revlog.rs index b5240cac2..a14f12b12 100644 --- a/rslib/src/scheduler/answering/revlog.rs +++ b/rslib/src/scheduler/answering/revlog.rs @@ -44,7 +44,7 @@ impl RevlogEntryPartial { taken_millis: u32, ) -> RevlogEntry { RevlogEntry { - id: answered_at, + id: answered_at.into(), cid, usn, button_chosen, diff --git a/rslib/src/scheduler/answering/undo.rs b/rslib/src/scheduler/answering/undo.rs new file mode 100644 index 000000000..41ae50815 --- /dev/null +++ b/rslib/src/scheduler/answering/undo.rs @@ -0,0 +1,145 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +#[cfg(test)] +mod test { + use crate::{ + card::{CardQueue, CardType}, + collection::open_test_collection, + deckconf::LeechAction, + prelude::*, + scheduler::answering::{CardAnswer, Rating}, + }; + + #[test] + fn undo() -> Result<()> { + // add a note + let mut col = open_test_collection(); + let nt = col + .get_notetype_by_name("Basic (and reversed card)")? + .unwrap(); + let mut note = nt.new_note(); + note.set_field(0, "one")?; + note.set_field(1, "two")?; + col.add_note(&mut note, DeckID(1))?; + + // turn burying and leech suspension on + let mut conf = col.storage.get_deck_config(DeckConfID(1))?.unwrap(); + conf.inner.bury_new = true; + conf.inner.leech_action = LeechAction::Suspend as i32; + col.storage.update_deck_conf(&conf)?; + + // get the first card + 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 assert_initial_state = |col: &mut Collection| -> Result<()> { + let first = col.storage.get_card(cid)?.unwrap(); + assert_eq!(first.queue, CardQueue::New); + let sibling = col.storage.get_card(sibling_cid)?.unwrap(); + assert_eq!(sibling.queue, CardQueue::New); + Ok(()) + }; + + assert_initial_state(&mut col)?; + + // immediately graduate the first card + col.answer_card(&CardAnswer { + card_id: queued.card.id, + current_state: queued.next_states.current, + new_state: queued.next_states.easy, + rating: Rating::Easy, + answered_at: TimestampMillis::now(), + milliseconds_taken: 0, + })?; + + // the sibling will be buried + let sibling = col.storage.get_card(sibling_cid)?.unwrap(); + assert_eq!(sibling.queue, CardQueue::SchedBuried); + + // make it due now, with 7 lapses. we use the storage layer directly, + // bypassing undo + let mut card = col.storage.get_card(cid)?.unwrap(); + assert_eq!(card.ctype, CardType::Review); + card.lapses = 7; + card.due = 0; + col.storage.update_card(&card)?; + + // fail it, which should cause it to be marked as a leech + col.clear_study_queues(); + let queued = col.next_card()?.unwrap(); + dbg!(&queued); + col.answer_card(&CardAnswer { + card_id: queued.card.id, + current_state: queued.next_states.current, + new_state: queued.next_states.again, + rating: Rating::Again, + answered_at: TimestampMillis::now(), + milliseconds_taken: 0, + })?; + + let assert_post_review_state = |col: &mut Collection| -> Result<()> { + let card = col.storage.get_card(cid)?.unwrap(); + assert_eq!(card.interval, 1); + assert_eq!(card.lapses, 8); + + assert_eq!( + col.storage.get_all_revlog_entries(TimestampSecs(0))?.len(), + 2 + ); + + let note = col.storage.get_note(nid)?.unwrap(); + assert_eq!(note.tags, vec!["leech".to_string()]); + assert_eq!(col.storage.all_tags()?.is_empty(), false); + + let deck = col.get_deck(DeckID(1))?.unwrap(); + assert_eq!(deck.common.review_studied, 1); + + dbg!(&col.next_card()?); + assert_eq!(col.next_card()?.is_some(), false); + + Ok(()) + }; + + let assert_pre_review_state = |col: &mut Collection| -> Result<()> { + // the card should have its old state, but a new mtime (which we can't + // easily test without waiting) + let card = col.storage.get_card(cid)?.unwrap(); + assert_eq!(card.interval, 4); + assert_eq!(card.lapses, 7); + + // the revlog entry should have been removed + assert_eq!( + col.storage.get_all_revlog_entries(TimestampSecs(0))?.len(), + 1 + ); + + // the note should no longer be tagged as a leech + let note = col.storage.get_note(nid)?.unwrap(); + assert_eq!(note.tags.is_empty(), true); + assert_eq!(col.storage.all_tags()?.is_empty(), true); + + let deck = col.get_deck(DeckID(1))?.unwrap(); + assert_eq!(deck.common.review_studied, 0); + + assert_eq!(col.next_card()?.is_some(), true); + + Ok(()) + }; + + // ensure everything is restored on undo/redo + assert_post_review_state(&mut col)?; + col.undo()?; + assert_pre_review_state(&mut col)?; + col.redo()?; + assert_post_review_state(&mut col)?; + col.undo()?; + assert_pre_review_state(&mut col)?; + col.undo()?; + assert_initial_state(&mut col)?; + + Ok(()) + } +} diff --git a/rslib/src/scheduler/bury_and_suspend.rs b/rslib/src/scheduler/bury_and_suspend.rs index fa114b9b5..24374ef10 100644 --- a/rslib/src/scheduler/bury_and_suspend.rs +++ b/rslib/src/scheduler/bury_and_suspend.rs @@ -3,15 +3,19 @@ use crate::{ backend_proto as pb, - card::{Card, CardID, CardQueue, CardType}, + card::{Card, CardID, CardQueue}, collection::Collection, config::SchedulerVersion, err::Result, + prelude::*, search::SortMode, }; -use super::cutoff::SchedTimingToday; -use pb::unbury_cards_in_current_deck_in::Mode as UnburyDeckMode; +use super::timing::SchedTimingToday; +use pb::{ + bury_or_suspend_cards_in::Mode as BuryOrSuspendMode, + unbury_cards_in_current_deck_in::Mode as UnburyDeckMode, +}; impl Card { /// True if card was buried/suspended prior to the call. @@ -22,19 +26,7 @@ impl Card { ) { false } else { - self.queue = match self.ctype { - CardType::Learn | CardType::Relearn => { - if self.original_or_current_due() > 1_000_000_000 { - // previous interval was in seconds - CardQueue::Learn - } else { - // previous interval was in days - CardQueue::DayLearn - } - } - CardType::New => CardQueue::New, - CardType::Review => CardQueue::Review, - }; + self.restore_queue_from_type(); true } } @@ -70,14 +62,14 @@ impl Collection { for original in self.storage.all_searched_cards()? { let mut card = original.clone(); if card.restore_queue_after_bury_or_suspend() { - self.update_card(&mut card, &original, usn)?; + self.update_card_inner(&mut card, &original, usn)?; } } self.storage.clear_searched_cards_table() } pub fn unbury_or_unsuspend_cards(&mut self, cids: &[CardID]) -> Result<()> { - self.transact(None, |col| { + self.transact(Some(UndoableOpKind::UnburyUnsuspend), |col| { col.storage.set_search_table_to_card_ids(cids, false)?; col.unsuspend_or_unbury_searched_cards() }) @@ -97,20 +89,16 @@ 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: pb::bury_or_suspend_cards_in::Mode, - ) -> Result<()> { - use pb::bury_or_suspend_cards_in::Mode; + fn bury_or_suspend_searched_cards(&mut self, mode: BuryOrSuspendMode) -> Result<()> { let usn = self.usn()?; let sched = self.scheduler_version(); for original in self.storage.all_searched_cards()? { let mut card = original.clone(); let desired_queue = match mode { - Mode::Suspend => CardQueue::Suspended, - Mode::BurySched => CardQueue::SchedBuried, - Mode::BuryUser => { + BuryOrSuspendMode::Suspend => CardQueue::Suspended, + BuryOrSuspendMode::BurySched => CardQueue::SchedBuried, + BuryOrSuspendMode::BuryUser => { if sched == SchedulerVersion::V1 { // v1 scheduler only had one bury type CardQueue::SchedBuried @@ -125,7 +113,7 @@ impl Collection { card.remove_from_learning(); } card.queue = desired_queue; - self.update_card(&mut card, &original, usn)?; + self.update_card_inner(&mut card, &original, usn)?; } } @@ -135,13 +123,29 @@ impl Collection { pub fn bury_or_suspend_cards( &mut self, cids: &[CardID], - mode: pb::bury_or_suspend_cards_in::Mode, + mode: BuryOrSuspendMode, ) -> Result<()> { - self.transact(None, |col| { + let op = match mode { + BuryOrSuspendMode::Suspend => UndoableOpKind::Suspend, + BuryOrSuspendMode::BurySched | BuryOrSuspendMode::BuryUser => UndoableOpKind::Bury, + }; + self.transact(Some(op), |col| { col.storage.set_search_table_to_card_ids(cids, false)?; col.bury_or_suspend_searched_cards(mode) }) } + + pub(crate) fn bury_siblings( + &mut self, + cid: CardID, + nid: NoteID, + include_new: bool, + include_reviews: bool, + ) -> Result<()> { + self.storage + .search_siblings_for_bury(cid, nid, include_new, include_reviews)?; + self.bury_or_suspend_searched_cards(BuryOrSuspendMode::BurySched) + } } #[cfg(test)] @@ -155,8 +159,10 @@ mod test { #[test] fn unbury() { let mut col = open_test_collection(); - let mut card = Card::default(); - card.queue = CardQueue::UserBuried; + let mut card = Card { + queue: CardQueue::UserBuried, + ..Default::default() + }; col.add_card(&mut card).unwrap(); let assert_count = |col: &mut Collection, cnt| { assert_eq!( diff --git a/rslib/src/scheduler/mod.rs b/rslib/src/scheduler/mod.rs index 4a1bfc941..01b1cda82 100644 --- a/rslib/src/scheduler/mod.rs +++ b/rslib/src/scheduler/mod.rs @@ -6,23 +6,24 @@ use crate::{collection::Collection, config::SchedulerVersion, err::Result, prelu pub mod answering; pub mod bury_and_suspend; pub(crate) mod congrats; -pub mod cutoff; mod learning; pub mod new; +pub(crate) mod queue; mod reviews; pub mod states; pub mod timespan; +pub mod timing; mod upgrade; use chrono::FixedOffset; -use cutoff::{ +pub use reviews::parse_due_date_str; +use timing::{ sched_timing_today, v1_creation_date_adjusted_to_hour, v1_rollover_from_creation_stamp, SchedTimingToday, }; -pub use reviews::parse_due_date_str; impl Collection { - pub fn timing_today(&self) -> Result { + pub fn timing_today(&mut self) -> Result { self.timing_for_timestamp(TimestampSecs::now()) } @@ -30,7 +31,7 @@ impl Collection { Ok(((self.timing_today()?.days_elapsed as i32) + delta).max(0) as u32) } - pub(crate) fn timing_for_timestamp(&self, now: TimestampSecs) -> Result { + pub(crate) fn timing_for_timestamp(&mut self, now: TimestampSecs) -> Result { let current_utc_offset = self.local_utc_offset_for_user()?; let rollover_hour = match self.scheduler_version() { @@ -62,7 +63,7 @@ impl Collection { /// ensuring the config reflects the current value. /// In the server case, return the value set in the config, and /// fall back on UTC if it's missing/invalid. - pub(crate) fn local_utc_offset_for_user(&self) -> Result { + pub(crate) fn local_utc_offset_for_user(&mut self) -> Result { let config_tz = self .get_configured_utc_offset() .and_then(|v| FixedOffset::west_opt(v * 60)) @@ -98,7 +99,7 @@ impl Collection { } } - pub(crate) fn set_rollover_for_current_scheduler(&self, hour: u8) -> Result<()> { + pub(crate) fn set_rollover_for_current_scheduler(&mut self, hour: u8) -> Result<()> { match self.scheduler_version() { SchedulerVersion::V1 => { self.storage diff --git a/rslib/src/scheduler/new.rs b/rslib/src/scheduler/new.rs index 16d8f215b..dcf534510 100644 --- a/rslib/src/scheduler/new.rs +++ b/rslib/src/scheduler/new.rs @@ -114,7 +114,7 @@ impl Collection { if log { col.log_manually_scheduled_review(&card, &original, usn)?; } - col.update_card(&mut card, &original, usn)?; + col.update_card_inner(&mut card, &original, usn)?; position += 1; } col.set_next_card_position(position)?; @@ -155,7 +155,7 @@ impl Collection { for mut card in cards { let original = card.clone(); card.set_new_position(sorter.position(&card)); - self.update_card(&mut card, &original, usn)?; + self.update_card_inner(&mut card, &original, usn)?; } self.storage.clear_searched_cards_table() } @@ -177,7 +177,7 @@ impl Collection { for mut card in self.storage.all_searched_cards()? { let original = card.clone(); card.set_new_position(card.due as u32 + by); - self.update_card(&mut card, &original, usn)?; + self.update_card_inner(&mut card, &original, usn)?; } self.storage.clear_searched_cards_table()?; Ok(()) diff --git a/rslib/src/scheduler/queue/builder/gathering.rs b/rslib/src/scheduler/queue/builder/gathering.rs new file mode 100644 index 000000000..06838f479 --- /dev/null +++ b/rslib/src/scheduler/queue/builder/gathering.rs @@ -0,0 +1,176 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{super::limits::RemainingLimits, DueCard, NewCard, QueueBuilder}; +use crate::{card::CardQueue, prelude::*}; + +impl QueueBuilder { + /// Assumes cards will arrive sorted in (queue, due) order, so learning + /// cards come first, and reviews come before day-learning and preview cards. + pub(in super::super) fn add_due_card( + &mut self, + limit: &mut RemainingLimits, + queue: CardQueue, + card: DueCard, + ) -> bool { + let should_add = self.should_add_review_card(card.note_id); + + match queue { + CardQueue::Learn | CardQueue::PreviewRepeat => self.learning.push(card), + CardQueue::DayLearn => { + self.day_learning.push(card); + } + CardQueue::Review => { + if should_add { + self.review.push(card); + limit.review -= 1; + } + } + CardQueue::New + | CardQueue::Suspended + | CardQueue::SchedBuried + | CardQueue::UserBuried => { + unreachable!() + } + } + + limit.review != 0 + } + + pub(in super::super) fn add_new_card( + &mut self, + limit: &mut RemainingLimits, + card: NewCard, + ) -> bool { + let already_seen = self.have_seen_note_id(card.note_id); + if !already_seen { + self.new.push(card); + limit.new -= 1; + return limit.new != 0; + } + + // Cards will be arriving in (due, card_id) order, with all + // siblings sharing the same due number by default. In the + // common case, card ids will match template order, and nothing + // special is required. But if some cards have been generated + // after the initial note creation, they will have higher card + // ids, and the siblings will thus arrive in the wrong order. + // Sorting by ordinal in the DB layer is fairly costly, as it + // doesn't allow us to exit early when the daily limits have + // been met, so we want to enforce ordering as we add instead. + let previous_card_was_sibling_with_higher_ordinal = self + .new + .last() + .map(|previous| previous.note_id == card.note_id && previous.extra > card.extra) + .unwrap_or(false); + + if previous_card_was_sibling_with_higher_ordinal { + if self.bury_new { + // When burying is enabled, we replace the existing sibling + // with the lower ordinal one. + *self.new.last_mut().unwrap() = card; + } else { + // When burying disabled, we'll want to add this card as well, but + // not at the end of the list. + let target_idx = self + .new + .iter() + .enumerate() + .rev() + .filter_map(|(idx, queued_card)| { + if queued_card.note_id != card.note_id || queued_card.extra < card.extra { + Some(idx + 1) + } else { + None + } + }) + .next() + .unwrap_or(0); + self.new.insert(target_idx, card); + limit.new -= 1; + } + } else { + // card has arrived in expected order - add if burying disabled + if !self.bury_new { + self.new.push(card); + limit.new -= 1; + } + } + + limit.new != 0 + } + + fn should_add_review_card(&mut self, note_id: NoteID) -> bool { + !self.have_seen_note_id(note_id) || !self.bury_reviews + } + + /// Mark note seen, and return true if seen before. + fn have_seen_note_id(&mut self, note_id: NoteID) -> bool { + !self.seen_note_ids.insert(note_id) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn new_siblings() { + let mut builder = QueueBuilder { + bury_new: true, + ..Default::default() + }; + let mut limits = RemainingLimits { + review: 0, + new: 100, + }; + + let cards = vec![ + NewCard { + id: CardID(1), + note_id: NoteID(1), + extra: 0, + ..Default::default() + }, + NewCard { + id: CardID(2), + note_id: NoteID(2), + extra: 1, + ..Default::default() + }, + NewCard { + id: CardID(3), + note_id: NoteID(2), + extra: 2, + ..Default::default() + }, + NewCard { + id: CardID(4), + note_id: NoteID(2), + extra: 0, + ..Default::default() + }, + ]; + + for card in &cards { + builder.add_new_card(&mut limits, card.clone()); + } + + assert_eq!(builder.new[0].id, CardID(1)); + assert_eq!(builder.new[1].id, CardID(4)); + assert_eq!(builder.new.len(), 2); + + // with burying disabled, we should get all siblings in order + builder.bury_new = false; + builder.new.truncate(0); + + for card in &cards { + builder.add_new_card(&mut limits, card.clone()); + } + + assert_eq!(builder.new[0].id, CardID(1)); + assert_eq!(builder.new[1].id, CardID(4)); + assert_eq!(builder.new[2].id, CardID(2)); + assert_eq!(builder.new[3].id, CardID(3)); + } +} diff --git a/rslib/src/scheduler/queue/builder/intersperser.rs b/rslib/src/scheduler/queue/builder/intersperser.rs new file mode 100644 index 000000000..4252f31ac --- /dev/null +++ b/rslib/src/scheduler/queue/builder/intersperser.rs @@ -0,0 +1,123 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +/// Adapter to evenly mix two iterators of varying lengths into one. +pub(crate) struct Intersperser +where + I: Iterator + ExactSizeIterator, +{ + one: I, + two: I2, + one_idx: usize, + two_idx: usize, + one_len: usize, + two_len: usize, + ratio: f32, +} + +impl Intersperser +where + I: ExactSizeIterator, + I2: ExactSizeIterator, +{ + pub fn new(one: I, two: I2) -> Self { + let one_len = one.len(); + let two_len = two.len(); + let ratio = one_len as f32 / two_len as f32; + Intersperser { + one, + two, + one_idx: 0, + two_idx: 0, + one_len, + two_len, + ratio, + } + } + + fn one_idx(&self) -> Option { + if self.one_idx == self.one_len { + None + } else { + Some(self.one_idx) + } + } + + fn two_idx(&self) -> Option { + if self.two_idx == self.two_len { + None + } else { + Some(self.two_idx) + } + } + + fn next_one(&mut self) -> Option { + self.one_idx += 1; + self.one.next() + } + + fn next_two(&mut self) -> Option { + self.two_idx += 1; + self.two.next() + } +} + +impl Iterator for Intersperser +where + I: ExactSizeIterator, + I2: ExactSizeIterator, +{ + type Item = I::Item; + + fn next(&mut self) -> Option { + match (self.one_idx(), self.two_idx()) { + (Some(idx1), Some(idx2)) => { + let relative_idx2 = idx2 as f32 * self.ratio; + if relative_idx2 < idx1 as f32 { + self.next_two() + } else { + self.next_one() + } + } + (Some(_), None) => self.next_one(), + (None, Some(_)) => self.next_two(), + (None, None) => None, + } + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = (self.one_len + self.two_len) - (self.one_idx + self.two_idx); + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for Intersperser +where + I: ExactSizeIterator, + I2: ExactSizeIterator, +{ +} + +#[cfg(test)] +mod test { + use super::Intersperser; + + fn intersperse(a: &[u32], b: &[u32]) -> Vec { + Intersperser::new(a.iter().cloned(), b.iter().cloned()).collect() + } + + #[test] + fn interspersing() { + let a = &[1, 2, 3]; + let b = &[11, 22, 33]; + assert_eq!(&intersperse(a, b), &[1, 11, 2, 22, 3, 33]); + + let b = &[11, 22]; + assert_eq!(&intersperse(a, b), &[1, 11, 2, 22, 3]); + + // when both lists have the same relative position, we add from + // list 1 even if list 2 has more elements + let b = &[11, 22, 33, 44, 55, 66]; + assert_eq!(&intersperse(a, b), &[1, 11, 22, 2, 33, 44, 3, 55, 66]); + } +} diff --git a/rslib/src/scheduler/queue/builder/mod.rs b/rslib/src/scheduler/queue/builder/mod.rs new file mode 100644 index 000000000..af7d3491e --- /dev/null +++ b/rslib/src/scheduler/queue/builder/mod.rs @@ -0,0 +1,227 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +mod gathering; +pub(crate) mod intersperser; +pub(crate) mod sized_chain; +mod sorting; + +use std::{ + cmp::Reverse, + collections::{BinaryHeap, HashSet, VecDeque}, +}; + +use super::{ + limits::{remaining_limits_capped_to_parents, RemainingLimits}, + CardQueues, Counts, LearningQueueEntry, MainQueueEntry, MainQueueEntryKind, +}; +use crate::deckconf::{NewCardOrder, ReviewCardOrder, ReviewMix}; +use crate::prelude::*; +use {intersperser::Intersperser, sized_chain::SizedChain}; + +/// Temporary holder for review cards that will be built into a queue. +#[derive(Debug, Default, Clone)] +pub(crate) struct DueCard { + pub id: CardID, + pub note_id: NoteID, + pub mtime: TimestampSecs, + pub due: i32, + pub interval: u32, + pub hash: u64, +} + +/// Temporary holder for new cards that will be built into a queue. +#[derive(Debug, Default, Clone)] +pub(crate) struct NewCard { + pub id: CardID, + pub note_id: NoteID, + pub mtime: TimestampSecs, + pub due: i32, + /// Used to store template_idx, and for shuffling + pub extra: u64, +} + +impl From for MainQueueEntry { + fn from(c: DueCard) -> Self { + MainQueueEntry { + id: c.id, + mtime: c.mtime, + kind: MainQueueEntryKind::Review, + } + } +} + +impl From for MainQueueEntry { + fn from(c: NewCard) -> Self { + MainQueueEntry { + id: c.id, + mtime: c.mtime, + kind: MainQueueEntryKind::New, + } + } +} + +impl From for LearningQueueEntry { + fn from(c: DueCard) -> Self { + LearningQueueEntry { + due: TimestampSecs(c.due as i64), + id: c.id, + mtime: c.mtime, + } + } +} + +#[derive(Default)] +pub(super) struct QueueBuilder { + pub(super) new: Vec, + pub(super) review: Vec, + pub(super) learning: Vec, + pub(super) day_learning: Vec, + pub(super) seen_note_ids: HashSet, + pub(super) new_order: NewCardOrder, + pub(super) review_order: ReviewCardOrder, + pub(super) day_learn_mix: ReviewMix, + pub(super) new_review_mix: ReviewMix, + pub(super) bury_new: bool, + pub(super) bury_reviews: bool, +} + +impl QueueBuilder { + pub(super) fn build( + mut self, + top_deck_limits: RemainingLimits, + learn_ahead_secs: u32, + selected_deck: DeckID, + current_day: u32, + ) -> CardQueues { + self.sort_new(); + self.sort_reviews(); + + // split and sort learning + let learn_ahead_secs = learn_ahead_secs as i64; + let (due_learning, later_learning) = split_learning(self.learning, learn_ahead_secs); + let learn_count = due_learning.len(); + + // merge day learning in, and cap to parent review count + let main_iter = merge_day_learning(self.review, self.day_learning, self.day_learn_mix); + let main_iter = main_iter.take(top_deck_limits.review as usize); + let review_count = main_iter.len(); + + // cap to parent new count, note down the new count, then merge new in + self.new.truncate(top_deck_limits.new as usize); + let new_count = self.new.len(); + let main_iter = merge_new(main_iter, self.new, self.new_review_mix); + + CardQueues { + counts: Counts { + new: new_count, + review: review_count, + learning: learn_count, + }, + undo: Vec::new(), + main: main_iter.collect(), + due_learning, + later_learning, + learn_ahead_secs, + selected_deck, + current_day, + } + } +} + +fn merge_day_learning( + reviews: Vec, + day_learning: Vec, + mode: ReviewMix, +) -> Box> { + let day_learning_iter = day_learning.into_iter().map(Into::into); + let reviews_iter = reviews.into_iter().map(Into::into); + + match mode { + ReviewMix::AfterReviews => Box::new(SizedChain::new(reviews_iter, day_learning_iter)), + ReviewMix::BeforeReviews => Box::new(SizedChain::new(day_learning_iter, reviews_iter)), + ReviewMix::MixWithReviews => Box::new(Intersperser::new(reviews_iter, day_learning_iter)), + } +} + +fn merge_new( + review_iter: impl ExactSizeIterator + 'static, + new: Vec, + mode: ReviewMix, +) -> Box> { + let new_iter = new.into_iter().map(Into::into); + + match mode { + ReviewMix::BeforeReviews => Box::new(SizedChain::new(new_iter, review_iter)), + ReviewMix::AfterReviews => Box::new(SizedChain::new(review_iter, new_iter)), + ReviewMix::MixWithReviews => Box::new(Intersperser::new(review_iter, new_iter)), + } +} + +/// Split the learning queue into cards due within limit, and cards due later +/// today. Learning does not need to be sorted in advance, as the sorting is +/// done as the heaps/dequeues are built. +fn split_learning( + learning: Vec, + learn_ahead_secs: i64, +) -> ( + VecDeque, + BinaryHeap>, +) { + let cutoff = TimestampSecs(TimestampSecs::now().0 + learn_ahead_secs); + + // split learning into now and later + let (mut now, later): (Vec<_>, Vec<_>) = learning + .into_iter() + .map(LearningQueueEntry::from) + .partition(|c| c.due <= cutoff); + + // sort due items in ascending order, as we pop the deque from the front + now.sort_unstable_by(|a, b| a.due.cmp(&b.due)); + // partition() requires both outputs to be the same, so we need to create the deque + // separately + let now = VecDeque::from(now); + + // build the binary min heap + let later: BinaryHeap<_> = later.into_iter().map(Reverse).collect(); + + (now, later) +} + +impl Collection { + pub(crate) fn build_queues(&mut self, deck_id: DeckID) -> Result { + let now = TimestampSecs::now(); + let timing = self.timing_for_timestamp(now)?; + let (decks, parent_count) = self.storage.deck_with_parents_and_children(deck_id)?; + let config = self.storage.get_deck_config_map()?; + let limits = remaining_limits_capped_to_parents(&decks, &config, timing.days_elapsed); + let selected_deck_limits = limits[parent_count]; + + let mut queues = QueueBuilder::default(); + + for (deck, mut limit) in decks.iter().zip(limits).skip(parent_count) { + if limit.review > 0 { + self.storage.for_each_due_card_in_deck( + timing.days_elapsed, + timing.next_day_at, + deck.id, + |queue, card| queues.add_due_card(&mut limit, queue, card), + )?; + } + if limit.new > 0 { + self.storage.for_each_new_card_in_deck(deck.id, |card| { + queues.add_new_card(&mut limit, card) + })?; + } + } + + let queues = queues.build( + selected_deck_limits, + self.learn_ahead_secs(), + deck_id, + timing.days_elapsed, + ); + + Ok(queues) + } +} diff --git a/rslib/src/scheduler/queue/builder/sized_chain.rs b/rslib/src/scheduler/queue/builder/sized_chain.rs new file mode 100644 index 000000000..9cb34da9e --- /dev/null +++ b/rslib/src/scheduler/queue/builder/sized_chain.rs @@ -0,0 +1,80 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +/// The standard Rust chain does not implement ExactSizeIterator, and we need +/// to keep track of size so we can intersperse. +pub(crate) struct SizedChain { + one: I, + two: I2, + one_idx: usize, + two_idx: usize, + one_len: usize, + two_len: usize, +} + +impl SizedChain +where + I: ExactSizeIterator, + I2: ExactSizeIterator, +{ + pub fn new(one: I, two: I2) -> Self { + let one_len = one.len(); + let two_len = two.len(); + SizedChain { + one, + two, + one_idx: 0, + two_idx: 0, + one_len, + two_len, + } + } +} + +impl Iterator for SizedChain +where + I: ExactSizeIterator, + I2: ExactSizeIterator, +{ + type Item = I::Item; + + fn next(&mut self) -> Option { + if self.one_idx < self.one_len { + self.one_idx += 1; + self.one.next() + } else if self.two_idx < self.two_len { + self.two_idx += 1; + self.two.next() + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = (self.one_len + self.two_len) - (self.one_idx + self.two_idx); + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for SizedChain +where + I: ExactSizeIterator, + I2: ExactSizeIterator, +{ +} + +#[cfg(test)] +mod test { + use super::SizedChain; + + fn chain(a: &[u32], b: &[u32]) -> Vec { + SizedChain::new(a.iter().cloned(), b.iter().cloned()).collect() + } + + #[test] + fn sized_chain() { + let a = &[1, 2, 3]; + let b = &[11, 22, 33]; + assert_eq!(&chain(a, b), &[1, 2, 3, 11, 22, 33]); + } +} diff --git a/rslib/src/scheduler/queue/builder/sorting.rs b/rslib/src/scheduler/queue/builder/sorting.rs new file mode 100644 index 000000000..2ba28c54a --- /dev/null +++ b/rslib/src/scheduler/queue/builder/sorting.rs @@ -0,0 +1,85 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{DueCard, NewCard, NewCardOrder, QueueBuilder, ReviewCardOrder}; +use fnv::FnvHasher; +use std::{cmp::Ordering, hash::Hasher}; + +impl QueueBuilder { + pub(super) fn sort_new(&mut self) { + match self.new_order { + NewCardOrder::Random => { + self.new.iter_mut().for_each(NewCard::hash_id_and_mtime); + self.new.sort_unstable_by(shuffle_new_card); + } + NewCardOrder::Due => { + self.new.sort_unstable_by(|a, b| a.due.cmp(&b.due)); + } + } + } + + pub(super) fn sort_reviews(&mut self) { + self.review.iter_mut().for_each(DueCard::hash_id_and_mtime); + self.day_learning + .iter_mut() + .for_each(DueCard::hash_id_and_mtime); + + match self.review_order { + ReviewCardOrder::ShuffledByDay => { + self.review.sort_unstable_by(shuffle_by_day); + self.day_learning.sort_unstable_by(shuffle_by_day); + } + ReviewCardOrder::Shuffled => { + self.review.sort_unstable_by(shuffle_due_card); + self.day_learning.sort_unstable_by(shuffle_due_card); + } + ReviewCardOrder::IntervalsAscending => { + self.review.sort_unstable_by(intervals_ascending); + self.day_learning.sort_unstable_by(shuffle_due_card); + } + ReviewCardOrder::IntervalsDescending => { + self.review + .sort_unstable_by(|a, b| intervals_ascending(b, a)); + self.day_learning.sort_unstable_by(shuffle_due_card); + } + } + } +} + +fn shuffle_new_card(a: &NewCard, b: &NewCard) -> Ordering { + a.extra.cmp(&b.extra) +} + +fn shuffle_by_day(a: &DueCard, b: &DueCard) -> Ordering { + (a.due, a.hash).cmp(&(b.due, b.hash)) +} + +fn shuffle_due_card(a: &DueCard, b: &DueCard) -> Ordering { + a.hash.cmp(&b.hash) +} + +fn intervals_ascending(a: &DueCard, b: &DueCard) -> Ordering { + (a.interval, a.hash).cmp(&(a.interval, b.hash)) +} + +// We sort based on a hash so that if the queue is rebuilt, remaining +// cards come back in the same approximate order (mixing + due learning cards +// may still result in a different card) + +impl DueCard { + fn hash_id_and_mtime(&mut self) { + let mut hasher = FnvHasher::default(); + hasher.write_i64(self.id.0); + hasher.write_i64(self.mtime.0); + self.hash = hasher.finish(); + } +} + +impl NewCard { + fn hash_id_and_mtime(&mut self) { + let mut hasher = FnvHasher::default(); + hasher.write_i64(self.id.0); + hasher.write_i64(self.mtime.0); + self.extra = hasher.finish(); + } +} diff --git a/rslib/src/scheduler/queue/entry.rs b/rslib/src/scheduler/queue/entry.rs new file mode 100644 index 000000000..ec80b083f --- /dev/null +++ b/rslib/src/scheduler/queue/entry.rs @@ -0,0 +1,81 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{LearningQueueEntry, MainQueueEntry, MainQueueEntryKind}; +use crate::card::CardQueue; +use crate::prelude::*; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum QueueEntry { + IntradayLearning(LearningQueueEntry), + Main(MainQueueEntry), +} + +impl QueueEntry { + pub fn card_id(&self) -> CardID { + match self { + QueueEntry::IntradayLearning(e) => e.id, + QueueEntry::Main(e) => e.id, + } + } + + pub fn mtime(&self) -> TimestampSecs { + match self { + QueueEntry::IntradayLearning(e) => e.mtime, + QueueEntry::Main(e) => e.mtime, + } + } + + pub fn kind(&self) -> QueueEntryKind { + match self { + QueueEntry::IntradayLearning(_e) => QueueEntryKind::Learning, + QueueEntry::Main(e) => match e.kind { + MainQueueEntryKind::New => QueueEntryKind::New, + MainQueueEntryKind::Review => QueueEntryKind::Review, + }, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum QueueEntryKind { + New, + Review, + Learning, +} + +impl From<&Card> for QueueEntry { + fn from(card: &Card) -> Self { + let kind = match card.queue { + CardQueue::Learn | CardQueue::PreviewRepeat => { + return QueueEntry::IntradayLearning(LearningQueueEntry { + due: TimestampSecs(card.due as i64), + id: card.id, + mtime: card.mtime, + }); + } + CardQueue::New => MainQueueEntryKind::New, + CardQueue::Review | CardQueue::DayLearn => MainQueueEntryKind::Review, + CardQueue::Suspended | CardQueue::SchedBuried | CardQueue::UserBuried => { + unreachable!() + } + }; + QueueEntry::Main(MainQueueEntry { + id: card.id, + mtime: card.mtime, + kind, + }) + } +} + +impl From for QueueEntry { + fn from(e: LearningQueueEntry) -> Self { + Self::IntradayLearning(e) + } +} + +impl From for QueueEntry { + fn from(e: MainQueueEntry) -> Self { + Self::Main(e) + } +} diff --git a/rslib/src/scheduler/queue/learning.rs b/rslib/src/scheduler/queue/learning.rs new file mode 100644 index 000000000..e00764b8a --- /dev/null +++ b/rslib/src/scheduler/queue/learning.rs @@ -0,0 +1,174 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::{ + cmp::{Ordering, Reverse}, + collections::VecDeque, +}; + +use super::CardQueues; +use crate::{prelude::*, scheduler::timing::SchedTimingToday}; + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] +pub(crate) struct LearningQueueEntry { + // due comes first, so the derived ordering sorts by due + pub due: TimestampSecs, + pub id: CardID, + pub mtime: TimestampSecs, +} + +impl CardQueues { + /// Check for any newly due cards, and then return the first, if any, + /// that is due before now. + pub(super) fn next_learning_entry_due_before_now( + &mut self, + now: TimestampSecs, + ) -> Option { + let learn_ahead_cutoff = now.adding_secs(self.learn_ahead_secs); + self.check_for_newly_due_learning_cards(learn_ahead_cutoff); + self.next_learning_entry_learning_ahead() + .filter(|c| c.due <= now) + } + + /// Check for due learning cards up to the learn ahead limit. + /// Does not check for newly due cards, as that is already done by + /// next_learning_entry_due_before_now() + pub(super) fn next_learning_entry_learning_ahead(&self) -> Option { + self.due_learning.front().copied() + } + + pub(super) fn pop_learning_entry(&mut self, id: CardID) -> Option { + if let Some(top) = self.due_learning.front() { + if top.id == id { + self.counts.learning -= 1; + return self.due_learning.pop_front(); + } + } + + // fixme: remove this in the future + // the current python unit tests answer learning cards before they're due, + // so for now we also check the head of the later_due queue + if let Some(top) = self.later_learning.peek() { + if top.0.id == id { + // self.counts.learning -= 1; + return self.later_learning.pop().map(|c| c.0); + } + } + + None + } + + /// Given the just-answered `card`, place it back in the learning queues if it's still + /// due today. Avoid placing it in a position where it would be shown again immediately. + pub(super) fn maybe_requeue_learning_card( + &mut self, + card: &Card, + timing: SchedTimingToday, + ) -> Option { + // not due today? + if !card.is_intraday_learning() || card.due >= timing.next_day_at as i32 { + return None; + } + + let entry = LearningQueueEntry { + due: TimestampSecs(card.due as i64), + id: card.id, + mtime: card.mtime, + }; + + Some(self.requeue_learning_entry(entry, timing)) + } + + /// Caller must have validated learning entry is due today. + pub(super) fn requeue_learning_entry( + &mut self, + mut entry: LearningQueueEntry, + timing: SchedTimingToday, + ) -> LearningQueueEntry { + let learn_ahead_limit = timing.now.adding_secs(self.learn_ahead_secs); + + if entry.due < learn_ahead_limit { + if self.learning_collapsed() { + if let Some(next) = self.due_learning.front() { + if next.due >= entry.due { + // the earliest due card is due later than this one; make this one + // due after that one + entry.due = next.due.adding_secs(1); + } + self.push_due_learning_card(entry); + } else { + // nothing else waiting to review; make this due in a minute + entry.due = learn_ahead_limit.adding_secs(60); + self.later_learning.push(Reverse(entry)); + } + } else { + // not collapsed; can add normally + self.push_due_learning_card(entry); + } + } else { + // due outside current learn ahead limit, but later today + self.later_learning.push(Reverse(entry)); + } + + entry + } + + fn learning_collapsed(&self) -> bool { + self.main.is_empty() + } + + /// Adds card, maintaining correct sort order, and increments learning count. + pub(super) fn push_due_learning_card(&mut self, entry: LearningQueueEntry) { + self.counts.learning += 1; + let target_idx = + binary_search_by(&self.due_learning, |e| e.due.cmp(&entry.due)).unwrap_or_else(|e| e); + self.due_learning.insert(target_idx, entry); + } + + fn check_for_newly_due_learning_cards(&mut self, cutoff: TimestampSecs) { + while let Some(earliest) = self.later_learning.peek() { + if earliest.0.due > cutoff { + break; + } + let entry = self.later_learning.pop().unwrap().0; + self.push_due_learning_card(entry); + } + } + + pub(super) fn remove_requeued_learning_card_after_undo(&mut self, id: CardID) { + let due_idx = self + .due_learning + .iter() + .enumerate() + .find_map(|(idx, entry)| if entry.id == id { Some(idx) } else { None }); + if let Some(idx) = due_idx { + self.counts.learning -= 1; + self.due_learning.remove(idx); + } else { + // card may be in the later_learning binary heap - we can't remove + // it in place, so we have to rebuild it + self.later_learning = self + .later_learning + .drain() + .filter(|e| e.0.id != id) + .collect(); + } + } +} + +/// Adapted from the Rust stdlib VecDeque implementation; we can drop this when the following +/// lands: https://github.com/rust-lang/rust/issues/78021 +fn binary_search_by<'a, F, T>(deque: &'a VecDeque, mut f: F) -> Result +where + F: FnMut(&'a T) -> Ordering, +{ + let (front, back) = deque.as_slices(); + + match back.first().map(|elem| f(elem)) { + Some(Ordering::Less) | Some(Ordering::Equal) => back + .binary_search_by(f) + .map(|idx| idx + front.len()) + .map_err(|idx| idx + front.len()), + _ => front.binary_search_by(f), + } +} diff --git a/rslib/src/scheduler/queue/limits.rs b/rslib/src/scheduler/queue/limits.rs new file mode 100644 index 000000000..744f72dec --- /dev/null +++ b/rslib/src/scheduler/queue/limits.rs @@ -0,0 +1,218 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{Deck, DeckKind}; +use crate::deckconf::{DeckConf, DeckConfID}; +use std::collections::HashMap; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct RemainingLimits { + pub review: u32, + pub new: u32, +} + +impl RemainingLimits { + pub(crate) fn new(deck: &Deck, config: Option<&DeckConf>, today: u32) -> Self { + if let Some(config) = config { + let (new_today, rev_today) = deck.new_rev_counts(today); + RemainingLimits { + review: ((config.inner.reviews_per_day as i32) - rev_today).max(0) as u32, + new: ((config.inner.new_per_day as i32) - new_today).max(0) as u32, + } + } else { + RemainingLimits { + review: std::u32::MAX, + new: std::u32::MAX, + } + } + } + + fn limit_to_parent(&mut self, parent: RemainingLimits) { + self.review = self.review.min(parent.review); + self.new = self.new.min(parent.new); + } +} + +pub(super) fn remaining_limits_capped_to_parents( + decks: &[Deck], + config: &HashMap, + today: u32, +) -> Vec { + let mut limits = get_remaining_limits(decks, config, today); + cap_limits_to_parents(decks.iter().map(|d| d.name.as_str()), &mut limits); + limits +} + +/// Return the remaining limits for each of the provided decks, in +/// the provided deck order. +fn get_remaining_limits( + decks: &[Deck], + config: &HashMap, + today: u32, +) -> Vec { + decks + .iter() + .map(move |deck| { + // get deck config if not filtered + let config = if let DeckKind::Normal(normal) = &deck.kind { + config.get(&DeckConfID(normal.config_id)) + } else { + None + }; + RemainingLimits::new(deck, config, today) + }) + .collect() +} + +/// Given a sorted list of deck names and their current limits, +/// cap child limits to their parents. +fn cap_limits_to_parents<'a>( + names: impl IntoIterator, + limits: &'a mut Vec, +) { + let mut parent_limits = vec![]; + let mut last_limit = None; + let mut last_level = 0; + + names + .into_iter() + .zip(limits.iter_mut()) + .for_each(|(name, limits)| { + let level = name.matches('\x1f').count() + 1; + if last_limit.is_none() { + // top-level deck + last_limit = Some(*limits); + last_level = level; + } else { + // add/remove parent limits if descending/ascending + let mut target = level; + while target != last_level { + if target < last_level { + // current deck is at higher level than previous + parent_limits.pop(); + target += 1; + } else { + // current deck is at a lower level than previous. this + // will push the same remaining counts multiple times if + // the deck tree is missing a parent + parent_limits.push(last_limit.unwrap()); + target -= 1; + } + } + + // apply current parent limit + limits.limit_to_parent(*parent_limits.last().unwrap()); + last_level = level; + last_limit = Some(*limits); + } + }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn limits() { + let limits_map = vec![ + ( + "A", + RemainingLimits { + review: 100, + new: 20, + }, + ), + ( + "A\x1fB", + RemainingLimits { + review: 50, + new: 30, + }, + ), + ( + "A\x1fC", + RemainingLimits { + review: 10, + new: 10, + }, + ), + ("A\x1fC\x1fD", RemainingLimits { review: 5, new: 30 }), + ( + "A\x1fE", + RemainingLimits { + review: 200, + new: 100, + }, + ), + ]; + let (names, mut limits): (Vec<_>, Vec<_>) = limits_map.into_iter().unzip(); + cap_limits_to_parents(names.into_iter(), &mut limits); + assert_eq!( + &limits, + &[ + RemainingLimits { + review: 100, + new: 20 + }, + RemainingLimits { + review: 50, + new: 20 + }, + RemainingLimits { + review: 10, + new: 10 + }, + RemainingLimits { review: 5, new: 10 }, + RemainingLimits { + review: 100, + new: 20 + } + ] + ); + + // missing parents should not break it + let limits_map = vec![ + ( + "A", + RemainingLimits { + review: 100, + new: 20, + }, + ), + ( + "A\x1fB\x1fC\x1fD", + RemainingLimits { + review: 50, + new: 30, + }, + ), + ( + "A\x1fC", + RemainingLimits { + review: 100, + new: 100, + }, + ), + ]; + + let (names, mut limits): (Vec<_>, Vec<_>) = limits_map.into_iter().unzip(); + cap_limits_to_parents(names.into_iter(), &mut limits); + assert_eq!( + &limits, + &[ + RemainingLimits { + review: 100, + new: 20 + }, + RemainingLimits { + review: 50, + new: 20 + }, + RemainingLimits { + review: 100, + new: 20 + }, + ] + ); + } +} diff --git a/rslib/src/scheduler/queue/main.rs b/rslib/src/scheduler/queue/main.rs new file mode 100644 index 000000000..718ba9e5a --- /dev/null +++ b/rslib/src/scheduler/queue/main.rs @@ -0,0 +1,38 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::CardQueues; +use crate::prelude::*; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct MainQueueEntry { + pub id: CardID, + pub mtime: TimestampSecs, + pub kind: MainQueueEntryKind, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum MainQueueEntryKind { + New, + Review, +} + +impl CardQueues { + pub(super) fn next_main_entry(&self) -> Option { + self.main.front().copied() + } + + pub(super) fn pop_main_entry(&mut self, id: CardID) -> Option { + if let Some(last) = self.main.front() { + if last.id == id { + match last.kind { + MainQueueEntryKind::New => self.counts.new -= 1, + MainQueueEntryKind::Review => self.counts.review -= 1, + } + return self.main.pop_front(); + } + } + + None + } +} diff --git a/rslib/src/scheduler/queue/mod.rs b/rslib/src/scheduler/queue/mod.rs new file mode 100644 index 000000000..64d38d1e0 --- /dev/null +++ b/rslib/src/scheduler/queue/mod.rs @@ -0,0 +1,220 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +mod builder; +mod entry; +mod learning; +mod limits; +mod main; +pub(crate) mod undo; + +use std::{ + cmp::Reverse, + collections::{BinaryHeap, VecDeque}, +}; + +use crate::{backend_proto as pb, prelude::*, timestamp::TimestampSecs}; +pub(crate) use builder::{DueCard, NewCard}; +pub(crate) use { + entry::{QueueEntry, QueueEntryKind}, + learning::LearningQueueEntry, + main::{MainQueueEntry, MainQueueEntryKind}, +}; + +use self::undo::QueueUpdate; + +use super::{states::NextCardStates, timing::SchedTimingToday}; + +#[derive(Debug)] +pub(crate) struct CardQueues { + counts: Counts, + + /// Any undone items take precedence. + undo: Vec, + + main: VecDeque, + + due_learning: VecDeque, + + later_learning: BinaryHeap>, + + selected_deck: DeckID, + current_day: u32, + learn_ahead_secs: i64, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct Counts { + pub new: usize, + pub learning: usize, + pub review: usize, +} + +#[derive(Debug)] +pub(crate) struct QueuedCard { + pub card: Card, + pub kind: QueueEntryKind, + pub next_states: NextCardStates, +} + +pub(crate) struct QueuedCards { + pub cards: Vec, + pub new_count: usize, + pub learning_count: usize, + pub review_count: usize, +} + +impl CardQueues { + /// Get the next due card, if there is one. + fn next_entry(&mut self, now: TimestampSecs) -> Option { + self.next_undo_entry() + .map(Into::into) + .or_else(|| self.next_learning_entry_due_before_now(now).map(Into::into)) + .or_else(|| self.next_main_entry().map(Into::into)) + .or_else(|| self.next_learning_entry_learning_ahead().map(Into::into)) + } + + /// Remove the provided card from the top of the queues. + /// If it was not at the top, return an error. + fn pop_answered(&mut self, id: CardID) -> Result { + if let Some(entry) = self.pop_undo_entry(id) { + Ok(entry) + } else if let Some(entry) = self.pop_main_entry(id) { + Ok(entry.into()) + } else if let Some(entry) = self.pop_learning_entry(id) { + Ok(entry.into()) + } else { + Err(AnkiError::invalid_input("not at top of queue")) + } + } + + pub(crate) fn counts(&self) -> Counts { + self.counts + } + + fn is_stale(&self, deck: DeckID, current_day: u32) -> bool { + self.selected_deck != deck || self.current_day != current_day + } + + fn update_after_answering_card( + &mut self, + card: &Card, + timing: SchedTimingToday, + ) -> Result> { + let entry = self.pop_answered(card.id)?; + let requeued_learning = self.maybe_requeue_learning_card(card, timing); + + Ok(Box::new(QueueUpdate { + entry, + learning_requeue: requeued_learning, + })) + } +} + +impl Collection { + pub(crate) fn get_queued_cards( + &mut self, + fetch_limit: u32, + intraday_learning_only: bool, + ) -> Result { + if let Some(next_cards) = self.next_cards(fetch_limit, intraday_learning_only)? { + Ok(pb::GetQueuedCardsOut { + value: Some(pb::get_queued_cards_out::Value::QueuedCards( + next_cards.into(), + )), + }) + } else { + Ok(pb::GetQueuedCardsOut { + value: Some(pb::get_queued_cards_out::Value::CongratsInfo( + self.congrats_info()?, + )), + }) + } + } + + /// This is automatically done when transact() is called for everything + /// except card answers, so unless you are modifying state outside of a + /// transaction, you probably don't need this. + pub(crate) fn clear_study_queues(&mut self) { + self.state.card_queues = None; + } + + pub(crate) fn update_queues_after_answering_card( + &mut self, + card: &Card, + timing: SchedTimingToday, + ) -> Result<()> { + if let Some(queues) = &mut self.state.card_queues { + let mutation = queues.update_after_answering_card(card, timing)?; + self.save_queue_update_undo(mutation); + Ok(()) + } else { + // we currenly allow the queues to be empty for unit tests + Ok(()) + } + } + + pub(crate) fn get_queues(&mut self) -> Result<&mut CardQueues> { + let timing = self.timing_today()?; + let deck = self.get_current_deck_id(); + let need_rebuild = self + .state + .card_queues + .as_ref() + .map(|q| q.is_stale(deck, timing.days_elapsed)) + .unwrap_or(true); + if need_rebuild { + self.state.card_queues = Some(self.build_queues(deck)?); + } + + Ok(self.state.card_queues.as_mut().unwrap()) + } + + fn next_cards( + &mut self, + _fetch_limit: u32, + _intraday_learning_only: bool, + ) -> Result> { + let queues = self.get_queues()?; + let mut cards = vec![]; + if let Some(entry) = queues.next_entry(TimestampSecs::now()) { + let card = self + .storage + .get_card(entry.card_id())? + .ok_or(AnkiError::NotFound)?; + if card.mtime != entry.mtime() { + return Err(AnkiError::invalid_input( + "bug: card modified without updating queue", + )); + } + + // fixme: pass in card instead of id + let next_states = self.get_next_card_states(card.id)?; + + cards.push(QueuedCard { + card, + next_states, + kind: entry.kind(), + }); + } + + if cards.is_empty() { + Ok(None) + } else { + let counts = self.get_queues()?.counts(); + Ok(Some(QueuedCards { + cards, + new_count: counts.new, + learning_count: counts.learning, + review_count: counts.review, + })) + } + } + + #[cfg(test)] + pub(crate) fn next_card(&mut self) -> Result> { + Ok(self + .next_cards(1, false)? + .map(|mut resp| resp.cards.pop().unwrap())) + } +} diff --git a/rslib/src/scheduler/queue/undo.rs b/rslib/src/scheduler/queue/undo.rs new file mode 100644 index 000000000..865a30d7c --- /dev/null +++ b/rslib/src/scheduler/queue/undo.rs @@ -0,0 +1,79 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{CardQueues, LearningQueueEntry, QueueEntry, QueueEntryKind}; +use crate::prelude::*; + +#[derive(Debug)] +pub(crate) enum UndoableQueueChange { + CardAnswered(Box), + CardAnswerUndone(Box), +} + +#[derive(Debug)] +pub(crate) struct QueueUpdate { + pub entry: QueueEntry, + pub learning_requeue: Option, +} + +impl Collection { + pub(crate) fn undo_queue_change(&mut self, change: UndoableQueueChange) -> Result<()> { + match change { + UndoableQueueChange::CardAnswered(update) => { + let queues = self.get_queues()?; + if let Some(learning) = &update.learning_requeue { + queues.remove_requeued_learning_card_after_undo(learning.id); + } + queues.push_undo_entry(update.entry); + self.save_undo(UndoableQueueChange::CardAnswerUndone(update)); + + Ok(()) + } + UndoableQueueChange::CardAnswerUndone(update) => { + // don't try to update existing queue when redoing; just + // rebuild it instead + self.clear_study_queues(); + // but preserve undo state for a subsequent undo + self.save_undo(UndoableQueueChange::CardAnswered(update)); + + Ok(()) + } + } + } + + pub(super) fn save_queue_update_undo(&mut self, change: Box) { + self.save_undo(UndoableQueueChange::CardAnswered(change)) + } +} + +impl CardQueues { + pub(super) fn next_undo_entry(&self) -> Option { + self.undo.last().copied() + } + + pub(super) fn pop_undo_entry(&mut self, id: CardID) -> Option { + if let Some(last) = self.undo.last() { + if last.card_id() == id { + match last.kind() { + QueueEntryKind::New => self.counts.new -= 1, + QueueEntryKind::Review => self.counts.review -= 1, + QueueEntryKind::Learning => self.counts.learning -= 1, + } + return self.undo.pop(); + } + } + + None + } + + /// Add an undone card back to the 'front' of the list, and update + /// the counts. + pub(super) fn push_undo_entry(&mut self, entry: QueueEntry) { + match entry.kind() { + QueueEntryKind::New => self.counts.new += 1, + QueueEntryKind::Review => self.counts.review += 1, + QueueEntryKind::Learning => self.counts.learning += 1, + } + self.undo.push(entry); + } +} diff --git a/rslib/src/scheduler/reviews.rs b/rslib/src/scheduler/reviews.rs index 45bfbc58e..1f540740b 100644 --- a/rslib/src/scheduler/reviews.rs +++ b/rslib/src/scheduler/reviews.rs @@ -96,7 +96,7 @@ impl Collection { let days_from_today = distribution.sample(&mut rng); card.set_due_date(today, days_from_today, spec.force_reset); col.log_manually_scheduled_review(&card, &original, usn)?; - col.update_card(&mut card, &original, usn)?; + col.update_card_inner(&mut card, &original, usn)?; } col.storage.clear_searched_cards_table()?; Ok(()) diff --git a/rslib/src/scheduler/states/filtered.rs b/rslib/src/scheduler/states/filtered.rs index a74966df0..b0de406a9 100644 --- a/rslib/src/scheduler/states/filtered.rs +++ b/rslib/src/scheduler/states/filtered.rs @@ -37,7 +37,7 @@ impl FilteredState { pub(crate) fn review_state(self) -> Option { match self { - FilteredState::Preview(state) => state.original_state.review_state(), + FilteredState::Preview(_) => None, FilteredState::Rescheduling(state) => state.original_state.review_state(), } } diff --git a/rslib/src/scheduler/states/normal.rs b/rslib/src/scheduler/states/normal.rs index 9b59c1b05..25c47cb81 100644 --- a/rslib/src/scheduler/states/normal.rs +++ b/rslib/src/scheduler/states/normal.rs @@ -64,6 +64,10 @@ impl NormalState { NormalState::Relearning(RelearnState { review, .. }) => Some(review), } } + + pub(crate) fn leeched(self) -> bool { + self.review_state().map(|r| r.leeched).unwrap_or_default() + } } impl From for NormalState { diff --git a/rslib/src/scheduler/states/preview_filter.rs b/rslib/src/scheduler/states/preview_filter.rs index 97173211c..b22cb2445 100644 --- a/rslib/src/scheduler/states/preview_filter.rs +++ b/rslib/src/scheduler/states/preview_filter.rs @@ -1,12 +1,12 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use super::{IntervalKind, NextCardStates, NormalState, StateContext}; +use super::{IntervalKind, NextCardStates, StateContext}; #[derive(Debug, Clone, Copy, PartialEq)] pub struct PreviewState { pub scheduled_secs: u32, - pub original_state: NormalState, + pub finished: bool, } impl PreviewState { @@ -22,9 +22,22 @@ impl PreviewState { ..self } .into(), - hard: self.original_state.into(), - good: self.original_state.into(), - easy: self.original_state.into(), + hard: PreviewState { + // ~15 minutes with the default setting + scheduled_secs: ctx.with_learning_fuzz(ctx.preview_step * 90), + ..self + } + .into(), + good: PreviewState { + scheduled_secs: ctx.with_learning_fuzz(ctx.preview_step * 120), + ..self + } + .into(), + easy: PreviewState { + scheduled_secs: 0, + finished: true, + } + .into(), } } } diff --git a/rslib/src/scheduler/timespan.rs b/rslib/src/scheduler/timespan.rs index 4c5ce2749..5c1f34d55 100644 --- a/rslib/src/scheduler/timespan.rs +++ b/rslib/src/scheduler/timespan.rs @@ -22,7 +22,9 @@ pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String { /// Times within the collapse time are represented like '<10m' pub fn answer_button_time_collapsible(seconds: u32, collapse_secs: u32, i18n: &I18n) -> String { let string = answer_button_time(seconds as f32, i18n); - if seconds < collapse_secs { + if seconds == 0 { + i18n.tr(TR::SchedulingEnd).into() + } else if seconds < collapse_secs { format!("<{}", string) } else { string diff --git a/rslib/src/scheduler/cutoff.rs b/rslib/src/scheduler/timing.rs similarity index 97% rename from rslib/src/scheduler/cutoff.rs rename to rslib/src/scheduler/timing.rs index c7727e46c..e5de473c5 100644 --- a/rslib/src/scheduler/cutoff.rs +++ b/rslib/src/scheduler/timing.rs @@ -6,6 +6,7 @@ use chrono::{Date, Duration, FixedOffset, Local, TimeZone, Timelike}; #[derive(Debug, PartialEq, Clone, Copy)] pub struct SchedTimingToday { + pub now: TimestampSecs, /// The number of days that have passed since the collection was created. pub days_elapsed: u32, /// Timestamp of the next day rollover. @@ -43,6 +44,7 @@ pub fn sched_timing_today_v2_new( let days_elapsed = days_elapsed(created_date, today, rollover_passed); SchedTimingToday { + now: current_secs, days_elapsed, next_day_at, } @@ -119,6 +121,7 @@ fn sched_timing_today_v1(crt: TimestampSecs, now: TimestampSecs) -> SchedTimingT let days_elapsed = (now.0 - crt.0) / 86_400; let next_day_at = crt.0 + (days_elapsed + 1) * 86_400; SchedTimingToday { + now, days_elapsed: days_elapsed as u32, next_day_at, } @@ -147,6 +150,7 @@ fn sched_timing_today_v2_legacy( } SchedTimingToday { + now, days_elapsed: days_elapsed as u32, next_day_at, } @@ -213,16 +217,6 @@ mod test { today.days_elapsed } - #[test] - #[cfg(target_vendor = "apple")] - /// On Linux, TZ needs to be set prior to the process being started to take effect, - /// so we limit this test to Macs. - fn local_minutes_west() { - // -480 throughout the year - std::env::set_var("TZ", "Australia/Perth"); - assert_eq!(local_minutes_west_for_stamp(Utc::now().timestamp()), -480); - } - #[test] fn days_elapsed() { let local_offset = local_minutes_west_for_stamp(Utc::now().timestamp()); @@ -361,6 +355,7 @@ mod test { assert_eq!( sched_timing_today_v1(TimestampSecs(1575226800), now), SchedTimingToday { + now, days_elapsed: 107, next_day_at: 1584558000 } @@ -369,6 +364,7 @@ mod test { assert_eq!( sched_timing_today_v2_legacy(TimestampSecs(1533564000), 0, now, aest_offset()), SchedTimingToday { + now, days_elapsed: 589, next_day_at: 1584540000 } @@ -377,6 +373,7 @@ mod test { assert_eq!( sched_timing_today_v2_legacy(TimestampSecs(1524038400), 4, now, aest_offset()), SchedTimingToday { + now, days_elapsed: 700, next_day_at: 1584554400 } diff --git a/rslib/src/scheduler/upgrade.rs b/rslib/src/scheduler/upgrade.rs index c7a5db9c9..f981943dc 100644 --- a/rslib/src/scheduler/upgrade.rs +++ b/rslib/src/scheduler/upgrade.rs @@ -10,7 +10,7 @@ use crate::{ search::SortMode, }; -use super::cutoff::local_minutes_west_for_stamp; +use super::timing::local_minutes_west_for_stamp; struct V1FilteredDeckInfo { /// True if the filtered deck had rescheduling enabled. @@ -39,7 +39,12 @@ impl Card { self.remaining_steps = self.remaining_steps.min(step_count); } - if !info.reschedule { + if info.reschedule { + // only new cards should be in the new queue + if self.queue == CardQueue::New && self.ctype != CardType::New { + self.restore_queue_from_type(); + } + } else { // preview cards start in the review queue in v2 if self.queue == CardQueue::New { self.queue = CardQueue::Review; @@ -182,5 +187,15 @@ mod test { })); assert_eq!(c.ctype, CardType::New); assert_eq!(c.queue, CardQueue::PreviewRepeat); + + // (early) reviews should be moved back from the new queue + c.ctype = CardType::Review; + c.queue = CardQueue::New; + c.upgrade_to_v2(Some(V1FilteredDeckInfo { + reschedule: true, + original_step_count: None, + })); + assert_eq!(c.ctype, CardType::Review); + assert_eq!(c.queue, CardQueue::Review); } } diff --git a/rslib/src/search/cards.rs b/rslib/src/search/cards.rs index 4661f5f84..f4bc802eb 100644 --- a/rslib/src/search/cards.rs +++ b/rslib/src/search/cards.rs @@ -6,7 +6,11 @@ use super::{ sqlwriter::{RequiredTable, SqlWriter}, }; use crate::{ - card::CardID, card::CardType, collection::Collection, config::SortKind, err::Result, + card::CardID, + card::CardType, + collection::Collection, + config::{BoolKey, SortKind}, + err::Result, search::parser::parse, }; @@ -124,7 +128,7 @@ impl Collection { if mode == &SortMode::FromConfig { *mode = SortMode::Builtin { kind: self.get_browser_sort_kind(), - reverse: self.get_browser_sort_reverse(), + reverse: self.get_bool(BoolKey::BrowserSortBackwards), } } } diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index f65f93832..4c0c82dd1 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -9,6 +9,7 @@ use crate::{ err::Result, notes::field_checksum, notetype::NoteTypeID, + prelude::*, storage::ids_to_string, text::{ is_glob, matches_glob, normalize_to_nfc, strip_html_preserving_media_filenames, @@ -28,7 +29,7 @@ pub(crate) struct SqlWriter<'a> { impl SqlWriter<'_> { pub(crate) fn new(col: &mut Collection) -> SqlWriter<'_> { - let normalize_note_text = col.normalize_note_text(); + let normalize_note_text = col.get_bool(BoolKey::NormalizeNoteText); let sql = String::new(); let args = vec![]; SqlWriter { diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 549613e4a..f6d58e544 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -126,7 +126,7 @@ impl Collection { }) } - fn card_stats_to_string(&self, cs: CardStats) -> Result { + fn card_stats_to_string(&mut self, cs: CardStats) -> Result { let offset = self.local_utc_offset_for_user()?; let i18n = &self.i18n; diff --git a/rslib/src/stats/graphs.rs b/rslib/src/stats/graphs.rs index 5d52c159b..fb376541c 100644 --- a/rslib/src/stats/graphs.rs +++ b/rslib/src/stats/graphs.rs @@ -2,7 +2,11 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use crate::{ - backend_proto as pb, config::Weekday, prelude::*, revlog::RevlogEntry, search::SortMode, + backend_proto as pb, + config::{BoolKey, Weekday}, + prelude::*, + revlog::RevlogEntry, + search::SortMode, }; impl Collection { @@ -16,7 +20,7 @@ impl Collection { self.graph_data(all, days) } - fn graph_data(&self, all: bool, days: u32) -> Result { + fn graph_data(&mut self, all: bool, days: u32) -> Result { let timing = self.timing_today()?; let revlog_start = TimestampSecs(if days > 0 { timing.next_day_at - (((days as i64) + 1) * 86_400) @@ -50,21 +54,24 @@ impl Collection { pub(crate) fn get_graph_preferences(&self) -> Result { Ok(pb::GraphPreferences { calendar_first_day_of_week: self.get_first_day_of_week() as i32, - card_counts_separate_inactive: self.get_card_counts_separate_inactive(), + card_counts_separate_inactive: self.get_bool(BoolKey::CardCountsSeparateInactive), browser_links_supported: true, - future_due_show_backlog: self.get_future_due_show_backlog(), + future_due_show_backlog: self.get_bool(BoolKey::FutureDueShowBacklog), }) } - pub(crate) fn set_graph_preferences(&self, prefs: pb::GraphPreferences) -> Result<()> { + pub(crate) fn set_graph_preferences(&mut self, prefs: pb::GraphPreferences) -> Result<()> { self.set_first_day_of_week(match prefs.calendar_first_day_of_week { 1 => Weekday::Monday, 5 => Weekday::Friday, 6 => Weekday::Saturday, _ => Weekday::Sunday, })?; - self.set_card_counts_separate_inactive(prefs.card_counts_separate_inactive)?; - self.set_future_due_show_backlog(prefs.future_due_show_backlog)?; + self.set_bool( + BoolKey::CardCountsSeparateInactive, + prefs.card_counts_separate_inactive, + )?; + self.set_bool(BoolKey::FutureDueShowBacklog, prefs.future_due_show_backlog)?; Ok(()) } } diff --git a/rslib/src/stats/today.rs b/rslib/src/stats/today.rs index 3886dddfe..0a75d04c4 100644 --- a/rslib/src/stats/today.rs +++ b/rslib/src/stats/today.rs @@ -18,10 +18,9 @@ pub fn studied_today(cards: u32, secs: f32, i18n: &I18n) -> String { } impl Collection { - pub fn studied_today(&self) -> Result { - let today = self - .storage - .studied_today(self.timing_today()?.next_day_at)?; + pub fn studied_today(&mut self) -> Result { + let timing = self.timing_today()?; + let today = self.storage.studied_today(timing.next_day_at)?; Ok(studied_today(today.cards, today.seconds as f32, &self.i18n)) } } diff --git a/rslib/src/storage/card/due_cards.sql b/rslib/src/storage/card/due_cards.sql new file mode 100644 index 000000000..96241c48e --- /dev/null +++ b/rslib/src/storage/card/due_cards.sql @@ -0,0 +1,18 @@ +SELECT queue, + id, + nid, + due, + cast(ivl AS integer), + cast(mod AS integer) +FROM cards +WHERE did = ?1 + AND ( + ( + queue IN (2, 3) + AND due <= ?2 + ) + OR ( + queue IN (1, 4) + AND due <= ?3 + ) + ) \ No newline at end of file diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 4b9fa7168..40e476ff2 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -7,7 +7,10 @@ use crate::{ decks::{Deck, DeckID, DeckKind}, err::Result, notes::NoteID, - scheduler::congrats::CongratsInfo, + scheduler::{ + congrats::CongratsInfo, + queue::{DueCard, NewCard}, + }, timestamp::{TimestampMillis, TimestampSecs}, types::Usn, }; @@ -125,7 +128,7 @@ impl super::SqliteStorage { Ok(()) } - /// Add or update card, using the provided ID. Used when syncing. + /// Add or update card, using the provided ID. Used for syncing & undoing. pub(crate) fn add_or_update_card(&self, card: &Card) -> Result<()> { let mut stmt = self.db.prepare_cached(include_str!("add_or_update.sql"))?; stmt.execute(params![ @@ -159,6 +162,68 @@ impl super::SqliteStorage { Ok(()) } + /// Call func() for each due card, stopping when it returns false + /// or no more cards found. + pub(crate) fn for_each_due_card_in_deck( + &self, + day_cutoff: u32, + learn_cutoff: i64, + deck: DeckID, + mut func: F, + ) -> Result<()> + where + F: FnMut(CardQueue, DueCard) -> bool, + { + let mut stmt = self.db.prepare_cached(include_str!("due_cards.sql"))?; + let mut rows = stmt.query(params![ + // with many subdecks, avoiding named params shaves off a few milliseconds + deck, + day_cutoff, + learn_cutoff + ])?; + while let Some(row) = rows.next()? { + let queue: CardQueue = row.get(0)?; + if !func( + queue, + DueCard { + id: row.get(1)?, + note_id: row.get(2)?, + due: row.get(3).ok().unwrap_or_default(), + interval: row.get(4)?, + mtime: row.get(5)?, + hash: 0, + }, + ) { + break; + } + } + + Ok(()) + } + + /// Call func() for each new card, stopping when it returns false + /// or no more cards found. Cards will arrive in (deck_id, due) order. + pub(crate) fn for_each_new_card_in_deck(&self, deck: DeckID, mut func: F) -> Result<()> + where + F: FnMut(NewCard) -> bool, + { + let mut stmt = self.db.prepare_cached(include_str!("new_cards.sql"))?; + let mut rows = stmt.query(params![deck])?; + while let Some(row) = rows.next()? { + if !func(NewCard { + id: row.get(0)?, + note_id: row.get(1)?, + due: row.get(2)?, + extra: row.get::<_, u32>(3)? as u64, + mtime: row.get(4)?, + }) { + break; + } + } + + Ok(()) + } + /// Fix some invalid card properties, and return number of changed cards. pub(crate) fn fix_card_properties( &self, @@ -247,6 +312,28 @@ impl super::SqliteStorage { .collect() } + /// Place matching card ids into the search table. + pub(crate) fn search_siblings_for_bury( + &self, + cid: CardID, + nid: NoteID, + include_new: bool, + include_reviews: bool, + ) -> Result<()> { + self.setup_searched_cards_table()?; + self.db + .prepare_cached(include_str!("siblings_for_bury.sql"))? + .execute(params![ + cid, + nid, + include_new, + CardQueue::New as i8, + include_reviews, + CardQueue::Review as i8 + ])?; + Ok(()) + } + pub(crate) fn note_ids_of_cards(&self, cids: &[CardID]) -> Result> { let mut stmt = self .db diff --git a/rslib/src/storage/card/new_cards.sql b/rslib/src/storage/card/new_cards.sql new file mode 100644 index 000000000..aa8ec5987 --- /dev/null +++ b/rslib/src/storage/card/new_cards.sql @@ -0,0 +1,8 @@ +SELECT id, + nid, + due, + ord, + cast(mod AS integer) +FROM cards +WHERE did = ? + AND queue = 0 \ No newline at end of file diff --git a/rslib/src/storage/card/siblings_for_bury.sql b/rslib/src/storage/card/siblings_for_bury.sql new file mode 100644 index 000000000..4f8ad09e4 --- /dev/null +++ b/rslib/src/storage/card/siblings_for_bury.sql @@ -0,0 +1,15 @@ +INSERT INTO search_cids +SELECT id +FROM cards +WHERE id != ? + AND nid = ? + AND ( + ( + ? + AND queue = ? + ) + OR ( + ? + AND queue = ? + ) + ); \ No newline at end of file diff --git a/rslib/src/storage/config/get_entry.sql b/rslib/src/storage/config/get_entry.sql new file mode 100644 index 000000000..be64ae323 --- /dev/null +++ b/rslib/src/storage/config/get_entry.sql @@ -0,0 +1,5 @@ +SELECT val, + usn, + mtime_secs +FROM config +WHERE KEY = ? \ No newline at end of file diff --git a/rslib/src/storage/config/mod.rs b/rslib/src/storage/config/mod.rs index e12ad8186..d404002e4 100644 --- a/rslib/src/storage/config/mod.rs +++ b/rslib/src/storage/config/mod.rs @@ -2,24 +2,17 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::SqliteStorage; -use crate::{err::Result, timestamp::TimestampSecs, types::Usn}; +use crate::{config::ConfigEntry, err::Result, timestamp::TimestampSecs, types::Usn}; use rusqlite::{params, NO_PARAMS}; -use serde::{de::DeserializeOwned, Serialize}; +use serde::de::DeserializeOwned; use serde_json::Value; use std::collections::HashMap; impl SqliteStorage { - pub(crate) fn set_config_value( - &self, - key: &str, - val: &T, - usn: Usn, - mtime: TimestampSecs, - ) -> Result<()> { - let json = serde_json::to_vec(val)?; + pub(crate) fn set_config_entry(&self, entry: &ConfigEntry) -> Result<()> { self.db .prepare_cached(include_str!("add.sql"))? - .execute(params![key, usn, mtime, &json])?; + .execute(params![&entry.key, entry.usn, entry.mtime, &entry.value])?; Ok(()) } @@ -41,6 +34,33 @@ impl SqliteStorage { .transpose() } + /// Return the raw bytes and other metadata, for undoing. + pub(crate) fn get_config_entry(&self, key: &str) -> Result>> { + self.db + .prepare_cached(include_str!("get_entry.sql"))? + .query_and_then(&[key], |row| { + Ok(ConfigEntry::boxed( + key, + row.get(0)?, + row.get(1)?, + row.get(2)?, + )) + })? + .next() + .transpose() + } + + /// Prefix is expected to end with '_'. + pub(crate) fn get_config_prefix(&self, prefix: &str) -> Result)>> { + let mut end = prefix.to_string(); + assert_eq!(end.pop(), Some('_')); + end.push(std::char::from_u32('_' as u32 + 1).unwrap()); + self.db + .prepare("select key, val from config where key > ? and key < ?")? + .query_and_then(params![prefix, &end], |row| Ok((row.get(0)?, row.get(1)?)))? + .collect() + } + pub(crate) fn get_all_config(&self) -> Result> { self.db .prepare("select key, val from config")? @@ -59,7 +79,12 @@ impl SqliteStorage { ) -> Result<()> { self.db.execute("delete from config", NO_PARAMS)?; for (key, val) in conf.iter() { - self.set_config_value(key, val, usn, mtime)?; + self.set_config_entry(&ConfigEntry::boxed( + key, + serde_json::to_vec(&val)?, + usn, + mtime, + ))?; } Ok(()) } diff --git a/rslib/src/storage/deck/add_or_update_deck.sql b/rslib/src/storage/deck/add_or_update_deck.sql new file mode 100644 index 000000000..7e35c1902 --- /dev/null +++ b/rslib/src/storage/deck/add_or_update_deck.sql @@ -0,0 +1,3 @@ +INSERT + OR REPLACE INTO decks (id, name, mtime_secs, usn, common, kind) +VALUES (?, ?, ?, ?, ?, ?) \ No newline at end of file diff --git a/rslib/src/storage/deck/mod.rs b/rslib/src/storage/deck/mod.rs index b69224c0c..14e50597c 100644 --- a/rslib/src/storage/deck/mod.rs +++ b/rslib/src/storage/deck/mod.rs @@ -100,23 +100,53 @@ impl SqliteStorage { .db .prepare(include_str!("alloc_id.sql"))? .query_row(&[TimestampMillis::now()], |r| r.get(0))?; - self.update_deck(deck).map_err(|err| { - // restore id of 0 - deck.id.0 = 0; - err - }) + self.add_or_update_deck_with_existing_id(deck) + .map_err(|err| { + // restore id of 0 + deck.id.0 = 0; + err + }) } pub(crate) fn update_deck(&self, deck: &Deck) -> Result<()> { if deck.id.0 == 0 { return Err(AnkiError::invalid_input("deck with id 0")); } - self.add_or_update_deck(deck) + let mut stmt = self.db.prepare_cached(include_str!("update_deck.sql"))?; + let mut common = vec![]; + deck.common.encode(&mut common)?; + let kind_enum = DeckKindProto { + kind: Some(deck.kind.clone()), + }; + let mut kind = vec![]; + kind_enum.encode(&mut kind)?; + let count = stmt.execute(params![ + deck.name, + deck.mtime_secs, + deck.usn, + common, + kind, + deck.id + ])?; + + if count == 0 { + Err(AnkiError::invalid_input( + "update_deck() called with non-existent deck", + )) + } else { + Ok(()) + } } - /// Used for syncing; will keep existing ID. - pub(crate) fn add_or_update_deck(&self, deck: &Deck) -> Result<()> { - let mut stmt = self.db.prepare_cached(include_str!("update_deck.sql"))?; + /// Used for syncing; will keep existing ID. Shouldn't be used to add new decks locally, + /// since it does not allocate an id. + pub(crate) fn add_or_update_deck_with_existing_id(&self, deck: &Deck) -> Result<()> { + if deck.id.0 == 0 { + return Err(AnkiError::invalid_input("deck with id 0")); + } + let mut stmt = self + .db + .prepare_cached(include_str!("add_or_update_deck.sql"))?; let mut common = vec![]; deck.common.encode(&mut common)?; let kind_enum = DeckKindProto { @@ -162,6 +192,38 @@ impl SqliteStorage { .collect() } + /// Return the provided deck with its parents and children in an ordered list, and + /// the number of parent decks that need to be skipped to get to the chosen deck. + pub(crate) fn deck_with_parents_and_children( + &self, + deck_id: DeckID, + ) -> Result<(Vec, usize)> { + let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?; + let mut parents = self.parent_decks(&deck)?; + parents.reverse(); + let parent_count = parents.len(); + + let prefix_start = format!("{}\x1f", deck.name); + let prefix_end = format!("{}\x20", deck.name); + parents.push(deck); + + let decks = parents + .into_iter() + .map(Result::Ok) + .chain( + self.db + .prepare_cached(concat!( + include_str!("get_deck.sql"), + " where name > ? and name < ?" + ))? + .query_and_then(&[prefix_start, prefix_end], row_to_deck)?, + ) + .collect::>()?; + + Ok((decks, parent_count)) + } + + /// Return the parents of `child`, with the most immediate parent coming first. pub(crate) fn parent_decks(&self, child: &Deck) -> Result> { let mut decks: Vec = vec![]; while let Some(parent_name) = @@ -170,6 +232,9 @@ impl SqliteStorage { if let Some(parent_did) = self.get_deck_id(parent_name)? { let parent = self.get_deck(parent_did)?.unwrap(); decks.push(parent); + } else { + // missing parent + break; } } @@ -275,7 +340,7 @@ impl SqliteStorage { deck.id.0 = 1; // fixme: separate key deck.name = i18n.tr(TR::DeckConfigDefaultName).into(); - self.update_deck(&deck) + self.add_or_update_deck_with_existing_id(&deck) } pub(crate) fn upgrade_decks_to_schema15(&self, server: bool) -> Result<()> { @@ -297,7 +362,7 @@ impl SqliteStorage { deck.name.push('_'); deck.set_modified(usn); } - self.update_deck(&deck)?; + self.add_or_update_deck_with_existing_id(&deck)?; } self.db.execute("update col set decks = ''", NO_PARAMS)?; Ok(()) diff --git a/rslib/src/storage/deck/update_deck.sql b/rslib/src/storage/deck/update_deck.sql index 7e35c1902..b250edb79 100644 --- a/rslib/src/storage/deck/update_deck.sql +++ b/rslib/src/storage/deck/update_deck.sql @@ -1,3 +1,7 @@ -INSERT - OR REPLACE INTO decks (id, name, mtime_secs, usn, common, kind) -VALUES (?, ?, ?, ?, ?, ?) \ No newline at end of file +UPDATE decks +SET name = ?, + mtime_secs = ?, + usn = ?, + common = ?, + kind = ? +WHERE id = ? \ No newline at end of file diff --git a/rslib/src/storage/deckconf/mod.rs b/rslib/src/storage/deckconf/mod.rs index b57917dbf..17facb35d 100644 --- a/rslib/src/storage/deckconf/mod.rs +++ b/rslib/src/storage/deckconf/mod.rs @@ -9,6 +9,7 @@ use crate::{ }; use prost::Message; use rusqlite::{params, Row, NO_PARAMS}; +use serde_json::Value; use std::collections::HashMap; fn row_to_deckconf(row: &Row) -> Result { @@ -139,13 +140,20 @@ impl SqliteStorage { } pub(super) fn upgrade_deck_conf_to_schema14(&self) -> Result<()> { - let conf = self - .db - .query_row_and_then("select dconf from col", NO_PARAMS, |row| { - let conf: Result> = - serde_json::from_str(row.get_raw(0).as_str()?).map_err(Into::into); - conf - })?; + let conf: HashMap = + self.db + .query_row_and_then("select dconf from col", NO_PARAMS, |row| -> Result<_> { + let text = row.get_raw(0).as_str()?; + // try direct parse + serde_json::from_str(text) + .or_else(|_| { + // failed, and could be caused by duplicate keys. Serialize into + // a value first to discard them, then try again + let conf: Value = serde_json::from_str(text)?; + serde_json::from_value(conf) + }) + .map_err(Into::into) + })?; for (_, mut conf) in conf.into_iter() { self.add_deck_conf_schema14(&mut conf)?; } diff --git a/rslib/src/storage/graves/mod.rs b/rslib/src/storage/graves/mod.rs index 5a18602bd..e9884126f 100644 --- a/rslib/src/storage/graves/mod.rs +++ b/rslib/src/storage/graves/mod.rs @@ -23,13 +23,6 @@ enum GraveKind { } impl SqliteStorage { - fn add_grave(&self, oid: i64, kind: GraveKind, usn: Usn) -> Result<()> { - self.db - .prepare_cached(include_str!("add.sql"))? - .execute(params![usn, oid, kind as u8])?; - Ok(()) - } - pub(crate) fn clear_all_graves(&self) -> Result<()> { self.db.execute("delete from graves", NO_PARAMS)?; Ok(()) @@ -47,6 +40,14 @@ impl SqliteStorage { self.add_grave(did.0, GraveKind::Deck, usn) } + pub(crate) fn remove_card_grave(&self, cid: CardID) -> Result<()> { + self.remove_grave(cid.0, GraveKind::Card) + } + + pub(crate) fn remove_note_grave(&self, nid: NoteID) -> Result<()> { + self.remove_grave(nid.0, GraveKind::Note) + } + pub(crate) fn pending_graves(&self, pending_usn: Usn) -> Result { let mut stmt = self.db.prepare(&format!( "select oid, type from graves where {}", @@ -74,4 +75,19 @@ impl SqliteStorage { .execute(&[new_usn])?; Ok(()) } + + fn add_grave(&self, oid: i64, kind: GraveKind, usn: Usn) -> Result<()> { + self.db + .prepare_cached(include_str!("add.sql"))? + .execute(params![usn, oid, kind as u8])?; + Ok(()) + } + + /// Only useful when undoing + fn remove_grave(&self, oid: i64, kind: GraveKind) -> Result<()> { + self.db + .prepare_cached(include_str!("remove.sql"))? + .execute(params![oid, kind as u8])?; + Ok(()) + } } diff --git a/rslib/src/storage/graves/remove.sql b/rslib/src/storage/graves/remove.sql new file mode 100644 index 000000000..a1430c405 --- /dev/null +++ b/rslib/src/storage/graves/remove.sql @@ -0,0 +1,3 @@ +DELETE FROM graves +WHERE oid = ? + AND type = ? \ No newline at end of file diff --git a/rslib/src/storage/note/get.sql b/rslib/src/storage/note/get.sql index fb1ba3619..53c8129ef 100644 --- a/rslib/src/storage/note/get.sql +++ b/rslib/src/storage/note/get.sql @@ -4,5 +4,7 @@ SELECT id, mod, usn, tags, - flds + flds, + cast(sfld AS text), + csum FROM notes \ No newline at end of file diff --git a/rslib/src/storage/note/mod.rs b/rslib/src/storage/note/mod.rs index c1a6e5aab..9ea67cf0d 100644 --- a/rslib/src/storage/note/mod.rs +++ b/rslib/src/storage/note/mod.rs @@ -21,19 +21,19 @@ pub(crate) fn join_fields(fields: &[String]) -> String { } fn row_to_note(row: &Row) -> Result { - Ok(Note { - id: row.get(0)?, - guid: row.get(1)?, - notetype_id: row.get(2)?, - mtime: row.get(3)?, - usn: row.get(4)?, - tags: split_tags(row.get_raw(5).as_str()?) + Ok(Note::new_from_storage( + row.get(0)?, + row.get(1)?, + row.get(2)?, + row.get(3)?, + row.get(4)?, + split_tags(row.get_raw(5).as_str()?) .map(Into::into) .collect(), - fields: split_fields(row.get_raw(6).as_str()?), - sort_field: None, - checksum: None, - }) + split_fields(row.get_raw(6).as_str()?), + Some(row.get(7)?), + Some(row.get(8).unwrap_or_default()), + )) } impl super::SqliteStorage { @@ -45,7 +45,7 @@ impl super::SqliteStorage { .transpose() } - /// Caller must call note.prepare_for_update() prior to calling this. + /// If fields have been modified, caller must call note.prepare_for_update() prior to calling this. pub(crate) fn update_note(&self, note: &Note) -> Result<()> { assert!(note.id.0 != 0); let mut stmt = self.db.prepare_cached(include_str!("update.sql"))?; diff --git a/rslib/src/storage/revlog/add.sql b/rslib/src/storage/revlog/add.sql index 13f12e073..fae7953b8 100644 --- a/rslib/src/storage/revlog/add.sql +++ b/rslib/src/storage/revlog/add.sql @@ -13,14 +13,15 @@ INSERT VALUES ( ( CASE - WHEN ?1 IN ( + WHEN ?1 + AND ?2 IN ( SELECT id FROM revlog ) THEN ( SELECT max(id) + 1 FROM revlog ) - ELSE ?1 + ELSE ?2 END ), ?, diff --git a/rslib/src/storage/revlog/mod.rs b/rslib/src/storage/revlog/mod.rs index 9d6ef3aaa..a93a5e299 100644 --- a/rslib/src/storage/revlog/mod.rs +++ b/rslib/src/storage/revlog/mod.rs @@ -59,10 +59,16 @@ impl SqliteStorage { Ok(()) } - pub(crate) fn add_revlog_entry(&self, entry: &RevlogEntry) -> Result<()> { + /// Returns the used id, which may differ if `ensure_unique` is true. + pub(crate) fn add_revlog_entry( + &self, + entry: &RevlogEntry, + ensure_unique: bool, + ) -> Result { self.db .prepare_cached(include_str!("add.sql"))? .execute(params![ + ensure_unique, entry.id, entry.cid, entry.usn, @@ -73,7 +79,7 @@ impl SqliteStorage { entry.taken_millis, entry.review_kind as u8 ])?; - Ok(()) + Ok(RevlogID(self.db.last_insert_rowid())) } pub(crate) fn get_revlog_entry(&self, id: RevlogID) -> Result> { @@ -84,6 +90,14 @@ impl SqliteStorage { .transpose() } + /// Only intended to be used by the undo code, as Anki can not sync revlog deletions. + pub(crate) fn remove_revlog_entry(&self, id: RevlogID) -> Result<()> { + self.db + .prepare_cached("delete from revlog where id = ?")? + .execute(&[id])?; + Ok(()) + } + pub(crate) fn get_revlog_entries_for_card(&self, cid: CardID) -> Result> { self.db .prepare_cached(concat!(include_str!("get.sql"), " where cid=?"))? diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 4030de0aa..bbc0e0ff0 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -1,11 +1,11 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::config::schema11_config_as_string; +use crate::config::schema11::schema11_config_as_string; use crate::err::Result; use crate::err::{AnkiError, DBErrorKind}; use crate::timestamp::{TimestampMillis, TimestampSecs}; -use crate::{i18n::I18n, scheduler::cutoff::v1_creation_date, text::without_combining}; +use crate::{i18n::I18n, scheduler::timing::v1_creation_date, text::without_combining}; use regex::Regex; use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS}; use std::cmp::Ordering; diff --git a/rslib/src/storage/tag/mod.rs b/rslib/src/storage/tag/mod.rs index b70fd99f7..c2aac651e 100644 --- a/rslib/src/storage/tag/mod.rs +++ b/rslib/src/storage/tag/mod.rs @@ -65,7 +65,24 @@ impl SqliteStorage { .map_err(Into::into) } - pub(crate) fn clear_tag(&self, tag: &str) -> Result<()> { + // for undo in the future + #[allow(dead_code)] + pub(crate) fn get_tag_and_children(&self, name: &str) -> Result> { + self.db + .prepare_cached("select tag, usn, collapsed from tags where tag regexp ?")? + .query_and_then(&[format!("(?i)^{}($|::)", regex::escape(name))], row_to_tag)? + .collect() + } + + pub(crate) fn remove_single_tag(&self, tag: &str) -> Result<()> { + self.db + .prepare_cached("delete from tags where tag = ?")? + .execute(&[tag])?; + + Ok(()) + } + + pub(crate) fn clear_tag_and_children(&self, tag: &str) -> Result<()> { self.db .prepare_cached("delete from tags where tag regexp ?")? .execute(&[format!("(?i)^{}($|::)", regex::escape(tag))])?; @@ -90,7 +107,7 @@ impl SqliteStorage { Ok(()) } - pub(crate) fn clear_tags(&self) -> Result<()> { + pub(crate) fn clear_all_tags(&self) -> Result<()> { self.db.execute("delete from tags", NO_PARAMS)?; Ok(()) } diff --git a/rslib/src/sync/http_client.rs b/rslib/src/sync/http_client.rs index 5b5faf3a7..bd1b47d9f 100644 --- a/rslib/src/sync/http_client.rs +++ b/rslib/src/sync/http_client.rs @@ -142,11 +142,19 @@ impl SyncServer for HTTPSyncClient { Ok(()) } - /// Download collection into a temporary file, returning it. - /// Caller should persist the file in the correct path after checking it. - /// Progress func must be set first. - async fn full_download(mut self: Box) -> Result { - let mut temp_file = NamedTempFile::new()?; + /// Download collection into a temporary file, returning it. Caller should + /// persist the file in the correct path after checking it. Progress func + /// must be set first. The caller should pass the collection's folder in as + /// the temp folder if it wishes to atomically .persist() it. + async fn full_download( + mut self: Box, + col_folder: Option<&Path>, + ) -> Result { + let mut temp_file = if let Some(folder) = col_folder { + NamedTempFile::new_in(folder) + } else { + NamedTempFile::new() + }?; let (size, mut stream) = self.download_inner().await?; let mut progress = FullSyncProgress { transferred_bytes: 0, @@ -410,7 +418,7 @@ mod test { syncer.set_full_sync_progress_fn(Some(Box::new(|progress, _throttle| { println!("progress: {:?}", progress); }))); - let out_path = syncer.full_download().await?; + let out_path = syncer.full_download(None).await?; let mut syncer = Box::new(HTTPSyncClient::new(None, 0)); syncer.set_full_sync_progress_fn(Some(Box::new(|progress, _throttle| { diff --git a/rslib/src/sync/mod.rs b/rslib/src/sync/mod.rs index 627a7facb..8c899c4e6 100644 --- a/rslib/src/sync/mod.rs +++ b/rslib/src/sync/mod.rs @@ -326,9 +326,10 @@ where SyncActionRequired::NoChanges => Ok(state.into()), SyncActionRequired::FullSyncRequired { .. } => Ok(state.into()), SyncActionRequired::NormalSyncRequired => { + self.col.discard_undo_and_study_queues(); self.col.storage.begin_trx()?; - self.col - .unbury_if_day_rolled_over(self.col.timing_today()?)?; + let timing = self.col.timing_today()?; + self.col.unbury_if_day_rolled_over(timing)?; match self.normal_sync_inner(state).await { Ok(success) => { self.col.storage.commit_trx()?; @@ -672,8 +673,11 @@ impl Collection { pub(crate) async fn full_download_inner(self, server: Box) -> Result<()> { let col_path = self.col_path.clone(); + let col_folder = col_path + .parent() + .ok_or_else(|| AnkiError::invalid_input("couldn't get col_folder"))?; self.close(false)?; - let out_file = server.full_download().await?; + let out_file = server.full_download(Some(col_folder)).await?; // check file ok let db = open_and_check_sqlite_file(out_file.path())?; db.execute_batch("update col set ls=mod")?; @@ -879,7 +883,7 @@ impl Collection { if proceed { let mut deck = deck.into(); self.ensure_deck_name_unique(&mut deck, latest_usn)?; - self.storage.add_or_update_deck(&deck)?; + self.storage.add_or_update_deck_with_existing_id(&deck)?; self.state.deck_cache.remove(&deck.id); } } @@ -901,7 +905,7 @@ impl Collection { Ok(()) } - fn merge_tags(&self, tags: Vec, latest_usn: Usn) -> Result<()> { + fn merge_tags(&mut self, tags: Vec, latest_usn: Usn) -> Result<()> { for tag in tags { self.register_tag(&mut Tag::new(tag, latest_usn))?; } @@ -922,7 +926,7 @@ impl Collection { fn merge_revlog(&self, entries: Vec) -> Result<()> { for entry in entries { - self.storage.add_revlog_entry(&entry)?; + self.storage.add_revlog_entry(&entry, false)?; } Ok(()) } @@ -1131,17 +1135,18 @@ impl From for CardEntry { impl From for Note { fn from(e: NoteEntry) -> Self { - Note { - id: e.id, - guid: e.guid, - notetype_id: e.ntid, - mtime: e.mtime, - usn: e.usn, - tags: split_tags(&e.tags).map(ToString::to_string).collect(), - fields: e.fields.split('\x1f').map(ToString::to_string).collect(), - sort_field: None, - checksum: None, - } + let fields = e.fields.split('\x1f').map(ToString::to_string).collect(); + Note::new_from_storage( + e.id, + e.guid, + e.ntid, + e.mtime, + e.usn, + split_tags(&e.tags).map(ToString::to_string).collect(), + fields, + None, + None, + ) } } @@ -1149,12 +1154,12 @@ impl From for NoteEntry { fn from(e: Note) -> Self { NoteEntry { id: e.id, + fields: e.fields().iter().join("\x1f"), guid: e.guid, ntid: e.notetype_id, mtime: e.mtime, usn: e.usn, tags: join_tags(&e.tags), - fields: e.fields.into_iter().join("\x1f"), sfld: String::new(), csum: String::new(), flags: 0, @@ -1321,7 +1326,7 @@ mod test { fn col1_setup(col: &mut Collection) { let nt = col.get_notetype_by_name("Basic").unwrap().unwrap(); let mut note = nt.new_note(); - note.fields[0] = "1".into(); + note.set_field(0, "1").unwrap(); col.add_note(&mut note, DeckID(1)).unwrap(); // // set our schema time back, so when initial server @@ -1374,8 +1379,10 @@ mod test { let mut deck = col1.get_or_create_normal_deck("new deck")?; // give it a new option group - let mut dconf = DeckConf::default(); - dconf.name = "new dconf".into(); + let mut dconf = DeckConf { + name: "new dconf".into(), + ..Default::default() + }; col1.add_or_update_deck_config(&mut dconf, false)?; if let DeckKind::Normal(deck) = &mut deck.kind { deck.config_id = dconf.id.0; @@ -1389,18 +1396,21 @@ mod test { // add another note+card+tag let mut note = nt.new_note(); - note.fields[0] = "2".into(); + note.set_field(0, "2")?; note.tags.push("tag".into()); col1.add_note(&mut note, deck.id)?; // mock revlog entry - col1.storage.add_revlog_entry(&RevlogEntry { - id: TimestampMillis(123), - cid: CardID(456), - usn: Usn(-1), - interval: 10, - ..Default::default() - })?; + col1.storage.add_revlog_entry( + &RevlogEntry { + id: RevlogID(123), + cid: CardID(456), + usn: Usn(-1), + interval: 10, + ..Default::default() + }, + true, + )?; // config + creation col1.set_config("test", &"test1")?; @@ -1483,7 +1493,7 @@ mod test { // make some modifications let mut note = col2.storage.get_note(note.id)?.unwrap(); - note.fields[1] = "new".into(); + note.set_field(1, "new")?; note.tags.push("tag2".into()); col2.update_note(&mut note)?; @@ -1521,7 +1531,7 @@ mod test { // fixme: inconsistent usn arg col1.remove_cards_and_orphaned_notes(&[cardid])?; let usn = col1.usn()?; - col1.remove_note_only(noteid, usn)?; + col1.remove_note_only_undoable(noteid, usn)?; col1.remove_decks_and_child_decks(vec![deckid])?; let out = ctx.normal_sync(&mut col1).await; diff --git a/rslib/src/sync/server.rs b/rslib/src/sync/server.rs index 93822c063..842f0d37c 100644 --- a/rslib/src/sync/server.rs +++ b/rslib/src/sync/server.rs @@ -36,7 +36,10 @@ pub trait SyncServer { /// If `can_consume` is true, the local server will move or remove the file, instead /// creating a copy. The remote server ignores this argument. async fn full_upload(self: Box, col_path: &Path, can_consume: bool) -> Result<()>; - async fn full_download(self: Box) -> Result; + /// If the calling code intends to .persist() the named temp file to + /// atomically update the collection, it should pass in the collection's + /// folder, as .persist() can't work across filesystems. + async fn full_download(self: Box, temp_folder: Option<&Path>) -> Result; } pub struct LocalServer { @@ -99,6 +102,7 @@ impl SyncServer for LocalServer { self.client_usn = client_usn; self.client_is_newer = client_is_newer; + self.col.discard_undo_and_study_queues(); self.col.storage.begin_rust_trx()?; // make sure any pending cards have been unburied first if necessary @@ -199,7 +203,12 @@ impl SyncServer for LocalServer { fs::rename(col_path, &target_col_path).map_err(Into::into) } - async fn full_download(mut self: Box) -> Result { + /// The provided folder is ignored, as in the server case the local data + /// will be sent over the network, instead of written into a local file. + async fn full_download( + mut self: Box, + _col_folder: Option<&Path>, + ) -> Result { // bump usn/mod & close self.col.transact(None, |col| col.storage.increment_usn())?; let col_path = self.col.col_path.clone(); diff --git a/rslib/src/tags.rs b/rslib/src/tags/mod.rs similarity index 94% rename from rslib/src/tags.rs rename to rslib/src/tags/mod.rs index 15e6e7310..39985dd72 100644 --- a/rslib/src/tags.rs +++ b/rslib/src/tags/mod.rs @@ -1,11 +1,14 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +pub(crate) mod undo; + use crate::{ backend_proto::TagTreeNode, collection::Collection, err::{AnkiError, Result}, notes::{NoteID, TransformNoteOutput}, + prelude::*, text::{normalize_to_nfc, to_re}, types::Usn, }; @@ -195,7 +198,11 @@ impl Collection { /// Given a list of tags, fix case, ordering and duplicates. /// Returns true if any new tags were added. - pub(crate) fn canonify_tags(&self, tags: Vec, usn: Usn) -> Result<(Vec, bool)> { + pub(crate) fn canonify_tags( + &mut self, + tags: Vec, + usn: Usn, + ) -> Result<(Vec, bool)> { let mut seen = HashSet::new(); let mut added = false; @@ -223,7 +230,7 @@ impl Collection { /// in the tags list. True if the tag was added and not already in tag list. /// In the case the tag is already registered, tag will be mutated to match the existing /// name. - pub(crate) fn register_tag(&self, tag: &mut Tag) -> Result { + pub(crate) fn register_tag(&mut self, tag: &mut Tag) -> Result { let normalized_name = normalize_tag_name(&tag.name); if normalized_name.is_empty() { // this should not be possible @@ -238,7 +245,7 @@ impl Collection { } else if let Cow::Owned(new_name) = normalized_name { tag.name = new_name; } - self.storage.register_tag(&tag)?; + self.register_tag_undoable(&tag)?; Ok(true) } } @@ -271,7 +278,7 @@ impl Collection { pub fn clear_unused_tags(&self) -> Result<()> { let expanded: HashSet<_> = self.storage.expanded_tags()?.into_iter().collect(); - self.storage.clear_tags()?; + self.storage.clear_all_tags()?; let usn = self.usn()?; for name in self.storage.all_tags_in_notes()? { let name = normalize_tag_name(&name).into(); @@ -334,7 +341,7 @@ impl Collection { tags: &[Regex], mut repl: R, ) -> Result { - self.transact(None, |col| { + self.transact(Some(UndoableOpKind::UpdateTag), |col| { col.transform_notes(nids, |note, _nt| { let mut changed = false; for re in tags { @@ -376,7 +383,7 @@ impl Collection { } } - pub fn add_tags_for_notes(&mut self, nids: &[NoteID], tags: &str) -> Result { + pub fn add_tags_to_notes(&mut self, nids: &[NoteID], tags: &str) -> Result { let tags: Vec<_> = split_tags(tags).collect(); let matcher = regex::RegexSet::new( tags.iter() @@ -385,7 +392,7 @@ impl Collection { ) .map_err(|_| AnkiError::invalid_input("invalid regex"))?; - self.transact(None, |col| { + self.transact(Some(UndoableOpKind::UpdateTag), |col| { col.transform_notes(nids, |note, _nt| { let mut need_to_add = true; let mut match_count = 0; @@ -472,7 +479,7 @@ impl Collection { self.transact(None, |col| { // clear the existing original tags for (source_tag, _) in &source_tags_and_outputs { - col.storage.clear_tag(source_tag)?; + col.storage.clear_tag_and_children(source_tag)?; } col.transform_notes(&nids, |note, _nt| { @@ -571,13 +578,13 @@ mod test { let note = col.storage.get_note(note.id)?.unwrap(); assert_eq!(note.tags[0], "baz"); - let cnt = col.add_tags_for_notes(&[note.id], "cee aye")?; + let cnt = col.add_tags_to_notes(&[note.id], "cee aye")?; assert_eq!(cnt, 1); let note = col.storage.get_note(note.id)?.unwrap(); assert_eq!(¬e.tags, &["aye", "baz", "cee"]); // if all tags already on note, it doesn't get updated - let cnt = col.add_tags_for_notes(&[note.id], "cee aye")?; + let cnt = col.add_tags_to_notes(&[note.id], "cee aye")?; assert_eq!(cnt, 0); // empty replacement deletes tag @@ -606,11 +613,11 @@ mod test { assert_eq!(¬e.tags, &["barfoo", "foobar"]); // tag children are also cleared when clearing their parent - col.storage.clear_tags()?; + col.storage.clear_all_tags()?; for name in vec!["a", "a::b", "A::b::c"] { col.register_tag(&mut Tag::new(name.to_string(), Usn(0)))?; } - col.storage.clear_tag("a")?; + col.storage.clear_tag_and_children("a")?; assert_eq!(col.storage.all_tags()?, vec![]); Ok(()) @@ -655,9 +662,9 @@ mod test { // differing case should result in only one parent case being added - // the first one - col.storage.clear_tags()?; - *(&mut note.tags[0]) = "foo::BAR::a".into(); - *(&mut note.tags[1]) = "FOO::bar::b".into(); + col.storage.clear_all_tags()?; + note.tags[0] = "foo::BAR::a".into(); + note.tags[1] = "FOO::bar::b".into(); col.update_note(&mut note)?; assert_eq!( col.tag_tree()?, @@ -673,9 +680,9 @@ mod test { ); // things should work even if the immediate parent is not missing - col.storage.clear_tags()?; - *(&mut note.tags[0]) = "foo::bar::baz".into(); - *(&mut note.tags[1]) = "foo::bar::baz::quux".into(); + col.storage.clear_all_tags()?; + note.tags[0] = "foo::bar::baz".into(); + note.tags[1] = "foo::bar::baz::quux".into(); col.update_note(&mut note)?; assert_eq!( col.tag_tree()?, @@ -692,9 +699,9 @@ mod test { // numbers have a smaller ascii number than ':', so a naive sort on // '::' would result in one::two being nested under one1. - col.storage.clear_tags()?; - *(&mut note.tags[0]) = "one".into(); - *(&mut note.tags[1]) = "one1".into(); + col.storage.clear_all_tags()?; + note.tags[0] = "one".into(); + note.tags[1] = "one1".into(); note.tags.push("one::two".into()); col.update_note(&mut note)?; assert_eq!( @@ -707,10 +714,10 @@ mod test { ); // children should match the case of their parents - col.storage.clear_tags()?; - *(&mut note.tags[0]) = "FOO".into(); - *(&mut note.tags[1]) = "foo::BAR".into(); - *(&mut note.tags[2]) = "foo::bar::baz".into(); + col.storage.clear_all_tags()?; + note.tags[0] = "FOO".into(); + note.tags[1] = "foo::BAR".into(); + note.tags[2] = "foo::bar::baz".into(); col.update_note(&mut note)?; assert_eq!(note.tags, vec!["FOO", "FOO::BAR", "FOO::BAR::baz"]); diff --git a/rslib/src/tags/undo.rs b/rslib/src/tags/undo.rs new file mode 100644 index 000000000..907a7ea86 --- /dev/null +++ b/rslib/src/tags/undo.rs @@ -0,0 +1,31 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::Tag; +use crate::prelude::*; + +#[derive(Debug)] +pub(crate) enum UndoableTagChange { + Added(Box), + Removed(Box), +} + +impl Collection { + pub(crate) fn undo_tag_change(&mut self, change: UndoableTagChange) -> Result<()> { + match change { + UndoableTagChange::Added(tag) => self.remove_single_tag_undoable(&tag), + UndoableTagChange::Removed(tag) => self.register_tag_undoable(&tag), + } + } + /// Adds an already-validated tag to the DB and undo list. + /// Caller is responsible for setting usn. + pub(super) fn register_tag_undoable(&mut self, tag: &Tag) -> Result<()> { + self.save_undo(UndoableTagChange::Added(Box::new(tag.clone()))); + self.storage.register_tag(&tag) + } + + fn remove_single_tag_undoable(&mut self, tag: &Tag) -> Result<()> { + self.save_undo(UndoableTagChange::Removed(Box::new(tag.clone()))); + self.storage.remove_single_tag(&tag.name) + } +} diff --git a/rslib/src/template.rs b/rslib/src/template.rs index 46000bf4d..129253afd 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -558,27 +558,29 @@ pub fn render_card( .map_err(|e| template_error_to_anki_error(e, true, i18n))?; // check if the front side was empty - if is_cloze { - if cloze_is_empty(field_map, card_ord) { - let info = format!( - "
{}
{}
", - i18n.trn( - TR::CardTemplateRenderingMissingCloze, - tr_args!["number"=>card_ord+1] - ), - TEMPLATE_BLANK_CLOZE_LINK, - i18n.tr(TR::CardTemplateRenderingMoreInfo) - ); - qnodes.push(RenderedNode::Text { text: info }); - } - } else if !qtmpl.renders_with_fields(context.nonempty_fields) { - let info = format!( + let empty_message = if is_cloze && cloze_is_empty(field_map, card_ord) { + Some(format!( + "
{}
{}
", + i18n.trn( + TR::CardTemplateRenderingMissingCloze, + tr_args!["number"=>card_ord+1] + ), + TEMPLATE_BLANK_CLOZE_LINK, + i18n.tr(TR::CardTemplateRenderingMoreInfo) + )) + } else if !is_cloze && !qtmpl.renders_with_fields(context.nonempty_fields) { + Some(format!( "
{}
{}
", i18n.tr(TR::CardTemplateRenderingEmptyFront), TEMPLATE_BLANK_LINK, i18n.tr(TR::CardTemplateRenderingMoreInfo) - ); - qnodes.push(RenderedNode::Text { text: info }); + )) + } else { + None + }; + if let Some(text) = empty_message { + qnodes.push(RenderedNode::Text { text: text.clone() }); + return Ok((qnodes, vec![RenderedNode::Text { text }])); } // answer side diff --git a/rslib/src/timestamp.rs b/rslib/src/timestamp.rs index ebadf757e..f8aadd1df 100644 --- a/rslib/src/timestamp.rs +++ b/rslib/src/timestamp.rs @@ -35,6 +35,10 @@ impl TimestampSecs { pub fn datetime(self, utc_offset: FixedOffset) -> DateTime { utc_offset.timestamp(self.0, 0) } + + pub fn adding_secs(self, secs: i64) -> Self { + TimestampSecs(self.0 + secs) + } } impl TimestampMillis { @@ -62,7 +66,7 @@ fn elapsed() -> time::Duration { let mut elap = time::SystemTime::now() .duration_since(time::SystemTime::UNIX_EPOCH) .unwrap(); - let now = Local::now(); + let now = Utc::now(); if now.hour() >= 2 && now.hour() < 4 { elap -= time::Duration::from_secs(60 * 60 * 2); } diff --git a/rslib/src/undo.rs b/rslib/src/undo.rs deleted file mode 100644 index bc84add78..000000000 --- a/rslib/src/undo.rs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -use crate::{ - collection::{Collection, CollectionOp}, - err::Result, - types::Usn, -}; -use std::fmt; - -pub(crate) trait Undoable: fmt::Debug + Send { - /// Undo the recorded action. - fn apply(&self, ctx: &mut Collection, usn: Usn) -> Result<()>; -} - -#[derive(Debug)] -struct UndoStep { - kind: CollectionOp, - changes: Vec>, -} - -#[derive(Debug, PartialEq)] -enum UndoMode { - NormalOp, - Undoing, - Redoing, -} - -impl Default for UndoMode { - fn default() -> Self { - Self::NormalOp - } -} - -#[derive(Debug, Default)] -pub(crate) struct UndoManager { - undo_steps: Vec, - redo_steps: Vec, - mode: UndoMode, - current_step: Option, -} - -impl UndoManager { - pub(crate) fn save_undoable(&mut self, item: Box) { - if let Some(step) = self.current_step.as_mut() { - step.changes.push(item) - } - } - - pub(crate) fn begin_step(&mut self, op: Option) { - if op.is_none() { - // action doesn't support undoing; clear the queue - self.undo_steps.clear(); - self.redo_steps.clear(); - } else if self.mode == UndoMode::NormalOp { - // a normal op clears the redo queue - self.redo_steps.clear(); - } - self.current_step = op.map(|op| UndoStep { - kind: op, - changes: vec![], - }); - } - - pub(crate) fn end_step(&mut self) { - if let Some(step) = self.current_step.take() { - if self.mode == UndoMode::Undoing { - self.redo_steps.push(step); - } else { - self.undo_steps.push(step); - } - } - } - - pub(crate) fn discard_step(&mut self) { - self.begin_step(None) - } - - fn can_undo(&self) -> Option { - self.undo_steps.last().map(|s| s.kind.clone()) - } - - fn can_redo(&self) -> Option { - self.redo_steps.last().map(|s| s.kind.clone()) - } -} - -impl Collection { - pub fn can_undo(&self) -> Option { - self.state.undo.can_undo() - } - - pub fn can_redo(&self) -> Option { - self.state.undo.can_redo() - } - - pub fn undo(&mut self) -> Result<()> { - if let Some(step) = self.state.undo.undo_steps.pop() { - let changes = step.changes; - self.state.undo.mode = UndoMode::Undoing; - let res = self.transact(Some(step.kind), |col| { - let usn = col.usn()?; - for change in changes.iter().rev() { - change.apply(col, usn)?; - } - Ok(()) - }); - self.state.undo.mode = UndoMode::NormalOp; - res?; - } - Ok(()) - } - - pub fn redo(&mut self) -> Result<()> { - if let Some(step) = self.state.undo.redo_steps.pop() { - let changes = step.changes; - self.state.undo.mode = UndoMode::Redoing; - let res = self.transact(Some(step.kind), |col| { - let usn = col.usn()?; - for change in changes.iter().rev() { - change.apply(col, usn)?; - } - Ok(()) - }); - self.state.undo.mode = UndoMode::NormalOp; - res?; - } - Ok(()) - } -} diff --git a/rslib/src/undo/changes.rs b/rslib/src/undo/changes.rs new file mode 100644 index 000000000..f50eba795 --- /dev/null +++ b/rslib/src/undo/changes.rs @@ -0,0 +1,76 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{ + card::undo::UndoableCardChange, config::undo::UndoableConfigChange, + decks::undo::UndoableDeckChange, notes::undo::UndoableNoteChange, prelude::*, + revlog::undo::UndoableRevlogChange, scheduler::queue::undo::UndoableQueueChange, + tags::undo::UndoableTagChange, +}; + +#[derive(Debug)] +pub(crate) enum UndoableChange { + Card(UndoableCardChange), + Note(UndoableNoteChange), + Deck(UndoableDeckChange), + Tag(UndoableTagChange), + Revlog(UndoableRevlogChange), + Queue(UndoableQueueChange), + Config(UndoableConfigChange), +} + +impl UndoableChange { + pub(super) fn undo(self, col: &mut Collection) -> Result<()> { + match self { + UndoableChange::Card(c) => col.undo_card_change(c), + UndoableChange::Note(c) => col.undo_note_change(c), + UndoableChange::Deck(c) => col.undo_deck_change(c), + UndoableChange::Tag(c) => col.undo_tag_change(c), + UndoableChange::Revlog(c) => col.undo_revlog_change(c), + UndoableChange::Queue(c) => col.undo_queue_change(c), + UndoableChange::Config(c) => col.undo_config_change(c), + } + } +} + +impl From for UndoableChange { + fn from(c: UndoableCardChange) -> Self { + UndoableChange::Card(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableNoteChange) -> Self { + UndoableChange::Note(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableDeckChange) -> Self { + UndoableChange::Deck(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableTagChange) -> Self { + UndoableChange::Tag(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableRevlogChange) -> Self { + UndoableChange::Revlog(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableQueueChange) -> Self { + UndoableChange::Queue(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableConfigChange) -> Self { + UndoableChange::Config(c) + } +} diff --git a/rslib/src/undo/mod.rs b/rslib/src/undo/mod.rs new file mode 100644 index 000000000..339db290a --- /dev/null +++ b/rslib/src/undo/mod.rs @@ -0,0 +1,281 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +mod changes; +mod ops; + +pub(crate) use changes::UndoableChange; +pub use ops::UndoableOpKind; + +use crate::backend_proto as pb; +use crate::prelude::*; +use std::collections::VecDeque; + +const UNDO_LIMIT: usize = 30; + +#[derive(Debug)] +pub(crate) struct UndoableOp { + pub kind: UndoableOpKind, + pub timestamp: TimestampSecs, + pub changes: Vec, +} + +#[derive(Debug, PartialEq)] +enum UndoMode { + NormalOp, + Undoing, + Redoing, +} + +impl Default for UndoMode { + fn default() -> Self { + Self::NormalOp + } +} + +#[derive(Debug, Default)] +pub(crate) struct UndoManager { + // undo steps are added to the front of a double-ended queue, so we can + // efficiently cap the number of steps we retain in memory + undo_steps: VecDeque, + // redo steps are added to the end + redo_steps: Vec, + mode: UndoMode, + current_step: Option, +} + +impl UndoManager { + fn save(&mut self, item: UndoableChange) { + if let Some(step) = self.current_step.as_mut() { + step.changes.push(item) + } + } + + fn begin_step(&mut self, op: Option) { + println!("begin: {:?}", op); + if op.is_none() { + self.undo_steps.clear(); + self.redo_steps.clear(); + } else if self.mode == UndoMode::NormalOp { + // a normal op clears the redo queue + self.redo_steps.clear(); + } + self.current_step = op.map(|op| UndoableOp { + kind: op, + timestamp: TimestampSecs::now(), + changes: vec![], + }); + } + + fn end_step(&mut self) { + if let Some(step) = self.current_step.take() { + if !step.changes.is_empty() { + if self.mode == UndoMode::Undoing { + self.redo_steps.push(step); + } else { + self.undo_steps.truncate(UNDO_LIMIT - 1); + self.undo_steps.push_front(step); + } + } + } + println!("ended, undo steps count now {}", self.undo_steps.len()); + } + + fn current_step_requires_study_queue_reset(&self) -> bool { + self.current_step + .as_ref() + .map(|s| s.kind.needs_study_queue_reset()) + .unwrap_or(true) + } + + fn can_undo(&self) -> Option { + self.undo_steps.front().map(|s| s.kind) + } + + fn can_redo(&self) -> Option { + self.redo_steps.last().map(|s| s.kind) + } + + pub(crate) fn previous_op(&self) -> Option<&UndoableOp> { + self.undo_steps.front() + } +} + +impl Collection { + pub fn can_undo(&self) -> Option { + self.state.undo.can_undo() + } + + pub fn can_redo(&self) -> Option { + self.state.undo.can_redo() + } + + pub fn undo(&mut self) -> Result<()> { + if let Some(step) = self.state.undo.undo_steps.pop_front() { + let changes = step.changes; + self.state.undo.mode = UndoMode::Undoing; + let res = self.transact(Some(step.kind), |col| { + for change in changes.into_iter().rev() { + change.undo(col)?; + } + Ok(()) + }); + self.state.undo.mode = UndoMode::NormalOp; + res?; + } + Ok(()) + } + + pub fn redo(&mut self) -> Result<()> { + if let Some(step) = self.state.undo.redo_steps.pop() { + let changes = step.changes; + self.state.undo.mode = UndoMode::Redoing; + let res = self.transact(Some(step.kind), |col| { + for change in changes.into_iter().rev() { + change.undo(col)?; + } + Ok(()) + }); + self.state.undo.mode = UndoMode::NormalOp; + res?; + } + Ok(()) + } + + pub fn undo_status(&self) -> pb::UndoStatus { + pb::UndoStatus { + undo: self + .can_undo() + .map(|op| self.describe_op_kind(op)) + .unwrap_or_default(), + redo: self + .can_redo() + .map(|op| self.describe_op_kind(op)) + .unwrap_or_default(), + } + } + + /// If op is None, clears the undo/redo queues. + pub(crate) fn begin_undoable_operation(&mut self, op: Option) { + self.state.undo.begin_step(op); + } + + /// Called at the end of a successful transaction. + /// In most instances, this will also clear the study queues. + pub(crate) fn end_undoable_operation(&mut self) { + if self.state.undo.current_step_requires_study_queue_reset() { + self.clear_study_queues(); + } + self.state.undo.end_step(); + } + + pub(crate) fn discard_undo_and_study_queues(&mut self) { + self.state.undo.begin_step(None); + self.clear_study_queues(); + } + + #[inline] + pub(crate) fn save_undo(&mut self, item: impl Into) { + self.state.undo.save(item.into()); + } + + pub(crate) fn previous_undo_op(&self) -> Option<&UndoableOp> { + self.state.undo.previous_op() + } +} + +#[cfg(test)] +mod test { + use crate::card::Card; + use crate::{collection::open_test_collection, prelude::*}; + + #[test] + fn undo() { + let mut col = open_test_collection(); + + let mut card = Card::default(); + card.interval = 1; + col.add_card(&mut card).unwrap(); + let cid = card.id; + + assert_eq!(col.can_undo(), None); + assert_eq!(col.can_redo(), None); + + // outside of a transaction, no undo info recorded + let card = col + .get_and_update_card(cid, |card| { + card.interval = 2; + Ok(()) + }) + .unwrap(); + assert_eq!(card.interval, 2); + assert_eq!(col.can_undo(), None); + assert_eq!(col.can_redo(), None); + + // record a few undo steps + for i in 3..=4 { + col.transact(Some(UndoableOpKind::UpdateCard), |col| { + col.get_and_update_card(cid, |card| { + card.interval = i; + Ok(()) + }) + .unwrap(); + Ok(()) + }) + .unwrap(); + } + + assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 4); + assert_eq!(col.can_undo(), Some(UndoableOpKind::UpdateCard)); + assert_eq!(col.can_redo(), None); + + // undo a step + col.undo().unwrap(); + assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 3); + assert_eq!(col.can_undo(), Some(UndoableOpKind::UpdateCard)); + assert_eq!(col.can_redo(), Some(UndoableOpKind::UpdateCard)); + + // and again + col.undo().unwrap(); + assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 2); + assert_eq!(col.can_undo(), None); + assert_eq!(col.can_redo(), Some(UndoableOpKind::UpdateCard)); + + // redo a step + col.redo().unwrap(); + assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 3); + assert_eq!(col.can_undo(), Some(UndoableOpKind::UpdateCard)); + assert_eq!(col.can_redo(), Some(UndoableOpKind::UpdateCard)); + + // and another + col.redo().unwrap(); + assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 4); + assert_eq!(col.can_undo(), Some(UndoableOpKind::UpdateCard)); + assert_eq!(col.can_redo(), None); + + // and undo the redo + col.undo().unwrap(); + assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 3); + assert_eq!(col.can_undo(), Some(UndoableOpKind::UpdateCard)); + assert_eq!(col.can_redo(), Some(UndoableOpKind::UpdateCard)); + + // if any action is performed, it should clear the redo queue + col.transact(Some(UndoableOpKind::UpdateCard), |col| { + col.get_and_update_card(cid, |card| { + card.interval = 5; + Ok(()) + }) + .unwrap(); + Ok(()) + }) + .unwrap(); + assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 5); + assert_eq!(col.can_undo(), Some(UndoableOpKind::UpdateCard)); + assert_eq!(col.can_redo(), None); + + // and any action that doesn't support undoing will clear both queues + col.transact(None, |_col| Ok(())).unwrap(); + assert_eq!(col.can_undo(), None); + assert_eq!(col.can_redo(), None); + } +} diff --git a/rslib/src/undo/ops.rs b/rslib/src/undo/ops.rs new file mode 100644 index 000000000..dea29e517 --- /dev/null +++ b/rslib/src/undo/ops.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 crate::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UndoableOpKind { + UpdateCard, + AnswerCard, + Bury, + Suspend, + UnburyUnsuspend, + AddNote, + RemoveNote, + UpdateTag, + UpdateNote, +} + +impl UndoableOpKind { + pub(crate) fn needs_study_queue_reset(self) -> bool { + self != UndoableOpKind::AnswerCard + } +} + +impl Collection { + pub fn describe_op_kind(&self, op: UndoableOpKind) -> String { + let key = match op { + UndoableOpKind::UpdateCard => TR::UndoUpdateCard, + UndoableOpKind::AnswerCard => TR::UndoAnswerCard, + UndoableOpKind::Bury => TR::StudyingBury, + UndoableOpKind::Suspend => TR::StudyingSuspend, + UndoableOpKind::UnburyUnsuspend => TR::UndoUnburyUnsuspend, + UndoableOpKind::AddNote => TR::UndoAddNote, + UndoableOpKind::RemoveNote => TR::StudyingDeleteNote, + UndoableOpKind::UpdateTag => TR::UndoUpdateTag, + UndoableOpKind::UpdateNote => TR::UndoUpdateNote, + }; + + self.i18n.tr(key).to_string() + } +} diff --git a/ts/editor/changeTimer.ts b/ts/editor/changeTimer.ts index 5b7ce8153..fc7c22bbe 100644 --- a/ts/editor/changeTimer.ts +++ b/ts/editor/changeTimer.ts @@ -1,21 +1,17 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -import type { EditingArea } from "."; +import type { EditingArea } from "./editingArea"; import { getCurrentField } from "."; import { bridgeCommand } from "./lib"; import { getNoteId } from "./noteId"; -import { updateButtonState } from "./toolbar"; let changeTimer: number | null = null; export function triggerChangeTimer(currentField: EditingArea): void { clearChangeTimer(); - changeTimer = setTimeout(function () { - updateButtonState(); - saveField(currentField, "key"); - }, 600); + changeTimer = setTimeout(() => saveField(currentField, "key"), 600); } function clearChangeTimer(): void { diff --git a/ts/editor/editable.ts b/ts/editor/editable.ts new file mode 100644 index 000000000..69eec5624 --- /dev/null +++ b/ts/editor/editable.ts @@ -0,0 +1,36 @@ +import { nodeIsInline } from "./helpers"; + +function containsInlineContent(field: Element): boolean { + if (field.childNodes.length === 0) { + // for now, for all practical purposes, empty fields are in block mode + return false; + } + + for (const child of field.children) { + if (!nodeIsInline(child)) { + return false; + } + } + + return true; +} + +export class Editable extends HTMLElement { + set fieldHTML(content: string) { + this.innerHTML = content; + + if (containsInlineContent(this)) { + this.appendChild(document.createElement("br")); + } + } + + get fieldHTML(): string { + return containsInlineContent(this) && this.innerHTML.endsWith("
") + ? this.innerHTML.slice(0, -4) // trim trailing
+ : this.innerHTML; + } + + connectedCallback() { + this.setAttribute("contenteditable", ""); + } +} diff --git a/ts/editor/editingArea.ts b/ts/editor/editingArea.ts new file mode 100644 index 000000000..6b7d7c4af --- /dev/null +++ b/ts/editor/editingArea.ts @@ -0,0 +1,114 @@ +import type { Editable } from "./editable"; + +import { bridgeCommand } from "./lib"; +import { onInput, onKey, onKeyUp } from "./inputHandlers"; +import { onFocus, onBlur } from "./focusHandlers"; +import { updateButtonState } from "./toolbar"; + +function onPaste(evt: ClipboardEvent): void { + bridgeCommand("paste"); + evt.preventDefault(); +} + +function onCutOrCopy(): void { + bridgeCommand("cutOrCopy"); +} + +export class EditingArea extends HTMLDivElement { + editable: Editable; + baseStyle: HTMLStyleElement; + + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.className = "field"; + + const rootStyle = document.createElement("link"); + rootStyle.setAttribute("rel", "stylesheet"); + rootStyle.setAttribute("href", "./_anki/css/editable.css"); + this.shadowRoot!.appendChild(rootStyle); + + this.baseStyle = document.createElement("style"); + this.baseStyle.setAttribute("rel", "stylesheet"); + this.shadowRoot!.appendChild(this.baseStyle); + + this.editable = document.createElement("anki-editable") as Editable; + this.shadowRoot!.appendChild(this.editable); + } + + get ord(): number { + return Number(this.getAttribute("ord")); + } + + set fieldHTML(content: string) { + this.editable.fieldHTML = content; + } + + get fieldHTML(): string { + return this.editable.fieldHTML; + } + + connectedCallback(): void { + this.addEventListener("keydown", onKey); + this.addEventListener("keyup", onKeyUp); + this.addEventListener("input", onInput); + this.addEventListener("focus", onFocus); + this.addEventListener("blur", onBlur); + this.addEventListener("paste", onPaste); + this.addEventListener("copy", onCutOrCopy); + this.addEventListener("oncut", onCutOrCopy); + this.addEventListener("mouseup", updateButtonState); + + const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet; + baseStyleSheet.insertRule("anki-editable {}", 0); + } + + disconnectedCallback(): void { + this.removeEventListener("keydown", onKey); + this.removeEventListener("keyup", onKeyUp); + this.removeEventListener("input", onInput); + this.removeEventListener("focus", onFocus); + this.removeEventListener("blur", onBlur); + this.removeEventListener("paste", onPaste); + this.removeEventListener("copy", onCutOrCopy); + this.removeEventListener("oncut", onCutOrCopy); + this.removeEventListener("mouseup", updateButtonState); + } + + initialize(color: string, content: string): void { + this.setBaseColor(color); + this.editable.fieldHTML = content; + } + + setBaseColor(color: string): void { + const styleSheet = this.baseStyle.sheet as CSSStyleSheet; + const firstRule = styleSheet.cssRules[0] as CSSStyleRule; + firstRule.style.color = color; + } + + setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { + const styleSheet = this.baseStyle.sheet as CSSStyleSheet; + const firstRule = styleSheet.cssRules[0] as CSSStyleRule; + firstRule.style.fontFamily = fontFamily; + firstRule.style.fontSize = fontSize; + firstRule.style.direction = direction; + } + + isRightToLeft(): boolean { + const styleSheet = this.baseStyle.sheet as CSSStyleSheet; + const firstRule = styleSheet.cssRules[0] as CSSStyleRule; + return firstRule.style.direction === "rtl"; + } + + getSelection(): Selection { + return this.shadowRoot!.getSelection()!; + } + + focusEditable(): void { + this.editable.focus(); + } + + blurEditable(): void { + this.editable.blur(); + } +} diff --git a/ts/editor/editor.scss b/ts/editor/editor.scss index 30b0af96b..3be5e2b0f 100644 --- a/ts/editor/editor.scss +++ b/ts/editor/editor.scss @@ -1,27 +1,15 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -html { - background: var(--bg-color); +body { + color: var(--text-fg); + background-color: var(--bg-color); } #fields { display: flex; flex-direction: column; margin: 5px; - - & > *, - & > * > * { - margin: 1px 0; - - &:first-child { - margin-top: 0; - } - - &:last-child { - margin-bottom: 0; - } - } } .field { @@ -38,10 +26,6 @@ html { padding: 0; } -body { - margin: 0; -} - #topbutsOuter { display: flex; flex-wrap: wrap; @@ -54,6 +38,7 @@ body { padding: 2px; background: var(--bg-color); + font-size: 13px; } .topbuts { @@ -73,9 +58,11 @@ body { } .topbut { + display: inline-block; width: 16px; height: 16px; margin-top: 4px; + vertical-align: -0.125em; } .rainbow { @@ -122,10 +109,11 @@ button.highlighted { #topbutsright & { border-bottom: 3px solid black; - } + border-radius: 3px; - .nightMode #topbutsright & { - border-bottom: 3px solid white; + .nightMode & { + border-bottom-color: white; + } } } @@ -144,3 +132,16 @@ button.highlighted { color: var(--link); } } + +.icon { + cursor: pointer; + color: var(--text-fg); + + &.is-inactive::before { + opacity: 0.1; + } + + &.icon--hover::before { + opacity: 0.5; + } +} diff --git a/ts/editor/editorField.ts b/ts/editor/editorField.ts new file mode 100644 index 000000000..eb0b3005e --- /dev/null +++ b/ts/editor/editorField.ts @@ -0,0 +1,45 @@ +import type { EditingArea } from "./editingArea"; +import type { LabelContainer } from "./labelContainer"; + +export class EditorField extends HTMLDivElement { + labelContainer: LabelContainer; + editingArea: EditingArea; + + constructor() { + super(); + this.labelContainer = document.createElement("div", { + is: "anki-label-container", + }) as LabelContainer; + this.appendChild(this.labelContainer); + + this.editingArea = document.createElement("div", { + is: "anki-editing-area", + }) as EditingArea; + this.appendChild(this.editingArea); + } + + static get observedAttributes(): string[] { + return ["ord"]; + } + + set ord(n: number) { + this.setAttribute("ord", String(n)); + } + + attributeChangedCallback(name: string, _oldValue: string, newValue: string): void { + switch (name) { + case "ord": + this.editingArea.setAttribute("ord", newValue); + this.labelContainer.setAttribute("ord", newValue); + } + } + + initialize(label: string, color: string, content: string): void { + this.labelContainer.initialize(label); + this.editingArea.initialize(color, content); + } + + setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { + this.editingArea.setBaseStyling(fontFamily, fontSize, direction); + } +} diff --git a/ts/editor/focusHandlers.ts b/ts/editor/focusHandlers.ts index fde2ca7cc..ec676f6db 100644 --- a/ts/editor/focusHandlers.ts +++ b/ts/editor/focusHandlers.ts @@ -1,45 +1,23 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -import type { EditingArea } from "."; +import type { EditingArea } from "./editingArea"; +import { saveField } from "./changeTimer"; import { bridgeCommand } from "./lib"; import { enableButtons, disableButtons } from "./toolbar"; -import { saveField } from "./changeTimer"; - -function caretToEnd(currentField: EditingArea): void { - const range = document.createRange(); - range.selectNodeContents(currentField.editable); - range.collapse(false); - const selection = currentField.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); -} - -function focusField(field: EditingArea) { - field.focusEditable(); - bridgeCommand(`focus:${field.ord}`); - enableButtons(); - caretToEnd(field); -} - -// For distinguishing focus by refocusing window from deliberate focus -let previousActiveElement: EditingArea | null = null; export function onFocus(evt: FocusEvent): void { const currentField = evt.currentTarget as EditingArea; - const previousFocus = evt.relatedTarget as EditingArea; - - if (previousFocus === previousActiveElement || !previousFocus) { - focusField(currentField); - } + currentField.focusEditable(); + bridgeCommand(`focus:${currentField.ord}`); + enableButtons(); } export function onBlur(evt: FocusEvent): void { const previousFocus = evt.currentTarget as EditingArea; + const currentFieldUnchanged = previousFocus === document.activeElement; - saveField(previousFocus, previousFocus === document.activeElement ? "key" : "blur"); - // other widget or window focused; current field unchanged - previousActiveElement = previousFocus; + saveField(previousFocus, currentFieldUnchanged ? "key" : "blur"); disableButtons(); } diff --git a/ts/editor/helpers.ts b/ts/editor/helpers.ts index da7cb5d27..9281af818 100644 --- a/ts/editor/helpers.ts +++ b/ts/editor/helpers.ts @@ -1,6 +1,8 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ +import type { EditingArea } from "./editingArea"; + export function nodeIsElement(node: Node): node is Element { return node.nodeType === Node.ELEMENT_NODE; } @@ -66,3 +68,12 @@ const INLINE_TAGS = [ export function nodeIsInline(node: Node): boolean { return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName); } + +export function caretToEnd(currentField: EditingArea): void { + const range = document.createRange(); + range.selectNodeContents(currentField.editable); + range.collapse(false); + const selection = currentField.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); +} diff --git a/ts/editor/htmlFilter.ts b/ts/editor/htmlFilter.ts index 39a2cad9e..d9fc260f6 100644 --- a/ts/editor/htmlFilter.ts +++ b/ts/editor/htmlFilter.ts @@ -132,13 +132,17 @@ let filterInternalNode = function (elem: Element) { // filtering from external sources let filterNode = function (node: Node, extendedMode: boolean): void { + if (node.nodeType === Node.COMMENT_NODE) { + node.parentNode.removeChild(node); + return; + } if (!nodeIsElement(node)) { return; } // descend first, and take a copy of the child nodes as the loop will skip // elements due to node modifications otherwise - for (const child of [...node.children]) { + for (const child of [...node.childNodes]) { filterNode(child, extendedMode); } diff --git a/ts/editor/index.ts b/ts/editor/index.ts index 5861b3da4..3e02ffa43 100644 --- a/ts/editor/index.ts +++ b/ts/editor/index.ts @@ -1,13 +1,15 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -import { nodeIsInline } from "./helpers"; -import { bridgeCommand } from "./lib"; +import { caretToEnd } from "./helpers"; import { saveField } from "./changeTimer"; import { filterHTML } from "./htmlFilter"; import { updateButtonState } from "./toolbar"; -import { onInput, onKey, onKeyUp } from "./inputHandlers"; -import { onFocus, onBlur } from "./focusHandlers"; + +import { EditorField } from "./editorField"; +import { LabelContainer } from "./labelContainer"; +import { EditingArea } from "./editingArea"; +import { Editable } from "./editable"; export { setNoteId, getNoteId } from "./noteId"; export { preventButtonFocus, toggleEditorButton, setFGButton } from "./toolbar"; @@ -23,6 +25,11 @@ declare global { } } +customElements.define("anki-editable", Editable); +customElements.define("anki-editing-area", EditingArea, { extends: "div" }); +customElements.define("anki-label-container", LabelContainer, { extends: "div" }); +customElements.define("anki-editor-field", EditorField, { extends: "div" }); + export function getCurrentField(): EditingArea | null { return document.activeElement instanceof EditingArea ? document.activeElement @@ -34,6 +41,8 @@ export function focusField(n: number): void { if (field) { field.editingArea.focusEditable(); + caretToEnd(field.editingArea); + updateButtonState(); } } @@ -61,200 +70,6 @@ export function pasteHTML( } } -function onPaste(evt: ClipboardEvent): void { - bridgeCommand("paste"); - evt.preventDefault(); -} - -function onCutOrCopy(): boolean { - bridgeCommand("cutOrCopy"); - return true; -} - -function containsInlineContent(field: Element): boolean { - if (field.childNodes.length === 0) { - // for now, for all practical purposes, empty fields are in block mode - return false; - } - - for (const child of field.children) { - if (!nodeIsInline(child)) { - return false; - } - } - - return true; -} - -class Editable extends HTMLElement { - set fieldHTML(content: string) { - this.innerHTML = content; - - if (containsInlineContent(this)) { - this.appendChild(document.createElement("br")); - } - } - - get fieldHTML(): string { - return containsInlineContent(this) && this.innerHTML.endsWith("
") - ? this.innerHTML.slice(0, -4) // trim trailing
- : this.innerHTML; - } - - connectedCallback() { - this.setAttribute("contenteditable", ""); - } -} - -customElements.define("anki-editable", Editable); - -export class EditingArea extends HTMLDivElement { - editable: Editable; - baseStyle: HTMLStyleElement; - - constructor() { - super(); - this.attachShadow({ mode: "open" }); - this.className = "field"; - - const rootStyle = document.createElement("link"); - rootStyle.setAttribute("rel", "stylesheet"); - rootStyle.setAttribute("href", "./_anki/css/editable.css"); - this.shadowRoot!.appendChild(rootStyle); - - this.baseStyle = document.createElement("style"); - this.baseStyle.setAttribute("rel", "stylesheet"); - this.shadowRoot!.appendChild(this.baseStyle); - - this.editable = document.createElement("anki-editable") as Editable; - this.shadowRoot!.appendChild(this.editable); - } - - get ord(): number { - return Number(this.getAttribute("ord")); - } - - set fieldHTML(content: string) { - this.editable.fieldHTML = content; - } - - get fieldHTML(): string { - return this.editable.fieldHTML; - } - - connectedCallback(): void { - this.addEventListener("keydown", onKey); - this.addEventListener("keyup", onKeyUp); - this.addEventListener("input", onInput); - this.addEventListener("focus", onFocus); - this.addEventListener("blur", onBlur); - this.addEventListener("paste", onPaste); - this.addEventListener("copy", onCutOrCopy); - this.addEventListener("oncut", onCutOrCopy); - this.addEventListener("mouseup", updateButtonState); - - const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet; - baseStyleSheet.insertRule("anki-editable {}", 0); - } - - disconnectedCallback(): void { - this.removeEventListener("keydown", onKey); - this.removeEventListener("keyup", onKeyUp); - this.removeEventListener("input", onInput); - this.removeEventListener("focus", onFocus); - this.removeEventListener("blur", onBlur); - this.removeEventListener("paste", onPaste); - this.removeEventListener("copy", onCutOrCopy); - this.removeEventListener("oncut", onCutOrCopy); - this.removeEventListener("mouseup", updateButtonState); - } - - initialize(color: string, content: string): void { - this.setBaseColor(color); - this.editable.fieldHTML = content; - } - - setBaseColor(color: string): void { - const styleSheet = this.baseStyle.sheet as CSSStyleSheet; - const firstRule = styleSheet.cssRules[0] as CSSStyleRule; - firstRule.style.color = color; - } - - setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { - const styleSheet = this.baseStyle.sheet as CSSStyleSheet; - const firstRule = styleSheet.cssRules[0] as CSSStyleRule; - firstRule.style.fontFamily = fontFamily; - firstRule.style.fontSize = fontSize; - firstRule.style.direction = direction; - } - - isRightToLeft(): boolean { - return this.editable.style.direction === "rtl"; - } - - getSelection(): Selection { - return this.shadowRoot!.getSelection()!; - } - - focusEditable(): void { - this.editable.focus(); - } - - blurEditable(): void { - this.editable.blur(); - } -} - -customElements.define("anki-editing-area", EditingArea, { extends: "div" }); - -export class EditorField extends HTMLDivElement { - labelContainer: HTMLDivElement; - label: HTMLSpanElement; - editingArea: EditingArea; - - constructor() { - super(); - this.labelContainer = document.createElement("div"); - this.labelContainer.className = "fname"; - this.appendChild(this.labelContainer); - - this.label = document.createElement("span"); - this.label.className = "fieldname"; - this.labelContainer.appendChild(this.label); - - this.editingArea = document.createElement("div", { - is: "anki-editing-area", - }) as EditingArea; - this.appendChild(this.editingArea); - } - - static get observedAttributes(): string[] { - return ["ord"]; - } - - set ord(n: number) { - this.setAttribute("ord", String(n)); - } - - attributeChangedCallback(name: string, _oldValue: string, newValue: string): void { - switch (name) { - case "ord": - this.editingArea.setAttribute("ord", newValue); - } - } - - initialize(label: string, color: string, content: string): void { - this.label.innerText = label; - this.editingArea.initialize(color, content); - } - - setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { - this.editingArea.setBaseStyling(fontFamily, fontSize, direction); - } -} - -customElements.define("anki-editor-field", EditorField, { extends: "div" }); - function adjustFieldAmount(amount: number): void { const fieldsContainer = document.getElementById("fields")!; @@ -300,7 +115,7 @@ export function setFields(fields: [string, string][]): void { ); } -export function setBackgrounds(cols: ("dupe" | "")[]) { +export function setBackgrounds(cols: ("dupe" | "")[]): void { forEditorField(cols, (field, value) => field.editingArea.classList.toggle("dupe", value === "dupe") ); @@ -315,6 +130,12 @@ export function setFonts(fonts: [string, number, boolean][]): void { }); } +export function setSticky(stickies: boolean[]): void { + forEditorField(stickies, (field, isSticky) => { + field.labelContainer.activateSticky(isSticky); + }); +} + export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void { document.execCommand(cmd, false, arg); if (!nosave) { diff --git a/ts/editor/inputHandlers.ts b/ts/editor/inputHandlers.ts index f153d1968..8a4d5b28b 100644 --- a/ts/editor/inputHandlers.ts +++ b/ts/editor/inputHandlers.ts @@ -1,9 +1,10 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -import { EditingArea } from "."; -import { nodeIsElement } from "./helpers"; +import { EditingArea } from "./editingArea"; +import { caretToEnd, nodeIsElement } from "./helpers"; import { triggerChangeTimer } from "./changeTimer"; +import { updateButtonState } from "./toolbar"; function inListItem(currentField: EditingArea): boolean { const anchor = currentField.getSelection()!.anchorNode!; @@ -21,6 +22,7 @@ function inListItem(currentField: EditingArea): boolean { export function onInput(event: Event): void { // make sure IME changes get saved triggerChangeTimer(event.currentTarget as EditingArea); + updateButtonState(); } export function onKey(evt: KeyboardEvent): void { @@ -59,6 +61,22 @@ export function onKey(evt: KeyboardEvent): void { triggerChangeTimer(currentField); } +globalThis.addEventListener("keydown", (evt: KeyboardEvent) => { + if (evt.code === "Tab") { + globalThis.addEventListener( + "focusin", + (evt: FocusEvent) => { + const newFocusTarget = evt.target; + if (newFocusTarget instanceof EditingArea) { + caretToEnd(newFocusTarget); + updateButtonState(); + } + }, + { once: true } + ); + } +}); + export function onKeyUp(evt: KeyboardEvent): void { const currentField = evt.currentTarget as EditingArea; diff --git a/ts/editor/labelContainer.ts b/ts/editor/labelContainer.ts new file mode 100644 index 000000000..27148fbbb --- /dev/null +++ b/ts/editor/labelContainer.ts @@ -0,0 +1,67 @@ +import { bridgeCommand } from "./lib"; + +function removeHoverIcon(evt: Event): void { + const icon = evt.currentTarget as HTMLElement; + icon.classList.remove("icon--hover"); +} + +function hoverIcon(evt: Event): void { + const icon = evt.currentTarget as HTMLElement; + icon.classList.add("icon--hover"); +} + +export class LabelContainer extends HTMLDivElement { + sticky: HTMLSpanElement; + label: HTMLSpanElement; + + constructor() { + super(); + this.className = "d-flex justify-content-between"; + + this.label = document.createElement("span"); + this.label.className = "fieldname"; + this.appendChild(this.label); + + this.sticky = document.createElement("span"); + this.sticky.className = "bi me-1 bi-pin-angle icon"; + this.sticky.hidden = true; + this.appendChild(this.sticky); + + this.toggleSticky = this.toggleSticky.bind(this); + } + + connectedCallback(): void { + this.sticky.addEventListener("click", this.toggleSticky); + this.sticky.addEventListener("mouseenter", hoverIcon); + this.sticky.addEventListener("mouseleave", removeHoverIcon); + } + + disconnectedCallback(): void { + this.sticky.removeEventListener("click", this.toggleSticky); + this.sticky.removeEventListener("mouseenter", hoverIcon); + this.sticky.removeEventListener("mouseleave", removeHoverIcon); + } + + initialize(labelName: string): void { + this.label.innerText = labelName; + } + + setSticky(state: boolean): void { + this.sticky.classList.toggle("is-inactive", !state); + } + + activateSticky(initialState: boolean): void { + this.setSticky(initialState); + this.sticky.hidden = false; + } + + toggleSticky(evt: Event): void { + bridgeCommand( + `toggleSticky:${this.getAttribute("ord")}`, + (newState: boolean): void => { + this.setSticky(newState); + } + ); + removeHoverIcon(evt); + } +} diff --git a/ts/editor/toolbar.ts b/ts/editor/toolbar.ts index aa0b7ad5c..a4d87819b 100644 --- a/ts/editor/toolbar.ts +++ b/ts/editor/toolbar.ts @@ -1,9 +1,10 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ +const highlightButtons = ["bold", "italic", "underline", "superscript", "subscript"]; + export function updateButtonState(): void { - const buts = ["bold", "italic", "underline", "superscript", "subscript"]; - for (const name of buts) { + for (const name of highlightButtons) { const elem = document.querySelector(`#${name}`) as HTMLElement; elem.classList.toggle("highlighted", document.queryCommandState(name)); } @@ -12,6 +13,13 @@ export function updateButtonState(): void { // 'col': document.queryCommandValue("forecolor") } +function clearButtonHighlight(): void { + for (const name of highlightButtons) { + const elem = document.querySelector(`#${name}`) as HTMLElement; + elem.classList.remove("highlighted"); + } +} + export function preventButtonFocus(): void { for (const element of document.querySelectorAll("button.linkb")) { element.addEventListener("mousedown", (evt: Event) => { @@ -20,19 +28,34 @@ export function preventButtonFocus(): void { } } -export function disableButtons(): void { - $("button.linkb:not(.perm)").prop("disabled", true); +export function enableButtons(): void { + const buttons = document.querySelectorAll( + "button.linkb" + ) as NodeListOf; + buttons.forEach((elem: HTMLButtonElement): void => { + elem.disabled = false; + }); + updateButtonState(); } -export function enableButtons(): void { - $("button.linkb").prop("disabled", false); +export function disableButtons(): void { + const buttons = document.querySelectorAll( + "button.linkb:not(.perm)" + ) as NodeListOf; + buttons.forEach((elem: HTMLButtonElement): void => { + elem.disabled = true; + }); + clearButtonHighlight(); } export function setFGButton(col: string): void { document.getElementById("forecolor")!.style.backgroundColor = col; } -export function toggleEditorButton(buttonid: string): void { - const button = $(buttonid)[0]; +export function toggleEditorButton(buttonOrId: string | HTMLElement): void { + const button = + typeof buttonOrId === "string" + ? (document.getElementById(buttonOrId) as HTMLElement) + : buttonOrId; button.classList.toggle("highlighted"); } diff --git a/ts/licenses.json b/ts/licenses.json index c6dcc95dc..f2db233a3 100644 --- a/ts/licenses.json +++ b/ts/licenses.json @@ -99,6 +99,21 @@ "path": "node_modules/protobufjs/node_modules/@types/node", "licenseFile": "node_modules/protobufjs/node_modules/@types/node/LICENSE" }, + "bootstrap-icons@1.4.0": { + "licenses": "MIT", + "repository": "https://github.com/twbs/icons", + "publisher": "mdo", + "path": "node_modules/bootstrap-icons", + "licenseFile": "node_modules/bootstrap-icons/LICENSE.md" + }, + "bootstrap@5.0.0-beta2": { + "licenses": "MIT", + "repository": "https://github.com/twbs/bootstrap", + "publisher": "The Bootstrap Authors", + "url": "https://github.com/twbs/bootstrap/graphs/contributors", + "path": "node_modules/bootstrap", + "licenseFile": "node_modules/bootstrap/LICENSE" + }, "commander@2.20.3": { "licenses": "MIT", "repository": "https://github.com/tj/commander.js", diff --git a/ts/package.json b/ts/package.json index 84dbfc85c..553196035 100644 --- a/ts/package.json +++ b/ts/package.json @@ -50,6 +50,8 @@ }, "dependencies": { "@fluent/bundle": "^0.15.1", + "bootstrap": "^5.0.0-beta2", + "bootstrap-icons": "^1.4.0", "css-browser-selector": "^0.6.5", "d3": "^6.5.0", "intl-pluralrules": "^1.2.2", diff --git a/ts/sass/_buttons.scss b/ts/sass/_buttons.scss index 7a6556b38..d0a96de75 100644 --- a/ts/sass/_buttons.scss +++ b/ts/sass/_buttons.scss @@ -7,49 +7,51 @@ $fusion-button-border: #646464; $fusion-button-base-bg: #454545; .isWin { - button { - font-size: 12px; - } + button { + font-size: 12px; + } } .isMac { - button { - font-size: 13px; - } + button { + font-size: 13px; + } } .isLin { - button { - font-size: 14px; + button { + font-size: 14px; - -webkit-appearance: none; - border-radius: 3px; - padding: 5px; - border: 1px solid var(--border); - } + -webkit-appearance: none; + border-radius: 3px; + padding: 5px; + border: 1px solid var(--border); + } } .nightMode { - button { - -webkit-appearance: none; - color: var(--text-fg); + button { + -webkit-appearance: none; + color: var(--text-fg); - /* match the fusion button gradient */ - background: linear-gradient(0deg, + /* match the fusion button gradient */ + background: linear-gradient( + 0deg, $fusion-button-gradient-start 0%, - $fusion-button-gradient-end 100%); - box-shadow: 0 0 3px $fusion-button-outline; - border: 1px solid $fusion-button-border; + $fusion-button-gradient-end 100% + ); + box-shadow: 0 0 3px $fusion-button-outline; + border: 1px solid $fusion-button-border; - border-radius: 2px; - padding: 10px; - padding-top: 3px; - padding-bottom: 3px; - } + border-radius: 2px; + padding: 10px; + padding-top: 3px; + padding-bottom: 3px; + } - button:hover { - background: $fusion-button-hover-bg; - } + button:hover { + background: $fusion-button-hover-bg; + } } /* imitate standard macOS dark mode buttons */ diff --git a/ts/sass/scrollbar.scss b/ts/sass/scrollbar.scss index fe24c769a..714d19b03 100644 --- a/ts/sass/scrollbar.scss +++ b/ts/sass/scrollbar.scss @@ -5,28 +5,28 @@ @use 'buttons'; @mixin night-mode { - &::-webkit-scrollbar { - background: var(--window-bg); + &::-webkit-scrollbar { + background: var(--window-bg); - &:horizontal { - height: 12px; + &:horizontal { + height: 12px; + } + + &:vertical { + width: 12px; + } } - &:vertical { - width: 12px; - } - } + &::-webkit-scrollbar-thumb { + background: buttons.$fusion-button-hover-bg; + border-radius: 8px; - &::-webkit-scrollbar-thumb { - background: buttons.$fusion-button-hover-bg; - border-radius: 8px; + &:horizontal { + min-width: 50px; + } - &:horizontal { - min-width: 50px; + &:vertical { + min-height: 50px; + } } - - &:vertical { - min-height: 50px; - } - } } diff --git a/ts/vendor.bzl b/ts/vendor.bzl index c1cbb5bb6..c8a0e21a1 100644 --- a/ts/vendor.bzl +++ b/ts/vendor.bzl @@ -94,3 +94,38 @@ def copy_css_browser_selector(name = "css-browser-selector", visibility = ["//vi ], visibility = visibility, ) + +def copy_bootstrap_js(name = "bootstrap-js", visibility = ["//visibility:public"]): + vendor_js_lib( + name = name, + pkg = _pkg_from_name(name), + include = [ + "dist/js/bootstrap.bundle.min.js", + ], + strip_prefix = "dist/js/", + visibility = visibility, + ) + +def copy_bootstrap_css(name = "bootstrap-css", visibility = ["//visibility:public"]): + vendor_js_lib( + name = name, + pkg = _pkg_from_name(name), + include = [ + "dist/css/bootstrap.min.css", + ], + strip_prefix = "dist/css/", + visibility = visibility, + ) + +def copy_bootstrap_icons(name = "bootstrap-icons", visibility = ["//visibility:public"]): + vendor_js_lib( + name = name, + pkg = _pkg_from_name(name), + include = [ + "font/bootstrap-icons.css", + "font/fonts/bootstrap-icons.woff", + "font/fonts/bootstrap-icons.woff2", + ], + strip_prefix = "font/", + visibility = visibility, + ) diff --git a/ts/yarn.lock b/ts/yarn.lock index f73eb1287..0b8099158 100644 --- a/ts/yarn.lock +++ b/ts/yarn.lock @@ -699,6 +699,16 @@ bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +bootstrap-icons@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/bootstrap-icons/-/bootstrap-icons-1.4.0.tgz#ea08e2c8bc1535576ad267312cca9ee84ea73343" + integrity sha512-EynaOv/G/X/sQgPUqkdLJoxPrWk73wwsVjVR3cDNYO0jMS58poq7DOC2CraBWlBt1AberEmt0blfw4ony2/ZIg== + +bootstrap@^5.0.0-beta2: + version "5.0.0-beta2" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.0.0-beta2.tgz#ab1504a12807fa58e5e41408e35fcea42461e84b" + integrity sha512-e+uPbPHqTQWKyCX435uVlOmgH9tUt0xtjvyOC7knhKgOS643BrQKuTo+KecGpPV7qlmOyZgCfaM4xxPWtDEN/g== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"