Compare commits
8 Commits
feature/ni
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e35f209d | ||
|
|
55a043366a | ||
|
|
864a37f1e6 | ||
|
|
72c7c94f3d | ||
|
|
c3a9b006d2 | ||
|
|
b37397acb8 | ||
|
|
49c3b14bf0 | ||
|
|
d2cba48f8e |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
target/*
|
||||
.DS_Store
|
||||
**/node_modules/*
|
||||
.DS_Store
|
||||
|
||||
432
Cargo.lock
generated
432
Cargo.lock
generated
@@ -19,6 +19,18 @@ dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -28,6 +40,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
@@ -76,6 +94,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -124,6 +148,21 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.90"
|
||||
@@ -151,7 +190,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-targets",
|
||||
"windows-targets 0.52.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -186,6 +225,19 @@ dependencies = [
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
@@ -232,6 +284,31 @@ version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@@ -299,6 +376,22 @@ dependencies = [
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
@@ -331,6 +424,12 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
@@ -411,12 +510,31 @@ version = "0.2.153"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.2"
|
||||
@@ -432,6 +550,18 @@ dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@@ -453,6 +583,29 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets 0.52.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-zoneinfo"
|
||||
version = "0.3.0"
|
||||
@@ -473,6 +626,12 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.11.0"
|
||||
@@ -589,6 +748,35 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"lru",
|
||||
"paste",
|
||||
"stability",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.4"
|
||||
@@ -624,6 +812,12 @@ version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.17"
|
||||
@@ -636,6 +830,12 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
@@ -700,12 +900,86 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "stability"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
@@ -762,6 +1036,26 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "tiron"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"ironcalc",
|
||||
"ratatui",
|
||||
"tui-input",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui-input"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3e785f863a3af4c800a2a669d0b64c879b538738e352607e2624d03f868dc01"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
@@ -774,6 +1068,18 @@ version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.8.0"
|
||||
@@ -908,13 +1214,59 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.52.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -923,57 +1275,119 @@ version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_gnullvm 0.52.4",
|
||||
"windows_aarch64_msvc 0.52.4",
|
||||
"windows_i686_gnu 0.52.4",
|
||||
"windows_i686_msvc 0.52.4",
|
||||
"windows_x86_64_gnu 0.52.4",
|
||||
"windows_x86_64_gnullvm 0.52.4",
|
||||
"windows_x86_64_msvc 0.52.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
|
||||
@@ -4,6 +4,7 @@ resolver = "2"
|
||||
members = [
|
||||
"base",
|
||||
"xlsx",
|
||||
"tironcalc",
|
||||
"bindings/wasm",
|
||||
]
|
||||
|
||||
|
||||
@@ -69,26 +69,21 @@ impl Model {
|
||||
target_row: i32,
|
||||
target_column: i32,
|
||||
) -> Result<(), String> {
|
||||
if let Some(source_cell) = self
|
||||
let source_cell = self
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(source_row, source_column)
|
||||
{
|
||||
let style = source_cell.get_style();
|
||||
// FIXME: we need some user_input getter instead of get_text
|
||||
let formula_or_value = self
|
||||
.get_cell_formula(sheet, source_row, source_column)?
|
||||
.unwrap_or_else(|| {
|
||||
source_cell.get_text(&self.workbook.shared_strings, &self.language)
|
||||
});
|
||||
self.set_user_input(sheet, target_row, target_column, formula_or_value);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_cell_style(target_row, target_column, style);
|
||||
self.cell_clear_all(sheet, source_row, source_column)?;
|
||||
} else {
|
||||
self.cell_clear_all(sheet, target_row, target_column)?;
|
||||
}
|
||||
.ok_or("Expected Cell to exist")?;
|
||||
let style = source_cell.get_style();
|
||||
// FIXME: we need some user_input getter instead of get_text
|
||||
let formula_or_value = self
|
||||
.get_cell_formula(sheet, source_row, source_column)?
|
||||
.unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language));
|
||||
self.set_user_input(sheet, target_row, target_column, formula_or_value);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_cell_style(target_row, target_column, style);
|
||||
self.cell_clear_all(sheet, source_row, source_column)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -111,7 +106,7 @@ impl Model {
|
||||
return Err("Cannot add a negative number of cells :)".to_string());
|
||||
}
|
||||
// check if it is possible:
|
||||
let dimensions = self.workbook.worksheet(sheet)?.get_dimension();
|
||||
let dimensions = self.workbook.worksheet(sheet)?.dimension();
|
||||
let last_column = dimensions.max_column + column_count;
|
||||
if last_column > LAST_COLUMN {
|
||||
return Err(
|
||||
@@ -268,7 +263,7 @@ impl Model {
|
||||
return Err("Cannot add a negative number of cells :)".to_string());
|
||||
}
|
||||
// Check if it is possible:
|
||||
let dimensions = self.workbook.worksheet(sheet)?.get_dimension();
|
||||
let dimensions = self.workbook.worksheet(sheet)?.dimension();
|
||||
let last_row = dimensions.max_row + row_count;
|
||||
if last_row > LAST_ROW {
|
||||
return Err(
|
||||
@@ -372,162 +367,13 @@ impl Model {
|
||||
}
|
||||
}
|
||||
self.workbook.worksheets[sheet as usize].rows = new_rows;
|
||||
self.displace_cells(&DisplaceData::Row {
|
||||
sheet,
|
||||
row,
|
||||
delta: -row_count,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_cells_and_shift_left(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_column = worksheet.get_dimension().max_column;
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Move all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column + 1..max_column + 1 {
|
||||
println!("{r}-{c}");
|
||||
self.move_cell(sheet, r, c, r, c - column_delta)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta: -column_delta,
|
||||
row_delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert cells and shift right
|
||||
pub fn insert_cells_and_shift_right(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_column = worksheet.get_dimension().max_column;
|
||||
|
||||
// Move all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in (column..max_column + 1).rev() {
|
||||
self.move_cell(sheet, r, c, r, c + column_delta)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta,
|
||||
row_delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert cells and shift down
|
||||
pub fn insert_cells_and_shift_down(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_row = worksheet.get_dimension().max_row;
|
||||
|
||||
// Move all cells in the range
|
||||
for r in (row..row + max_row + 1).rev() {
|
||||
for c in column..column + column_delta {
|
||||
self.move_cell(sheet, r, c, r, c + column_delta)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta,
|
||||
row_delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_cells_and_shift_up(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_row = worksheet.get_dimension().max_row;
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Move all cells in the range
|
||||
for r in row..max_row + 1 {
|
||||
for c in column + 1..column + column_delta {
|
||||
self.move_cell(sheet, r, c, r - row_delta, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta,
|
||||
row_delta: -row_delta,
|
||||
});
|
||||
|
||||
self.displace_cells(
|
||||
&(DisplaceData::Row {
|
||||
sheet,
|
||||
row,
|
||||
delta: -row_count,
|
||||
}),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! A tokenizer for spreadsheet formulas.
|
||||
//!
|
||||
//! This is meant to feed a formula parser.
|
||||
@@ -9,10 +7,8 @@
|
||||
//! It supports two working modes:
|
||||
//!
|
||||
//! 1. A1 or display mode
|
||||
//!
|
||||
//! This is for user formulas. References are like `D4`, `D$4` or `F5:T10`
|
||||
//! 2. R1C1, internal or runtime mode
|
||||
//!
|
||||
//! A reference like R1C1 refers to $A$1 and R3C4 to $D$4
|
||||
//! R[2]C[5] refers to a cell two rows below and five columns to the right
|
||||
//! It uses the 'en' locale and language.
|
||||
@@ -59,8 +55,7 @@ use super::token::{Error, TokenType};
|
||||
use super::types::*;
|
||||
use super::utils;
|
||||
|
||||
/// Returns an iterator over tokens together with their position in the byte string.
|
||||
pub mod marked_token;
|
||||
pub mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
@@ -68,28 +63,17 @@ mod test;
|
||||
mod ranges;
|
||||
mod structured_references;
|
||||
|
||||
/// This is the TokenType we return if we cannot recognize a token
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LexerError {
|
||||
/// Position of the beginning of the token in the byte string.
|
||||
pub position: usize,
|
||||
/// Message describing what we think the error is.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub(super) type Result<T> = std::result::Result<T, LexerError>;
|
||||
|
||||
/// Whether we try to parse formulas in A1 mode or in the internal R1C1 mode
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum LexerMode {
|
||||
/// Cell references are written `=S34`. This is the display mode
|
||||
A1,
|
||||
/// R1C1, internal or runtime mode
|
||||
///
|
||||
/// A reference like R1C1 refers to $A$1 and R3C4 to $D$4
|
||||
/// R[2]C[5] refers to a cell two rows below and five columns to the right
|
||||
/// It uses the 'en' locale and language.
|
||||
/// This is used internally at runtime.
|
||||
R1C1,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod test_common;
|
||||
mod test_language;
|
||||
mod test_locale;
|
||||
mod test_marked_token;
|
||||
mod test_ranges;
|
||||
mod test_tables;
|
||||
mod test_util;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::expressions::{
|
||||
lexer::marked_token::{get_tokens, MarkedToken},
|
||||
lexer::util::get_tokens,
|
||||
token::{OpCompare, OpSum, TokenType},
|
||||
};
|
||||
|
||||
@@ -22,29 +22,6 @@ fn test_get_tokens() {
|
||||
assert_eq!(l.end, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chinese_characters() {
|
||||
let formula = "\"你好\" & \"世界!\"";
|
||||
let marked_tokens = get_tokens(formula);
|
||||
assert_eq!(marked_tokens.len(), 3);
|
||||
let first_t = MarkedToken {
|
||||
token: TokenType::String("你好".to_string()),
|
||||
start: 0,
|
||||
end: 4,
|
||||
};
|
||||
let second_t = MarkedToken {
|
||||
token: TokenType::And,
|
||||
start: 4,
|
||||
end: 6,
|
||||
};
|
||||
let third_t = MarkedToken {
|
||||
token: TokenType::String("世界!".to_string()),
|
||||
start: 6,
|
||||
end: 12,
|
||||
};
|
||||
assert_eq!(marked_tokens, vec![first_t, second_t, third_t]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_tokens() {
|
||||
assert_eq!(
|
||||
@@ -1,5 +1,3 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::expressions::token;
|
||||
@@ -11,11 +9,8 @@ use super::{Lexer, LexerMode};
|
||||
/// A MarkedToken is a token together with its position on a formula
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MarkedToken {
|
||||
/// Token type (see [token::TokenType])
|
||||
pub token: token::TokenType,
|
||||
/// Position of the start of the token (in bytes)
|
||||
pub start: i32,
|
||||
/// Position of the end of the token (in bytes)
|
||||
pub end: i32,
|
||||
}
|
||||
|
||||
@@ -24,7 +19,7 @@ pub struct MarkedToken {
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ironcalc_base::expressions::{
|
||||
/// lexer::marked_token::{get_tokens, MarkedToken},
|
||||
/// lexer::util::{get_tokens, MarkedToken},
|
||||
/// token::{OpSum, TokenType},
|
||||
/// };
|
||||
///
|
||||
@@ -1,3 +1,4 @@
|
||||
// public modules
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod token;
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
//! # GRAMMAR
|
||||
//!
|
||||
//! <pre class="rust">
|
||||
//! opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
|
||||
//! opFactor => '*' | '/'
|
||||
//! unaryOp => '-' | '+'
|
||||
//!
|
||||
//! expr => concat (opComp concat)*
|
||||
//! concat => term ('&' term)*
|
||||
//! term => factor (opFactor factor)*
|
||||
//! factor => prod (opProd prod)*
|
||||
//! prod => power ('^' power)*
|
||||
//! power => (unaryOp)* range '%'*
|
||||
//! range => primary (':' primary)?
|
||||
//! primary => '(' expr ')'
|
||||
//! => number
|
||||
//! => function '(' f_args ')'
|
||||
//! => name
|
||||
//! => string
|
||||
//! => '{' a_args '}'
|
||||
//! => bool
|
||||
//! => bool()
|
||||
//! => error
|
||||
//!
|
||||
//! f_args => e (',' e)*
|
||||
//! </pre>
|
||||
/*!
|
||||
# GRAMAR
|
||||
|
||||
<pre class="rust">
|
||||
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
|
||||
opFactor => '*' | '/'
|
||||
unaryOp => '-' | '+'
|
||||
|
||||
expr => concat (opComp concat)*
|
||||
concat => term ('&' term)*
|
||||
term => factor (opFactor factor)*
|
||||
factor => prod (opProd prod)*
|
||||
prod => power ('^' power)*
|
||||
power => (unaryOp)* range '%'*
|
||||
range => primary (':' primary)?
|
||||
primary => '(' expr ')'
|
||||
=> number
|
||||
=> function '(' f_args ')'
|
||||
=> name
|
||||
=> string
|
||||
=> '{' a_args '}'
|
||||
=> bool
|
||||
=> bool()
|
||||
=> error
|
||||
|
||||
f_args => e (',' e)*
|
||||
</pre>
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -42,15 +44,21 @@ use super::utils::number_to_column;
|
||||
|
||||
use token::OpCompare;
|
||||
|
||||
pub(crate) mod move_formula;
|
||||
pub(crate) mod walk;
|
||||
|
||||
/// Produces a string representation of a formula from the AST.
|
||||
pub mod move_formula;
|
||||
pub mod stringify;
|
||||
pub mod walk;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ranges;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_move_formula;
|
||||
#[cfg(test)]
|
||||
mod test_tables;
|
||||
|
||||
pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> {
|
||||
let mut lexer = lexer::Lexer::new(
|
||||
formula,
|
||||
|
||||
@@ -1,75 +1,43 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use super::{super::utils::quote_name, Node, Reference};
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::token::OpUnary;
|
||||
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
|
||||
|
||||
/// Displaced data
|
||||
pub enum DisplaceData {
|
||||
/// Displaces columns (inserting or deleting columns)
|
||||
Column {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Column from which the data is displaced
|
||||
column: i32,
|
||||
/// Number of columns displaced (might be negative, e.g. when deleting columns)
|
||||
delta: i32,
|
||||
},
|
||||
/// Displaces rows (Inserting or deleting rows)
|
||||
Row {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Row from which the data is displaced
|
||||
row: i32,
|
||||
/// Number of rows displaced (might be negative, e.g. when deleting rows)
|
||||
delta: i32,
|
||||
},
|
||||
/// Displaces cells horizontally
|
||||
ShiftCellsRight {
|
||||
/// Sheet in which the displace data applies
|
||||
CellHorizontal {
|
||||
sheet: u32,
|
||||
/// Row of the to left corner
|
||||
row: i32,
|
||||
/// Column of the top left corner
|
||||
column: i32,
|
||||
/// Number of rows to be displaced
|
||||
row_delta: i32,
|
||||
/// Number of columns to be displaced (might be negative)
|
||||
column_delta: i32,
|
||||
delta: i32,
|
||||
},
|
||||
/// Displaces cells vertically
|
||||
ShiftCellsDown {
|
||||
/// Sheet in which the displace data applies
|
||||
CellVertical {
|
||||
sheet: u32,
|
||||
/// Row of the to left corner
|
||||
row: i32,
|
||||
/// Column of the top left corner
|
||||
column: i32,
|
||||
/// Number of rows displaced (might be negative)
|
||||
row_delta: i32,
|
||||
/// Number of columns to be displaced
|
||||
column_delta: i32,
|
||||
delta: i32,
|
||||
},
|
||||
/// Displaces data due to a column move from column to column + delta
|
||||
ColumnMove {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Column that is moved
|
||||
column: i32,
|
||||
/// The position of the new column is column + delta (might be negative)
|
||||
delta: i32,
|
||||
},
|
||||
/// Doesn't do any cell displacement
|
||||
None,
|
||||
}
|
||||
|
||||
/// Stringifies the AST formula in its internal R1C1 format
|
||||
pub fn to_rc_format(node: &Node) -> String {
|
||||
stringify(node, None, &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
/// Stringifies the formula applying the _displace_data_.
|
||||
pub fn to_string_displaced(
|
||||
node: &Node,
|
||||
context: &CellReferenceRC,
|
||||
@@ -78,12 +46,10 @@ pub fn to_string_displaced(
|
||||
stringify(node, Some(context), displace_data, false)
|
||||
}
|
||||
|
||||
/// Stringifies a formula from the AST
|
||||
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
/// Stringifies the formula for Excel compatibility
|
||||
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, true)
|
||||
}
|
||||
@@ -150,49 +116,41 @@ pub(crate) fn stringify_reference(
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::ShiftCellsRight {
|
||||
DisplaceData::CellHorizontal {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
column: displace_column,
|
||||
column_delta,
|
||||
row_delta,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet
|
||||
&& displace_row >= &row
|
||||
&& *displace_row < row + *row_delta
|
||||
{
|
||||
if *column_delta < 0 {
|
||||
if sheet_index == *sheet && displace_row == &row {
|
||||
if *delta < 0 {
|
||||
if &column >= displace_column {
|
||||
if column < displace_column - *column_delta {
|
||||
if column < displace_column - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
column += *column_delta;
|
||||
column += *delta;
|
||||
}
|
||||
} else if &column >= displace_column {
|
||||
column += *column_delta;
|
||||
column += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::ShiftCellsDown {
|
||||
DisplaceData::CellVertical {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
column: displace_column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet
|
||||
&& displace_column >= &column
|
||||
&& *displace_column < column + *column_delta
|
||||
{
|
||||
if *row_delta < 0 {
|
||||
if sheet_index == *sheet && displace_column == &column {
|
||||
if *delta < 0 {
|
||||
if &row >= displace_row {
|
||||
if row < displace_row - *row_delta {
|
||||
if row < displace_row - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
row += *row_delta;
|
||||
row += *delta;
|
||||
}
|
||||
} else if &row >= displace_row {
|
||||
row += *row_delta;
|
||||
row += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
use crate::expressions::parser::stringify::{to_string_displaced, DisplaceData};
|
||||
use crate::expressions::parser::{
|
||||
stringify::{to_rc_format, to_string},
|
||||
Node, Parser,
|
||||
use crate::expressions::parser::stringify::DisplaceData;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use super::{
|
||||
super::parser::{
|
||||
stringify::{to_rc_format, to_string},
|
||||
Node,
|
||||
},
|
||||
stringify::to_string_displaced,
|
||||
};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
struct Formula<'a> {
|
||||
initial: &'a str,
|
||||
@@ -1,4 +0,0 @@
|
||||
mod test_genertal;
|
||||
mod test_move_formula;
|
||||
mod test_ranges;
|
||||
mod test_tables;
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::move_formula::{move_formula, MoveContext};
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::{Area, CellReferenceRC};
|
||||
use crate::expressions::types::Area;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
#[test]
|
||||
fn test_move_formula() {
|
||||
@@ -2,9 +2,9 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
|
||||
use crate::expressions::parser::stringify::{to_rc_format, to_string};
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
use super::super::parser::stringify::{to_rc_format, to_string};
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
struct Formula<'a> {
|
||||
formula_a1: &'a str,
|
||||
@@ -6,8 +6,8 @@ use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::utils::{number_to_column, parse_reference_a1};
|
||||
use crate::types::{Table, TableColumn, TableStyleInfo};
|
||||
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
fn create_test_table(
|
||||
table_name: &str,
|
||||
@@ -3,7 +3,7 @@ use std::fmt;
|
||||
use crate::{
|
||||
calc_result::CalcResult,
|
||||
expressions::{
|
||||
lexer::marked_token::get_tokens,
|
||||
lexer::util::get_tokens,
|
||||
parser::Node,
|
||||
token::{Error, OpSum, TokenType},
|
||||
types::CellReferenceIndex,
|
||||
|
||||
@@ -221,7 +221,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -229,7 +229,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -284,7 +284,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -292,7 +292,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -360,7 +360,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -368,7 +368,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -866,7 +866,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -874,7 +874,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
|
||||
@@ -132,7 +132,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -140,7 +140,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -199,7 +199,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -207,7 +207,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
|
||||
@@ -385,7 +385,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(first_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension();
|
||||
.dimension();
|
||||
let max_row = dimension.max_row;
|
||||
let max_column = dimension.max_column;
|
||||
|
||||
@@ -530,7 +530,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(sum_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if left_column == 1 && right_column == LAST_COLUMN {
|
||||
@@ -538,7 +538,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(sum_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
|
||||
|
||||
@@ -892,7 +892,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -900,7 +900,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
|
||||
@@ -255,7 +255,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -263,7 +263,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
let left = CellReferenceIndex {
|
||||
|
||||
@@ -118,6 +118,8 @@ pub struct Model {
|
||||
pub(crate) language: Language,
|
||||
/// The timezone used to evaluate the model
|
||||
pub(crate) tz: Tz,
|
||||
/// The view id. A view consist of a selected sheet and ranges.
|
||||
pub(crate) view_id: u32,
|
||||
}
|
||||
|
||||
// FIXME: Maybe this should be the same as CellReference
|
||||
@@ -681,6 +683,13 @@ impl Model {
|
||||
Err(format!("Invalid color: {}", color))
|
||||
}
|
||||
|
||||
/// Makes the grid lines in the sheet visible (`true`) or hidden (`false`)
|
||||
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
|
||||
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||
worksheet.show_grid_lines = show_grid_lines;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
|
||||
use Cell::*;
|
||||
match cell {
|
||||
@@ -886,6 +895,7 @@ impl Model {
|
||||
language,
|
||||
locale,
|
||||
tz,
|
||||
view_id: 0,
|
||||
};
|
||||
|
||||
model.parse_formulas();
|
||||
@@ -1788,7 +1798,7 @@ impl Model {
|
||||
/// Returns markup representation of the given `sheet`.
|
||||
pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> {
|
||||
let worksheet = self.workbook.worksheet(sheet)?;
|
||||
let dimension = worksheet.get_dimension();
|
||||
let dimension = worksheet.dimension();
|
||||
|
||||
let mut rows = Vec::new();
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ use crate::{
|
||||
language::get_language,
|
||||
locale::get_locale,
|
||||
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
|
||||
types::{Metadata, Selection, SheetState, Workbook, WorkbookSettings, Worksheet},
|
||||
types::{
|
||||
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
|
||||
},
|
||||
utils::ParsedReference,
|
||||
};
|
||||
|
||||
@@ -35,7 +37,20 @@ fn is_valid_sheet_name(name: &str) -> bool {
|
||||
|
||||
impl Model {
|
||||
/// Creates a new worksheet. Note that it does not check if the name or the sheet_id exists
|
||||
fn new_empty_worksheet(name: &str, sheet_id: u32) -> Worksheet {
|
||||
fn new_empty_worksheet(name: &str, sheet_id: u32, view_ids: &[&u32]) -> Worksheet {
|
||||
let mut views = HashMap::new();
|
||||
for id in view_ids {
|
||||
views.insert(
|
||||
**id,
|
||||
WorksheetView {
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
Worksheet {
|
||||
cols: vec![],
|
||||
rows: vec![],
|
||||
@@ -50,12 +65,8 @@ impl Model {
|
||||
color: Default::default(),
|
||||
frozen_columns: 0,
|
||||
frozen_rows: 0,
|
||||
selection: Selection {
|
||||
is_selected: false,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
},
|
||||
show_grid_lines: true,
|
||||
views,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +141,7 @@ impl Model {
|
||||
self.parsed_defined_names = parsed_defined_names;
|
||||
}
|
||||
|
||||
// Reparses all formulas and defined names
|
||||
/// Reparses all formulas and defined names
|
||||
pub(crate) fn reset_parsed_structures(&mut self) {
|
||||
self.parser
|
||||
.set_worksheets(self.workbook.get_worksheet_names());
|
||||
@@ -161,7 +172,8 @@ impl Model {
|
||||
let sheet_name = format!("{}{}", base_name, index);
|
||||
// Now we need a sheet_id
|
||||
let sheet_id = self.get_new_sheet_id();
|
||||
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id);
|
||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id, &view_ids);
|
||||
self.workbook.worksheets.push(worksheet);
|
||||
self.reset_parsed_structures();
|
||||
(sheet_name, self.workbook.worksheets.len() as u32 - 1)
|
||||
@@ -192,7 +204,8 @@ impl Model {
|
||||
Some(id) => id,
|
||||
None => self.get_new_sheet_id(),
|
||||
};
|
||||
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id);
|
||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id, &view_ids);
|
||||
if sheet_index as usize > self.workbook.worksheets.len() {
|
||||
return Err("Sheet index out of range".to_string());
|
||||
}
|
||||
@@ -339,11 +352,14 @@ impl Model {
|
||||
// "2020-08-06T21:20:53Z
|
||||
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
|
||||
let mut views = HashMap::new();
|
||||
views.insert(0, WorkbookView { sheet: 0 });
|
||||
|
||||
// String versions of the locale are added here to simplify the serialize/deserialize logic
|
||||
let workbook = Workbook {
|
||||
shared_strings: vec![],
|
||||
defined_names: vec![],
|
||||
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1)],
|
||||
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1, &[&0])],
|
||||
styles: Default::default(),
|
||||
name: name.to_string(),
|
||||
settings: WorkbookSettings {
|
||||
@@ -359,6 +375,7 @@ impl Model {
|
||||
last_modified: now,
|
||||
},
|
||||
tables: HashMap::new(),
|
||||
views,
|
||||
};
|
||||
let parsed_formulas = Vec::new();
|
||||
let worksheets = &workbook.worksheets;
|
||||
@@ -379,6 +396,7 @@ impl Model {
|
||||
locale,
|
||||
language,
|
||||
tz,
|
||||
view_id: 0,
|
||||
};
|
||||
model.parse_formulas();
|
||||
Ok(model)
|
||||
|
||||
@@ -76,10 +76,16 @@ fn fn_imconjugate() {
|
||||
fn fn_imcos() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMCOS("4+3i")"#);
|
||||
// In macos non intel this is "-6.58066304055116+7.58155274274655i"
|
||||
model._set("A2", r#"=COMPLEX(-6.58066304055116, 7.58155274274654)"#);
|
||||
model._set("A3", r#"=IMABS(IMSUB(A1, A2)) < G1"#);
|
||||
|
||||
// small number
|
||||
model._set("G1", "0.0000001");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "-6.58066304055116+7.58155274274654i");
|
||||
assert_eq!(model._get_text("A3"), "TRUE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
mod test_fn_average;
|
||||
mod test_fn_averageifs;
|
||||
mod test_fn_choose;
|
||||
mod test_fn_concatenate;
|
||||
mod test_fn_count;
|
||||
mod test_fn_exact;
|
||||
mod test_fn_financial;
|
||||
mod test_fn_if;
|
||||
mod test_fn_maxifs;
|
||||
mod test_fn_minifs;
|
||||
mod test_fn_offset;
|
||||
mod test_fn_product;
|
||||
mod test_fn_rept;
|
||||
mod test_fn_sum;
|
||||
mod test_fn_sumifs;
|
||||
mod test_fn_textbefore;
|
||||
mod test_fn_textjoin;
|
||||
mod test_fn_type;
|
||||
@@ -1,5 +1,3 @@
|
||||
mod engineering;
|
||||
mod functions;
|
||||
mod test_actions;
|
||||
mod test_binary_search;
|
||||
mod test_cell;
|
||||
@@ -10,30 +8,50 @@ mod test_criteria;
|
||||
mod test_currency;
|
||||
mod test_date_and_time;
|
||||
mod test_error_propagation;
|
||||
mod test_escape_quotes;
|
||||
mod test_extend;
|
||||
mod test_fn_average;
|
||||
mod test_fn_averageifs;
|
||||
mod test_fn_choose;
|
||||
mod test_fn_concatenate;
|
||||
mod test_fn_count;
|
||||
mod test_fn_exact;
|
||||
mod test_fn_financial;
|
||||
mod test_fn_if;
|
||||
mod test_fn_maxifs;
|
||||
mod test_fn_minifs;
|
||||
mod test_fn_product;
|
||||
mod test_fn_rept;
|
||||
mod test_fn_sum;
|
||||
mod test_fn_sumifs;
|
||||
mod test_fn_textbefore;
|
||||
mod test_fn_textjoin;
|
||||
mod test_forward_references;
|
||||
mod test_frozen_rows_and_columns;
|
||||
mod test_frozen_rows_columns;
|
||||
mod test_general;
|
||||
mod test_get_cell_content;
|
||||
mod test_math;
|
||||
mod test_metadata;
|
||||
mod test_model_cell_clear_all;
|
||||
mod test_model_is_empty_cell;
|
||||
mod test_move_formula;
|
||||
mod test_number_format;
|
||||
mod test_percentage;
|
||||
mod test_quote_prefix;
|
||||
mod test_set_user_input;
|
||||
mod test_sheet_markup;
|
||||
mod test_sheets;
|
||||
mod test_shift_cells;
|
||||
mod test_styles;
|
||||
mod test_today;
|
||||
mod test_trigonometric;
|
||||
mod test_types;
|
||||
mod test_workbook;
|
||||
mod test_worksheet;
|
||||
mod user_model;
|
||||
pub(crate) mod util;
|
||||
|
||||
mod engineering;
|
||||
mod test_fn_offset;
|
||||
mod test_number_format;
|
||||
|
||||
mod test_escape_quotes;
|
||||
mod test_extend;
|
||||
mod test_fn_type;
|
||||
mod test_frozen_rows_and_columns;
|
||||
mod test_get_cell_content;
|
||||
mod test_percentage;
|
||||
mod test_today;
|
||||
mod test_types;
|
||||
mod user_model;
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn shift_cells_right() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 3); // C5
|
||||
model.set_user_input(sheet, row, column, "Hi".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "world".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "!".to_string());
|
||||
|
||||
model
|
||||
.insert_cells_and_shift_right(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 5, 3), Ok("".to_string()));
|
||||
assert_eq!(model.get_cell_content(0, 5, 4), Ok("Hi".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_cells_right_with_formulas() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 3); // C5
|
||||
model.set_user_input(sheet, row, column - 1, "23".to_string());
|
||||
model.set_user_input(sheet, row, column, "42".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "=C5*2".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "=A5+2".to_string());
|
||||
model.set_user_input(sheet, 20, 3, "=C5*A2".to_string());
|
||||
model.evaluate();
|
||||
|
||||
model
|
||||
.insert_cells_and_shift_right(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column - 1),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
assert_eq!(model.get_cell_content(0, row, column), Ok("".to_string()));
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 1),
|
||||
Ok("42".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 2),
|
||||
Ok("=D5*2".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 3),
|
||||
Ok("=A5+2".to_string())
|
||||
);
|
||||
assert_eq!(model.get_cell_content(0, 20, 3), Ok("=D5*A2".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_cells_left() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 10); // J5
|
||||
model.set_user_input(sheet, row, column - 1, "23".to_string());
|
||||
model.set_user_input(sheet, row, column, "42".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "Hi".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "honey!".to_string());
|
||||
model.evaluate();
|
||||
|
||||
model
|
||||
.delete_cells_and_shift_left(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column - 1),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.get_cell_content(0, row, column), Ok("Hi".to_string()));
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 1),
|
||||
Ok("honey!".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_cells_left_with_formulas() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 10); // J5
|
||||
model.set_user_input(sheet, row, column - 1, "23".to_string());
|
||||
model.set_user_input(sheet, row, column, "42".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "33".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "=K5*A5".to_string());
|
||||
|
||||
model.set_user_input(sheet, row, column + 20, "=K5*A5".to_string());
|
||||
model.evaluate();
|
||||
|
||||
model
|
||||
.delete_cells_and_shift_left(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 1),
|
||||
Ok("=J5*A5".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 19),
|
||||
Ok("=J5*A5".to_string())
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
fn test_worksheet_dimension_empty_sheet() {
|
||||
let model = new_empty_model();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
@@ -25,7 +25,7 @@ fn test_worksheet_dimension_single_cell() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("W11", "1");
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 11,
|
||||
min_column: 23,
|
||||
@@ -41,7 +41,7 @@ fn test_worksheet_dimension_single_cell_set_empty() {
|
||||
model._set("W11", "1");
|
||||
model.cell_clear_contents(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 11,
|
||||
min_column: 23,
|
||||
@@ -57,7 +57,7 @@ fn test_worksheet_dimension_single_cell_deleted() {
|
||||
model._set("W11", "1");
|
||||
model.cell_clear_all(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
@@ -77,7 +77,7 @@ fn test_worksheet_dimension_multiple_cells() {
|
||||
model._set("B19", "1");
|
||||
model.cell_clear_all(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 11,
|
||||
min_column: 2,
|
||||
@@ -91,7 +91,7 @@ fn test_worksheet_dimension_multiple_cells() {
|
||||
fn test_worksheet_dimension_progressive() {
|
||||
let mut model = new_empty_model();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
@@ -102,7 +102,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 30, 50, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 30,
|
||||
min_column: 50,
|
||||
@@ -113,7 +113,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 10, 15, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 10,
|
||||
min_column: 15,
|
||||
@@ -124,7 +124,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 5, 25, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 5,
|
||||
min_column: 15,
|
||||
@@ -135,7 +135,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 10, 250, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 5,
|
||||
min_column: 15,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
mod test_add_delete_sheets;
|
||||
mod test_autofill_columns;
|
||||
mod test_autofill_rows;
|
||||
mod test_clear_cells;
|
||||
mod test_diff_queue;
|
||||
mod test_evaluation;
|
||||
mod test_general;
|
||||
mod test_grid_lines;
|
||||
mod test_rename_sheet;
|
||||
mod test_row_column;
|
||||
mod test_shift_cells;
|
||||
mod test_styles;
|
||||
mod test_to_from_bytes;
|
||||
mod test_undo_redo;
|
||||
mod test_view;
|
||||
|
||||
404
base/src/test/user_model/test_autofill_columns.rs
Normal file
404
base/src/test/user_model/test_autofill_columns.rs
Normal file
@@ -0,0 +1,404 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::Area;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// This is cell A3
|
||||
model.set_user_input(0, 3, 1, "alpha").unwrap();
|
||||
// We autofill from A3 to C3
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
// B3
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 3, 2),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
// C3
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 3, 3),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_cell_right() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
// B1
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 2),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_beta_gamma() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:B3
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap(); // A1
|
||||
model.set_user_input(0, 1, 2, "Bethe").unwrap(); // B1
|
||||
model.set_user_input(0, 1, 3, "Gamow").unwrap(); // C1
|
||||
model.set_user_input(0, 2, 1, "=A1").unwrap(); // A2
|
||||
model.set_user_input(0, 2, 2, "=B1").unwrap(); // B2
|
||||
model.set_user_input(0, 2, 3, "=C1").unwrap(); // C2
|
||||
|
||||
// We autofill from A1:C2 to I2
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 3,
|
||||
height: 2,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// D1
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 4),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 5),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 6),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 7),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 8),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 9),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 4),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 5),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 6),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 7),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 8),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 9),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 2, 4), Ok("=D1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:C1
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
||||
|
||||
let b1 = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 2,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let c1 = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 3,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model.update_range_style(&b1, "font.i", "true").unwrap();
|
||||
model
|
||||
.update_range_style(&c1, "fill.bg_color", "#334455")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 3,
|
||||
height: 1,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check that cell E1 has B1 style
|
||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 1, 4), Ok("".to_string()));
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
||||
assert!(!style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 4),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A12
|
||||
model.set_user_input(0, 1, 10, "Alpher").unwrap();
|
||||
model.set_user_input(0, 1, 11, "Bethe").unwrap();
|
||||
model.set_user_input(0, 1, 12, "Gamow").unwrap();
|
||||
|
||||
// We fill upwards to row 5
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 10,
|
||||
width: 3,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 9),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 8),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 7),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_4() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A13
|
||||
model.set_user_input(0, 1, 10, "Margaret Burbidge").unwrap();
|
||||
model.set_user_input(0, 1, 11, "Geoffrey Burbidge").unwrap();
|
||||
model.set_user_input(0, 1, 12, "Willy Fowler").unwrap();
|
||||
model.set_user_input(0, 1, 13, "Fred Hoyle").unwrap();
|
||||
|
||||
// We fill left to row 5
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 10,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 9),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 8),
|
||||
Ok("Willy Fowler".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 5),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
model.set_user_input(0, 1, 4, "Margaret Burbidge").unwrap();
|
||||
|
||||
// Invalid sheet
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 3,
|
||||
row: 1,
|
||||
column: 4,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid worksheet index: '3'".to_string())
|
||||
);
|
||||
|
||||
// invalid column
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: -1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '-1'".to_string())
|
||||
);
|
||||
|
||||
// invalid column
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: LAST_COLUMN - 1,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '16392'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: LAST_ROW + 1,
|
||||
column: 1,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '1048577'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: LAST_ROW - 2,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '1048583'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 5,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
-10,
|
||||
),
|
||||
Err("Invalid row: '-10'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_parameters() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 2,
|
||||
height: 1,
|
||||
},
|
||||
2,
|
||||
),
|
||||
Err("Invalid parameters for autofill".to_string())
|
||||
);
|
||||
}
|
||||
399
base/src/test/user_model/test_autofill_rows.rs
Normal file
399
base/src/test/user_model/test_autofill_rows.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::Area;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// This is cell A3
|
||||
model.set_user_input(0, 3, 1, "alpha").unwrap();
|
||||
// We autofill from A3 to A5
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 1),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_cell_down() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 1),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_beta_gamma() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:B3
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
||||
model.set_user_input(0, 1, 2, "=A1").unwrap();
|
||||
model.set_user_input(0, 2, 2, "=A2").unwrap();
|
||||
model.set_user_input(0, 3, 2, "=A3").unwrap();
|
||||
// We autofill from A1:B3 to A9
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 2,
|
||||
height: 3,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 6, 1),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 7, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 1),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 1),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 2),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 2),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 6, 2),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 7, 2),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 2),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 2),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 4, 2), Ok("=A4".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:B3
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
||||
|
||||
let a2 = Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let a3 = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model.update_range_style(&a2, "font.i", "true").unwrap();
|
||||
model
|
||||
.update_range_style(&a3, "fill.bg_color", "#334455")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 3,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 4, 1), Ok("".to_string()));
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
||||
assert!(!style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upwards() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A12
|
||||
model.set_user_input(0, 10, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 11, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 12, 1, "Gamow").unwrap();
|
||||
|
||||
// We fill upwards to row 5
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 10,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 3,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 1),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 1),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 7, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upwards_4() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A13
|
||||
model.set_user_input(0, 10, 1, "Margaret Burbidge").unwrap();
|
||||
model.set_user_input(0, 11, 1, "Geoffrey Burbidge").unwrap();
|
||||
model.set_user_input(0, 12, 1, "Willy Fowler").unwrap();
|
||||
model.set_user_input(0, 13, 1, "Fred Hoyle").unwrap();
|
||||
|
||||
// We fill upwards to row 5
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 10,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 4,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 1),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 1),
|
||||
Ok("Willy Fowler".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A13
|
||||
model.set_user_input(0, 4, 1, "Margaret Burbidge").unwrap();
|
||||
|
||||
// Invalid sheet
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 3,
|
||||
row: 4,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid worksheet index: '3'".to_string())
|
||||
);
|
||||
|
||||
// invalid row
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: -1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '-1'".to_string())
|
||||
);
|
||||
|
||||
// invalid row
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: LAST_ROW - 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '1048584'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: LAST_COLUMN + 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '16385'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: LAST_COLUMN - 2,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '16391'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
-10,
|
||||
),
|
||||
Err("Invalid row: '-10'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_parameters() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 2,
|
||||
},
|
||||
2,
|
||||
),
|
||||
Err("Invalid parameters for autofill".to_string())
|
||||
);
|
||||
}
|
||||
42
base/src/test/user_model/test_grid_lines.rs
Normal file
42
base/src/test/user_model/test_grid_lines.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.new_sheet();
|
||||
|
||||
// default sheet has show_grid_lines = true
|
||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
||||
|
||||
// default new sheet has show_grid_lines = true
|
||||
assert_eq!(model.get_show_grid_lines(1), Ok(true));
|
||||
|
||||
// wrong sheet number
|
||||
assert_eq!(
|
||||
model.get_show_grid_lines(2),
|
||||
Err("Invalid sheet index".to_string())
|
||||
);
|
||||
|
||||
// we can set it
|
||||
model.set_show_grid_lines(1, false).unwrap();
|
||||
assert_eq!(model.get_show_grid_lines(1), Ok(false));
|
||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(model.get_show_grid_lines(1), Ok(true));
|
||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
||||
|
||||
model.redo().unwrap();
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
let mut model2 = UserModel::from_model(new_empty_model());
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
assert_eq!(model2.get_show_grid_lines(1), Ok(false));
|
||||
assert_eq!(model2.get_show_grid_lines(0), Ok(true));
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn shift_cells_general() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
// some reference value in A1
|
||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||
|
||||
// We put some values in row 5
|
||||
model.set_user_input(0, 5, 3, "=1 + 1").unwrap(); // C5
|
||||
model.set_user_input(0, 5, 7, "=C5*A1").unwrap();
|
||||
|
||||
// Insert one cell in C5 and push right
|
||||
model.insert_cells_and_shift_right(0, 5, 3, 1, 1).unwrap();
|
||||
|
||||
// C5 should now be empty
|
||||
assert_eq!(model.get_cell_content(0, 5, 3), Ok("".to_string()));
|
||||
|
||||
// D5 should have 2
|
||||
assert_eq!(model.get_cell_content(0, 5, 4), Ok("=1+1".to_string()));
|
||||
}
|
||||
216
base/src/test/user_model/test_view.rs
Normal file
216
base/src/test/user_model/test_view.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
test::util::new_empty_model,
|
||||
user_model::SelectedView,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn initial_view() {
|
||||
let model = new_empty_model();
|
||||
let model = UserModel::from_model(model);
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_the_cell_sets_the_range() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_selected_cell(5, 4).unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 5, 4));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
column: 4,
|
||||
range: [5, 4, 5, 4],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_the_range_does_not_set_the_cell() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_selected_range(5, 4, 10, 6).unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [5, 4, 10, 6],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_new_sheet_and_back() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.new_sheet();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
model.set_selected_cell(5, 4).unwrap();
|
||||
model.set_selected_sheet(1).unwrap();
|
||||
assert_eq!(model.get_selected_cell(), (1, 1, 1));
|
||||
model.set_selected_sheet(0).unwrap();
|
||||
assert_eq!(model.get_selected_cell(), (0, 5, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_selected_cell_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(
|
||||
model.set_selected_cell(-5, 4),
|
||||
Err("Invalid row: '-5'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_cell(5, -4),
|
||||
Err("Invalid column: '-4'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(-1, 1, 1, 1),
|
||||
Err("Invalid row: '-1'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(1, 0, 1, 1),
|
||||
Err("Invalid column: '0'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(1, 1, LAST_ROW + 1, 1),
|
||||
Err("Invalid row: '1048577'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(1, 1, 1, LAST_COLUMN + 1),
|
||||
Err("Invalid column: '16385'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_selected_cell_errors_wrong_sheet() {
|
||||
let mut model = new_empty_model();
|
||||
// forcefully set a wrong index
|
||||
model.workbook.views.get_mut(&0).unwrap().sheet = 2;
|
||||
let mut model = UserModel::from_model(model);
|
||||
// It's returning the wrong number
|
||||
assert_eq!(model.get_selected_sheet(), 2);
|
||||
|
||||
// But we can't set the selected cell anymore
|
||||
assert_eq!(
|
||||
model.set_selected_cell(3, 4),
|
||||
Err("Invalid worksheet index 2".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(3, 4, 5, 6),
|
||||
Err("Invalid worksheet index 2".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.set_top_left_visible_cell(3, 4),
|
||||
Err("Invalid worksheet index 2".to_string())
|
||||
);
|
||||
|
||||
// we can fix it by setting the right cell
|
||||
model.set_selected_sheet(0).unwrap();
|
||||
model.set_selected_cell(3, 4).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_visible_cell() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_top_left_visible_cell(100, 12).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 100,
|
||||
left_column: 12
|
||||
}
|
||||
);
|
||||
|
||||
let s = serde_json::to_string(&model.get_selected_view()).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<SelectedView>(&s).unwrap(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 100,
|
||||
left_column: 12
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_visible_cell_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(
|
||||
model.set_top_left_visible_cell(-100, 12),
|
||||
Err("Invalid row: '-100'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_top_left_visible_cell(100, -12),
|
||||
Err("Invalid column: '-12'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_no_views() {
|
||||
let mut model = new_empty_model();
|
||||
// forcefully remove the view
|
||||
model.workbook.views = HashMap::new();
|
||||
// also in the sheet
|
||||
model.workbook.worksheets[0].views = HashMap::new();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// get methods will return defaults
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
|
||||
// set methods won't complain. but won't work either
|
||||
model.set_selected_sheet(0).unwrap();
|
||||
model.set_selected_cell(5, 6).unwrap();
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
}
|
||||
@@ -27,6 +27,14 @@ pub struct WorkbookSettings {
|
||||
pub tz: String,
|
||||
pub locale: String,
|
||||
}
|
||||
|
||||
/// A Workbook View tracks of the selected sheet for each view
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct WorkbookView {
|
||||
/// The index of the currently selected sheet.
|
||||
pub sheet: u32,
|
||||
}
|
||||
|
||||
/// An internal representation of an IronCalc Workbook
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Workbook {
|
||||
@@ -38,6 +46,7 @@ pub struct Workbook {
|
||||
pub settings: WorkbookSettings,
|
||||
pub metadata: Metadata,
|
||||
pub tables: HashMap<String, Table>,
|
||||
pub views: HashMap<u32, WorkbookView>,
|
||||
}
|
||||
|
||||
/// A defined name. The `sheet_id` is the sheet index in case the name is local
|
||||
@@ -48,9 +57,6 @@ pub struct DefinedName {
|
||||
pub sheet_id: Option<u32>,
|
||||
}
|
||||
|
||||
// TODO: Move to worksheet.rs make frozen_rows/columns private and u32
|
||||
/// Internal representation of a worksheet Excel object
|
||||
|
||||
/// * state:
|
||||
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
|
||||
/// hidden, veryHidden, visible
|
||||
@@ -71,12 +77,21 @@ impl Display for SheetState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of the worksheet as seen by the user. This includes
|
||||
/// details such as the currently selected cell, the visible range, and the
|
||||
/// position of the viewport.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Selection {
|
||||
pub is_selected: bool,
|
||||
pub struct WorksheetView {
|
||||
/// The row index of the currently selected cell.
|
||||
pub row: i32,
|
||||
/// The column index of the currently selected cell.
|
||||
pub column: i32,
|
||||
/// The selected range in the worksheet, specified as [start_row, start_column, end_row, end_column].
|
||||
pub range: [i32; 4],
|
||||
/// The row index of the topmost visible cell in the worksheet view.
|
||||
pub top_row: i32,
|
||||
/// The column index of the leftmost visible cell in the worksheet view.
|
||||
pub left_column: i32,
|
||||
}
|
||||
|
||||
/// Internal representation of a worksheet Excel object
|
||||
@@ -95,7 +110,9 @@ pub struct Worksheet {
|
||||
pub comments: Vec<Comment>,
|
||||
pub frozen_rows: i32,
|
||||
pub frozen_columns: i32,
|
||||
pub selection: Selection,
|
||||
pub views: HashMap<u32, WorksheetView>,
|
||||
/// Whether or not to show the grid lines in the worksheet
|
||||
pub show_grid_lines: bool,
|
||||
}
|
||||
|
||||
/// Internal representation of Excel's sheet_data
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use std::{collections::HashMap, fmt::Debug};
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
constants,
|
||||
@@ -18,6 +19,17 @@ use crate::{
|
||||
utils::is_valid_hex_color,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Debug))]
|
||||
pub struct SelectedView {
|
||||
pub sheet: u32,
|
||||
pub row: i32,
|
||||
pub column: i32,
|
||||
pub range: [i32; 4],
|
||||
pub top_row: i32,
|
||||
pub left_column: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
struct RowData {
|
||||
row: Option<Row>,
|
||||
@@ -91,36 +103,6 @@ enum Diff {
|
||||
column: i32,
|
||||
old_data: Box<ColumnData>,
|
||||
},
|
||||
InsertCellsShiftRight {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
},
|
||||
InsertCellsShiftDown {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
},
|
||||
DeleteCellsShiftLeft {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
old_data: Vec<Vec<Option<Cell>>>,
|
||||
},
|
||||
DeleteCellsShiftUp {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
old_data: Vec<Vec<Option<Cell>>>,
|
||||
},
|
||||
SetFrozenRowsCount {
|
||||
sheet: u32,
|
||||
new_value: i32,
|
||||
@@ -148,6 +130,11 @@ enum Diff {
|
||||
old_value: String,
|
||||
new_value: String,
|
||||
},
|
||||
SetShowGridLines {
|
||||
sheet: u32,
|
||||
old_value: bool,
|
||||
new_value: bool,
|
||||
}, // FIXME: we are missing SetViewDiffs
|
||||
}
|
||||
|
||||
type DiffList = Vec<Diff>;
|
||||
@@ -279,7 +266,7 @@ fn vertical(value: &str) -> Result<VerticalAlignment, String> {
|
||||
}
|
||||
|
||||
/// # A wrapper around [`Model`] for a spreadsheet end user.
|
||||
/// UserModel is a wrapper around Model with undo/redo history, _diffs_ and automatic evaluation.
|
||||
/// UserModel is a wrapper around Model with undo/redo history, _diffs_, automatic evaluation and view management.
|
||||
///
|
||||
/// A diff in this context (or more correctly a _user diff_) is a change created by a user.
|
||||
///
|
||||
@@ -305,7 +292,10 @@ fn vertical(value: &str) -> Result<VerticalAlignment, String> {
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct UserModel {
|
||||
model: Model,
|
||||
/// The underlying model
|
||||
/// See also:
|
||||
/// * [Model]
|
||||
pub model: Model,
|
||||
history: History,
|
||||
send_queue: Vec<QueueDiffs>,
|
||||
pause_evaluation: bool,
|
||||
@@ -658,7 +648,9 @@ impl UserModel {
|
||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), String> {
|
||||
let diff_list = vec![Diff::InsertRow { sheet, row }];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model.insert_rows(sheet, row, 1)
|
||||
self.model.insert_rows(sheet, row, 1)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a row
|
||||
@@ -685,7 +677,9 @@ impl UserModel {
|
||||
old_data,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model.delete_rows(sheet, row, 1)
|
||||
self.model.delete_rows(sheet, row, 1)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts a column
|
||||
@@ -695,7 +689,9 @@ impl UserModel {
|
||||
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), String> {
|
||||
let diff_list = vec![Diff::InsertColumn { sheet, column }];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model.insert_columns(sheet, column, 1)
|
||||
self.model.insert_columns(sheet, column, 1)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a column
|
||||
@@ -740,122 +736,7 @@ impl UserModel {
|
||||
}),
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model.delete_columns(sheet, column, 1)
|
||||
}
|
||||
|
||||
/// Insert cells in the area pushing the existing ones to the right
|
||||
///
|
||||
/// See also:
|
||||
/// * [Model::insert_cells_and_shift_right]
|
||||
pub fn insert_cells_and_shift_right(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let diff_list = vec![Diff::InsertCellsShiftRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.insert_cells_and_shift_right(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert cells in the area pushing the existing ones down
|
||||
pub fn insert_cells_and_shift_down(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let diff_list = vec![Diff::InsertCellsShiftDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.insert_cells_and_shift_down(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete cells in the specified area and then shift cells left to fill the gap.
|
||||
pub fn delete_cells_and_shift_left(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let mut old_data = Vec::new();
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
for r in row..row + row_delta {
|
||||
let mut row_data = Vec::new();
|
||||
for c in column..column + column_delta {
|
||||
let cell = worksheet.cell(r, c);
|
||||
row_data.push(cell.cloned());
|
||||
}
|
||||
old_data.push(row_data);
|
||||
}
|
||||
let diff_list = vec![Diff::DeleteCellsShiftLeft {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.delete_cells_and_shift_left(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete cells in the specified area and then shift cells upward to fill the gap.
|
||||
pub fn delete_cells_and_shift_up(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let mut old_data = Vec::new();
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
for r in row..row + row_delta {
|
||||
let mut row_data = Vec::new();
|
||||
for c in column..column + column_delta {
|
||||
let cell = worksheet.cell(r, c);
|
||||
row_data.push(cell.cloned());
|
||||
}
|
||||
old_data.push(row_data);
|
||||
}
|
||||
let diff_list = vec![Diff::DeleteCellsShiftUp {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.delete_cells_and_shift_up(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.delete_columns(sheet, column, 1)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
@@ -1057,7 +938,7 @@ impl UserModel {
|
||||
column,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(style),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
@@ -1073,6 +954,208 @@ impl UserModel {
|
||||
Ok(self.model.get_style_for_cell(sheet, row, column))
|
||||
}
|
||||
|
||||
/// Fills the cells from `source_area` until `to_row`.
|
||||
/// This simulates the user clicking on the cell outline handle and dragging it downwards (or upwards)
|
||||
pub fn auto_fill_rows(&mut self, source_area: &Area, to_row: i32) -> Result<(), String> {
|
||||
let mut diff_list = Vec::new();
|
||||
let sheet = source_area.sheet;
|
||||
let row1 = source_area.row;
|
||||
let column1 = source_area.column;
|
||||
let width1 = source_area.width;
|
||||
let height1 = source_area.height;
|
||||
|
||||
// Check first all parameters are valid
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index: '{sheet}'"));
|
||||
}
|
||||
|
||||
if !is_valid_column_number(column1) {
|
||||
return Err(format!("Invalid column: '{column1}'"));
|
||||
}
|
||||
if !is_valid_row(row1) {
|
||||
return Err(format!("Invalid row: '{row1}'"));
|
||||
}
|
||||
if !is_valid_column_number(column1 + width1 - 1) {
|
||||
return Err(format!("Invalid column: '{}'", column1 + width1 - 1));
|
||||
}
|
||||
if !is_valid_row(row1 + height1 - 1) {
|
||||
return Err(format!("Invalid row: '{}'", row1 + height1 - 1));
|
||||
}
|
||||
|
||||
if !is_valid_row(to_row) {
|
||||
return Err(format!("Invalid row: '{to_row}'"));
|
||||
}
|
||||
|
||||
// anchor_row is the first row that repeats in each case.
|
||||
let anchor_row;
|
||||
let sign;
|
||||
// this is the range of rows we are going to fill
|
||||
let row_range: Vec<i32>;
|
||||
|
||||
if to_row >= row1 + height1 {
|
||||
// we go downwards, we start from `row1 + height1` to `to_row`,
|
||||
anchor_row = row1;
|
||||
sign = 1;
|
||||
row_range = (row1 + height1..to_row + 1).collect();
|
||||
} else if to_row < row1 {
|
||||
// we go upwards, starting from `row1 - `` all the way to `to_row`
|
||||
anchor_row = row1 + height1 - 1;
|
||||
sign = -1;
|
||||
row_range = (to_row..row1).rev().collect();
|
||||
} else {
|
||||
return Err("Invalid parameters for autofill".to_string());
|
||||
}
|
||||
|
||||
for column in column1..column1 + width1 {
|
||||
let mut index = 0;
|
||||
for row_ref in &row_range {
|
||||
// Save value and style first
|
||||
let row = *row_ref;
|
||||
let old_value = self
|
||||
.model
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(row, column)
|
||||
.cloned();
|
||||
let old_style = self.model.get_style_for_cell(sheet, row, column);
|
||||
|
||||
// compute the new value and set it
|
||||
let source_row = anchor_row + index;
|
||||
let target_value = self
|
||||
.model
|
||||
.extend_to(sheet, source_row, column, row, column)?;
|
||||
self.model
|
||||
.set_user_input(sheet, row, column, target_value.to_string());
|
||||
|
||||
// Compute the new style and set it
|
||||
let new_style = self.model.get_style_for_cell(sheet, source_row, column);
|
||||
self.model.set_cell_style(sheet, row, column, &new_style)?;
|
||||
|
||||
// Add the diffs
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(old_style),
|
||||
new_value: Box::new(new_style),
|
||||
});
|
||||
diff_list.push(Diff::SetCellValue {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
new_value: target_value.to_string(),
|
||||
old_value: Box::new(old_value),
|
||||
});
|
||||
|
||||
index = (index + sign) % height1;
|
||||
}
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
self.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fills the cells from `source_area` until `to_column`.
|
||||
/// This simulates the user clicking on the cell outline handle and dragging it to the right (or to the left)
|
||||
pub fn auto_fill_columns(&mut self, source_area: &Area, to_column: i32) -> Result<(), String> {
|
||||
let mut diff_list = Vec::new();
|
||||
let sheet = source_area.sheet;
|
||||
let row1 = source_area.row;
|
||||
let column1 = source_area.column;
|
||||
let width1 = source_area.width;
|
||||
let height1 = source_area.height;
|
||||
|
||||
// Check first all parameters are valid
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index: '{sheet}'"));
|
||||
}
|
||||
|
||||
if !is_valid_column_number(column1) {
|
||||
return Err(format!("Invalid column: '{column1}'"));
|
||||
}
|
||||
if !is_valid_row(row1) {
|
||||
return Err(format!("Invalid row: '{row1}'"));
|
||||
}
|
||||
if !is_valid_column_number(column1 + width1 - 1) {
|
||||
return Err(format!("Invalid column: '{}'", column1 + width1 - 1));
|
||||
}
|
||||
if !is_valid_row(row1 + height1 - 1) {
|
||||
return Err(format!("Invalid row: '{}'", row1 + height1 - 1));
|
||||
}
|
||||
|
||||
if !is_valid_row(to_column) {
|
||||
return Err(format!("Invalid row: '{to_column}'"));
|
||||
}
|
||||
|
||||
// anchor_column is the first column that repeats in each case.
|
||||
let anchor_column;
|
||||
let sign;
|
||||
// this is the range of columns we are going to fill
|
||||
let column_range: Vec<i32>;
|
||||
|
||||
if to_column >= column1 + width1 {
|
||||
// we go right, we start from `1 + width` to `to_column`,
|
||||
anchor_column = column1;
|
||||
sign = 1;
|
||||
column_range = (column1 + width1..to_column + 1).collect();
|
||||
} else if to_column < column1 {
|
||||
// we go left, starting from `column1 - `` all the way to `to_column`
|
||||
anchor_column = column1 + width1 - 1;
|
||||
sign = -1;
|
||||
column_range = (to_column..column1).rev().collect();
|
||||
} else {
|
||||
return Err("Invalid parameters for autofill".to_string());
|
||||
}
|
||||
|
||||
for row in row1..row1 + height1 {
|
||||
let mut index = 0;
|
||||
for column_ref in &column_range {
|
||||
let column = *column_ref;
|
||||
// Save value and style first
|
||||
let old_value = self
|
||||
.model
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(row, column)
|
||||
.cloned();
|
||||
let old_style = self.model.get_style_for_cell(sheet, row, column);
|
||||
|
||||
// compute the new value and set it
|
||||
let source_column = anchor_column + index;
|
||||
let target_value = self
|
||||
.model
|
||||
.extend_to(sheet, row, source_column, row, column)?;
|
||||
self.model
|
||||
.set_user_input(sheet, row, column, target_value.to_string());
|
||||
|
||||
// Compute the new style and set it
|
||||
let new_style = self.model.get_style_for_cell(sheet, row, source_column);
|
||||
self.model.set_cell_style(sheet, row, column, &new_style)?;
|
||||
|
||||
// Add the diffs
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(old_style),
|
||||
new_value: Box::new(new_style),
|
||||
});
|
||||
diff_list.push(Diff::SetCellValue {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
new_value: target_value.to_string(),
|
||||
old_value: Box::new(old_value),
|
||||
});
|
||||
|
||||
index = (index + sign) % width1;
|
||||
}
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
self.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns information about the sheets
|
||||
///
|
||||
/// See also:
|
||||
@@ -1082,6 +1165,184 @@ impl UserModel {
|
||||
self.model.get_worksheets_properties()
|
||||
}
|
||||
|
||||
/// Returns the selected sheet index
|
||||
pub fn get_selected_sheet(&self) -> u32 {
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the selected cell
|
||||
pub fn get_selected_cell(&self) -> (u32, i32, i32) {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
return (sheet, view.row, view.column);
|
||||
}
|
||||
}
|
||||
// return a safe default
|
||||
(0, 1, 1)
|
||||
}
|
||||
|
||||
/// Returns selected view
|
||||
pub fn get_selected_view(&self) -> SelectedView {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
return SelectedView {
|
||||
sheet,
|
||||
row: view.row,
|
||||
column: view.column,
|
||||
range: view.range,
|
||||
top_row: view.top_row,
|
||||
left_column: view.left_column,
|
||||
};
|
||||
}
|
||||
}
|
||||
// return a safe default
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the the selected sheet
|
||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&0) {
|
||||
view.sheet = sheet;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the selected cell
|
||||
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if !is_valid_column_number(column) {
|
||||
return Err(format!("Invalid column: '{column}'"));
|
||||
}
|
||||
if !is_valid_row(row) {
|
||||
return Err(format!("Invalid row: '{row}'"));
|
||||
}
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||
view.row = row;
|
||||
view.column = column;
|
||||
view.range = [row, column, row, column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the selected range
|
||||
pub fn set_selected_range(
|
||||
&mut self,
|
||||
start_row: i32,
|
||||
start_column: i32,
|
||||
end_row: i32,
|
||||
end_column: i32,
|
||||
) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if !is_valid_column_number(start_column) {
|
||||
return Err(format!("Invalid column: '{start_column}'"));
|
||||
}
|
||||
if !is_valid_column_number(start_row) {
|
||||
return Err(format!("Invalid row: '{start_row}'"));
|
||||
}
|
||||
|
||||
if !is_valid_column_number(end_column) {
|
||||
return Err(format!("Invalid column: '{end_column}'"));
|
||||
}
|
||||
if !is_valid_column_number(end_row) {
|
||||
return Err(format!("Invalid row: '{end_row}'"));
|
||||
}
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||
view.range = [start_row, start_column, end_row, end_column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the value of the first visible cell
|
||||
pub fn set_top_left_visible_cell(
|
||||
&mut self,
|
||||
top_row: i32,
|
||||
left_column: i32,
|
||||
) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if !is_valid_column_number(left_column) {
|
||||
return Err(format!("Invalid column: '{left_column}'"));
|
||||
}
|
||||
if !is_valid_column_number(top_row) {
|
||||
return Err(format!("Invalid row: '{top_row}'"));
|
||||
}
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||
view.top_row = top_row;
|
||||
view.left_column = left_column;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the gid lines in the worksheet to visible (`true`) or hidden (`false`)
|
||||
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
|
||||
let old_value = self.model.workbook.worksheet(sheet)?.show_grid_lines;
|
||||
self.model.set_show_grid_lines(sheet, show_grid_lines)?;
|
||||
|
||||
self.push_diff_list(vec![Diff::SetShowGridLines {
|
||||
sheet,
|
||||
new_value: show_grid_lines,
|
||||
old_value,
|
||||
}]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true in the grid lines for
|
||||
pub fn get_show_grid_lines(&self, sheet: u32) -> Result<bool, String> {
|
||||
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
|
||||
}
|
||||
|
||||
// **** Private methods ****** //
|
||||
|
||||
fn push_diff_list(&mut self, diff_list: DiffList) {
|
||||
@@ -1245,93 +1506,12 @@ impl UserModel {
|
||||
} => {
|
||||
self.model.set_sheet_color(*index, old_value)?;
|
||||
}
|
||||
Diff::InsertCellsShiftRight {
|
||||
Diff::SetShowGridLines {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_value,
|
||||
new_value: _,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
self.model.delete_cells_and_shift_left(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
Diff::InsertCellsShiftDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
self.model.delete_cells_and_shift_up(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
Diff::DeleteCellsShiftLeft {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
// Sets old data
|
||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||
for r in *row..*row + *row_delta {
|
||||
for c in *column..*column + *column_delta {
|
||||
if let Some(cell) = &old_data[r as usize][c as usize] {
|
||||
worksheet.update_cell(r, c, cell.clone());
|
||||
} else {
|
||||
worksheet.cell_clear_contents(r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.model.insert_cells_and_shift_right(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
Diff::DeleteCellsShiftUp {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
// Sets old data
|
||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||
for r in *row..*row + *row_delta {
|
||||
for c in *column..*column + *column_delta {
|
||||
if let Some(cell) = &old_data[r as usize][c as usize] {
|
||||
worksheet.update_cell(r, c, cell.clone());
|
||||
} else {
|
||||
worksheet.cell_clear_contents(r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.model.insert_cells_and_shift_down(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
self.model.set_show_grid_lines(*sheet, *old_value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1453,71 +1633,12 @@ impl UserModel {
|
||||
} => {
|
||||
self.model.set_sheet_color(*index, new_value)?;
|
||||
}
|
||||
Diff::InsertCellsShiftRight {
|
||||
Diff::SetShowGridLines {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_value: _,
|
||||
new_value,
|
||||
} => {
|
||||
self.model.insert_cells_and_shift_right(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::InsertCellsShiftDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
self.model.insert_cells_and_shift_down(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteCellsShiftLeft {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data: _,
|
||||
} => {
|
||||
self.model.delete_cells_and_shift_left(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteCellsShiftUp {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data: _,
|
||||
} => {
|
||||
self.model.delete_cells_and_shift_up(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
self.model.set_show_grid_lines(*sheet, *new_value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ impl Worksheet {
|
||||
}
|
||||
|
||||
/// Calculates dimension of the sheet. This function isn't cheap to calculate.
|
||||
pub fn get_dimension(&self) -> WorksheetDimension {
|
||||
pub fn dimension(&self) -> WorksheetDimension {
|
||||
// FIXME: It's probably better to just track the size as operations happen.
|
||||
if self.sheet_data.is_empty() {
|
||||
return WorksheetDimension {
|
||||
|
||||
@@ -63,15 +63,65 @@ style_types = r"""
|
||||
getCellStyle(sheet: number, row: number, column: number): CellStyle;
|
||||
""".strip()
|
||||
|
||||
view = r"""
|
||||
* @returns {any}
|
||||
*/
|
||||
getSelectedView(): any;
|
||||
""".strip()
|
||||
|
||||
view_types = r"""
|
||||
* @returns {CellStyle}
|
||||
*/
|
||||
getSelectedView(): SelectedView;
|
||||
""".strip()
|
||||
|
||||
autofill_rows = r"""
|
||||
/**
|
||||
* @param {any} source_area
|
||||
* @param {number} to_row
|
||||
*/
|
||||
autoFillRows(source_area: any, to_row: number): void;
|
||||
"""
|
||||
|
||||
autofill_rows_types = r"""
|
||||
/**
|
||||
* @param {Area} source_area
|
||||
* @param {number} to_row
|
||||
*/
|
||||
autoFillRows(source_area: Area, to_row: number): void;
|
||||
"""
|
||||
|
||||
autofill_columns = r"""
|
||||
/**
|
||||
* @param {any} source_area
|
||||
* @param {number} to_column
|
||||
*/
|
||||
autoFillColumns(source_area: any, to_column: number): void;
|
||||
"""
|
||||
|
||||
autofill_columns_types = r"""
|
||||
/**
|
||||
* @param {Area} source_area
|
||||
* @param {number} to_column
|
||||
*/
|
||||
autoFillColumns(source_area: Area, to_column: number): void;
|
||||
"""
|
||||
|
||||
def fix_types(text):
|
||||
text = text.replace(get_tokens_str, get_tokens_str_types)
|
||||
text = text.replace(update_style_str, update_style_str_types)
|
||||
text = text.replace(properties, properties_types)
|
||||
text = text.replace(style, style_types)
|
||||
text = text.replace(view, view_types)
|
||||
text = text.replace(autofill_rows, autofill_rows_types)
|
||||
text = text.replace(autofill_columns, autofill_columns_types)
|
||||
with open("types.ts") as f:
|
||||
types_str = f.read()
|
||||
header_types = "{}\n\n{}".format(header, types_str)
|
||||
text = text.replace(header, header_types)
|
||||
if text.find("any") != -1:
|
||||
print("There are 'unfixed' types. Please check.")
|
||||
exit(1)
|
||||
return text
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use wasm_bindgen::{
|
||||
};
|
||||
|
||||
use ironcalc_base::{
|
||||
expressions::{lexer::marked_token::get_tokens as tokenizer, types::Area},
|
||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area},
|
||||
types::CellType,
|
||||
UserModel as BaseModel,
|
||||
};
|
||||
@@ -286,4 +286,94 @@ impl Model {
|
||||
pub fn get_worksheets_properties(&self) -> JsValue {
|
||||
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getSelectedSheet")]
|
||||
pub fn get_selected_sheet(&self) -> u32 {
|
||||
self.model.get_selected_sheet()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getSelectedCell")]
|
||||
pub fn get_selected_cell(&self) -> Vec<i32> {
|
||||
let (sheet, row, column) = self.model.get_selected_cell();
|
||||
vec![sheet as i32, row, column]
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getSelectedView")]
|
||||
pub fn get_selected_view(&self) -> JsValue {
|
||||
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setSelectedSheet")]
|
||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
|
||||
self.model.set_selected_sheet(sheet).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setSelectedCell")]
|
||||
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_selected_cell(row, column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setSelectedRange")]
|
||||
pub fn set_selected_range(
|
||||
&mut self,
|
||||
start_row: i32,
|
||||
start_column: i32,
|
||||
end_row: i32,
|
||||
end_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_selected_range(start_row, start_column, end_row, end_column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setTopLeftVisibleCell")]
|
||||
pub fn set_top_left_visible_cell(
|
||||
&mut self,
|
||||
top_row: i32,
|
||||
top_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_top_left_visible_cell(top_row, top_column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setShowGridLines")]
|
||||
pub fn set_show_grid_lines(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
show_grid_lines: bool,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_show_grid_lines(sheet, show_grid_lines)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getShowGridLines")]
|
||||
pub fn get_show_grid_lines(&mut self, sheet: u32) -> Result<bool, JsError> {
|
||||
self.model.get_show_grid_lines(sheet).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "autoFillRows")]
|
||||
pub fn auto_fill_rows(&mut self, source_area: JsValue, to_row: i32) -> Result<(), JsError> {
|
||||
let area: Area =
|
||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
self.model
|
||||
.auto_fill_rows(&area, to_row)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "autoFillColumns")]
|
||||
pub fn auto_fill_columns(
|
||||
&mut self,
|
||||
source_area: JsValue,
|
||||
to_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
let area: Area =
|
||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
self.model
|
||||
.auto_fill_columns(&area, to_column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,5 +119,14 @@ test("floating column numbers get truncated", () => {
|
||||
assert.strictEqual(model.getRowHeight(0, 5), 100.5);
|
||||
});
|
||||
|
||||
test("autofill", () => {
|
||||
const model = new Model('en', 'UTC');
|
||||
model.setUserInput(0, 1, 1, "23");
|
||||
model.autoFillRows({sheet: 0, row: 1, column: 1, width: 1, height: 1}, 2);
|
||||
|
||||
const result = model.getFormattedCellValue(0, 2, 1);
|
||||
assert.strictEqual(result, "23");
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
12
tironcalc/Cargo.toml
Normal file
12
tironcalc/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "tiron"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.27.0"
|
||||
ironcalc = { path = "../xlsx"}
|
||||
ratatui = "0.26.2"
|
||||
tui-input = "0.8.0"
|
||||
52
tironcalc/README.md
Normal file
52
tironcalc/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# TironCalc
|
||||
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/1206947691058171904.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/zZYWfh3RHJ
|
||||
|
||||
TironCalc, or Tiron for friends, is a TUI (Terminal User Interface) for IronCalc. Based on [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
|
||||

|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
You will find the binary at `./target/release/tiron`.
|
||||
|
||||
## Documentation
|
||||
|
||||
Start empty project:
|
||||
|
||||
```
|
||||
$ tiron
|
||||
```
|
||||
|
||||
Load an existing Excel file:
|
||||
|
||||
```
|
||||
$ tiron example.xlsx
|
||||
```
|
||||
|
||||
- `e` to edit a cell and enter the value or formula.
|
||||
- `q` to quit and save
|
||||
- `+` to add a sheet
|
||||
- `s` to go to the next sheet
|
||||
- `PgUp/PgDown` to navigate rows faster
|
||||
- `u` undo changes
|
||||
- `U` redo changes
|
||||
- `r` insert row
|
||||
- `c` insert column
|
||||
- `C` delete column
|
||||
- `R` delete row
|
||||
- `
|
||||
|
||||
|
||||
## Inspiration
|
||||
|
||||
James Gosling of Java fame created [sc](https://en.wikipedia.org/wiki/Sc_(spreadsheet_calculator)) the spreadsheet calculator.
|
||||
|
||||
Andrés Martinelli has been maintaining [sc-im](https://github.com/andmarti1424/sc-im), the spreadsheet calculator improvised.
|
||||
BIN
tironcalc/screenshot.png
Normal file
BIN
tironcalc/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
556
tironcalc/src/main.rs
Normal file
556
tironcalc/src/main.rs
Normal file
@@ -0,0 +1,556 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ironcalc::{
|
||||
base::{expressions::utils::number_to_column, Model, UserModel},
|
||||
export::save_to_xlsx,
|
||||
import::{load_from_icalc, load_from_xlsx},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Cell, Clear, Paragraph, Row, Table},
|
||||
Terminal,
|
||||
};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{io, sync::mpsc};
|
||||
use tui_input::{backend::crossterm::EventHandler, Input};
|
||||
|
||||
use std::env;
|
||||
|
||||
enum Event<I> {
|
||||
Input(I),
|
||||
Tick,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CursorMode {
|
||||
Navigate,
|
||||
Input,
|
||||
Popup,
|
||||
}
|
||||
|
||||
struct SelectedRange {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
min_row: i32,
|
||||
min_column: i32,
|
||||
max_row: i32,
|
||||
max_column: i32,
|
||||
}
|
||||
|
||||
struct SheetState {
|
||||
row: i32,
|
||||
column: i32,
|
||||
min_row: i32,
|
||||
min_column: i32,
|
||||
max_row: i32,
|
||||
max_column: i32,
|
||||
}
|
||||
|
||||
struct ModelState {
|
||||
selected_sheet: u32,
|
||||
sheet_states: Vec<SheetState>,
|
||||
}
|
||||
|
||||
impl ModelState {
|
||||
pub fn new(sheet_count: usize) -> ModelState {
|
||||
let mut sheet_states = vec![];
|
||||
for _ in 0..sheet_count {
|
||||
sheet_states.push(SheetState {
|
||||
row: 1,
|
||||
column: 1,
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
max_row: 1,
|
||||
max_column: 1,
|
||||
});
|
||||
}
|
||||
ModelState {
|
||||
selected_sheet: 0,
|
||||
sheet_states,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_range(&self) -> SelectedRange {
|
||||
let sheet = self.selected_sheet;
|
||||
let sheet_state = self.sheet_states.get(sheet as usize).unwrap();
|
||||
|
||||
SelectedRange {
|
||||
sheet,
|
||||
row: sheet_state.row,
|
||||
column: sheet_state.column,
|
||||
min_column: sheet_state.min_column,
|
||||
min_row: sheet_state.min_row,
|
||||
max_column: sheet_state.max_column,
|
||||
max_row: sheet_state.max_row,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_selected_sheet(&mut self, selected_sheet: u32) {
|
||||
self.selected_sheet = selected_sheet;
|
||||
}
|
||||
|
||||
pub fn get_selected_sheet(&self) -> u32 {
|
||||
self.selected_sheet
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
let sheet = self.selected_sheet;
|
||||
let mut sheet_state = &mut self.sheet_states.get(sheet as usize).unwrap();
|
||||
sheet_state.column -= 1;
|
||||
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_right(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_shift_up(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_shift_down(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_shift_left(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_shift_right(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
enable_raw_mode()?;
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let mut file_name = "model.xlsx";
|
||||
let model = if args.len() > 1 {
|
||||
file_name = &args[1];
|
||||
if file_name.ends_with(".ic") {
|
||||
load_from_icalc(file_name).unwrap()
|
||||
} else {
|
||||
load_from_xlsx(file_name, "en", "UTC").unwrap()
|
||||
}
|
||||
} else {
|
||||
Model::new_empty(file_name, "en", "UTC").unwrap()
|
||||
};
|
||||
let mut user_model = UserModel::from_model(model);
|
||||
let mut state = ModelState::new(user_model.get_worksheets_properties().len());
|
||||
// let mut selected_sheet = 0;
|
||||
// let mut selected_row_index = 1;
|
||||
// let mut selected_column_index = 1;
|
||||
let mut minimum_row_index = 1;
|
||||
let mut minimum_column_index = 1;
|
||||
let sheet_list_width = 20;
|
||||
let column_width: u16 = 11;
|
||||
let mut cursor_mode = CursorMode::Navigate;
|
||||
let mut input_formula = Input::default();
|
||||
|
||||
let mut input_file_name: Input = file_name.into();
|
||||
|
||||
let mut popup_open = false;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let tick_rate = Duration::from_millis(200);
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
if event::poll(timeout).expect("poll works") {
|
||||
if let CEvent::Key(key) = event::read().expect("can read events") {
|
||||
tx.send(Event::Input(key)).expect("can send events");
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate && tx.send(Event::Tick).is_ok() {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.clear()?;
|
||||
|
||||
let header_style = Style::default().fg(Color::Yellow).bg(Color::White);
|
||||
let selected_header_style = Style::default().bg(Color::Yellow).fg(Color::White);
|
||||
|
||||
let selected_cell_style = Style::default().fg(Color::Yellow).bg(Color::LightCyan);
|
||||
|
||||
let background_style = Style::default().bg(Color::Black);
|
||||
let selected_sheet_style = Style::default().bg(Color::White).fg(Color::LightMagenta);
|
||||
let non_selected_sheet_style = Style::default().fg(Color::White);
|
||||
let mut sheet_properties = user_model.get_worksheets_properties();
|
||||
loop {
|
||||
terminal.draw(|rect| {
|
||||
let size = rect.size();
|
||||
|
||||
let global_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(sheet_list_width), Constraint::Min(3)].as_ref())
|
||||
.split(size);
|
||||
|
||||
// Sheet list to the left
|
||||
let sheets = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.title("Sheets")
|
||||
.border_type(BorderType::Plain)
|
||||
.style(background_style);
|
||||
let mut rows = vec![];
|
||||
(0..sheet_properties.len()).for_each(|sheet_index| {
|
||||
let sheet_name = &sheet_properties[sheet_index].name;
|
||||
let style = if sheet_index == state.get_selected_sheet() {
|
||||
selected_sheet_style
|
||||
} else {
|
||||
non_selected_sheet_style
|
||||
};
|
||||
rows.push(Row::new(vec![Cell::from(sheet_name.clone()).style(style)]));
|
||||
});
|
||||
let widths = &[Constraint::Length(100)];
|
||||
let sheet_list = Table::new(rows, widths).block(sheets).column_spacing(0);
|
||||
|
||||
rect.render_widget(sheet_list, global_chunks[0]);
|
||||
|
||||
// The spreadsheet is the formula bar at the top and the sheet data
|
||||
let spreadsheet_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(2)].as_ref())
|
||||
.split(global_chunks[1]);
|
||||
|
||||
let spreadsheet_width = size.width - sheet_list_width;
|
||||
let spreadsheet_heigh = size.height - 1;
|
||||
let row_count = spreadsheet_heigh - 1;
|
||||
|
||||
let first_row_width: u16 = 3;
|
||||
let column_count =
|
||||
f64::ceil(((spreadsheet_width - first_row_width) as f64) / (column_width as f64))
|
||||
as i32;
|
||||
let mut rows = vec![];
|
||||
// The first row in the column headers
|
||||
let mut row = Vec::new();
|
||||
// The first cell in that row is the top left square of the spreadsheet
|
||||
row.push(Cell::from(""));
|
||||
let mut maximum_column_index = minimum_column_index + column_count - 1;
|
||||
let mut maximum_row_index = minimum_row_index + row_count - 1;
|
||||
|
||||
// We want to make sure the selected cell is visible.
|
||||
if selected_column_index > maximum_column_index {
|
||||
maximum_column_index = selected_column_index;
|
||||
minimum_column_index = maximum_column_index - column_count + 1;
|
||||
} else if selected_column_index < minimum_column_index {
|
||||
minimum_column_index = selected_column_index;
|
||||
maximum_column_index = minimum_column_index + column_count - 1;
|
||||
}
|
||||
if selected_row_index >= maximum_row_index {
|
||||
maximum_row_index = selected_row_index;
|
||||
minimum_row_index = maximum_row_index - row_count + 1;
|
||||
} else if selected_row_index < minimum_row_index {
|
||||
minimum_row_index = selected_row_index;
|
||||
maximum_row_index = minimum_row_index + row_count - 1;
|
||||
}
|
||||
for column_index in minimum_column_index..=maximum_column_index {
|
||||
let column_str = number_to_column(column_index);
|
||||
let style = if column_index == selected_column_index {
|
||||
selected_header_style
|
||||
} else {
|
||||
header_style
|
||||
};
|
||||
row.push(Cell::from(format!(" {}", column_str.unwrap())).style(style));
|
||||
}
|
||||
rows.push(Row::new(row));
|
||||
for row_index in minimum_row_index..=maximum_row_index {
|
||||
let mut row = Vec::new();
|
||||
let style = if row_index == selected_row_index {
|
||||
selected_header_style
|
||||
} else {
|
||||
header_style
|
||||
};
|
||||
row.push(Cell::from(format!("{}", row_index)).style(style));
|
||||
for column_index in minimum_column_index..=maximum_column_index {
|
||||
let value = user_model
|
||||
.get_formatted_cell_value(
|
||||
selected_sheet as u32,
|
||||
row_index as i32,
|
||||
column_index,
|
||||
)
|
||||
.unwrap();
|
||||
// let cell_style = user_model
|
||||
// .get_cell_style(selected_sheet as u32, row_index as i32, column_index)
|
||||
// .unwrap();
|
||||
let style = if selected_row_index == row_index
|
||||
&& selected_column_index == column_index
|
||||
{
|
||||
selected_cell_style
|
||||
} else {
|
||||
// let bg_color = match cell_style.fill.fg_color {
|
||||
// Some(s) => Color::from_str(&s).unwrap(),
|
||||
// None => Color::White,
|
||||
// };
|
||||
// let fg_color = match cell_style.font.color {
|
||||
// Some(s) => Color::from_str(&s).unwrap(),
|
||||
// None => Color::Black,
|
||||
// };
|
||||
let bg_color = Color::White;
|
||||
let fg_color = Color::Black;
|
||||
Style::default().fg(fg_color).bg(bg_color)
|
||||
};
|
||||
row.push(Cell::from(value.to_string()).style(style));
|
||||
}
|
||||
rows.push(Row::new(row));
|
||||
}
|
||||
let mut widths = Vec::new();
|
||||
widths.push(Constraint::Length(first_row_width));
|
||||
for _ in 0..column_count {
|
||||
widths.push(Constraint::Length(column_width));
|
||||
}
|
||||
let spreadsheet = Table::new(rows, widths)
|
||||
.block(Block::default().style(Style::default().bg(Color::Black)))
|
||||
.column_spacing(0);
|
||||
|
||||
let text = if cursor_mode != CursorMode::Input {
|
||||
user_model
|
||||
.get_cell_content(
|
||||
selected_sheet as u32,
|
||||
selected_row_index as i32,
|
||||
selected_column_index,
|
||||
)
|
||||
.unwrap()
|
||||
} else {
|
||||
input_formula.value().to_string()
|
||||
};
|
||||
let cell_address_text = format!(
|
||||
"{}{}: ",
|
||||
number_to_column(selected_column_index).unwrap(),
|
||||
selected_row_index,
|
||||
);
|
||||
let formula_bar_text = format!("{}{}", cell_address_text, text,);
|
||||
let formula_bar = Paragraph::new(vec![Line::from(vec![Span::raw(formula_bar_text)])]);
|
||||
rect.render_widget(formula_bar.block(Block::default()), spreadsheet_chunks[0]);
|
||||
rect.render_widget(spreadsheet, spreadsheet_chunks[1]);
|
||||
if cursor_mode == CursorMode::Input {
|
||||
let area = spreadsheet_chunks[0];
|
||||
rect.set_cursor(
|
||||
area.x
|
||||
+ (input_formula.visual_cursor() as u16)
|
||||
+ cell_address_text.len() as u16,
|
||||
area.y,
|
||||
)
|
||||
}
|
||||
|
||||
if popup_open {
|
||||
let area = centered_rect(60, 20, size);
|
||||
rect.render_widget(Clear, area);
|
||||
let input_text = input_file_name.value();
|
||||
let text = vec![
|
||||
Line::from(vec![input_text.fg(Color::Yellow)]),
|
||||
"".into(),
|
||||
Line::from(vec![
|
||||
"ESC".green(),
|
||||
" to abort. ".into(),
|
||||
"END".green(),
|
||||
" to quit without saving. ".into(),
|
||||
"Enter".green(),
|
||||
" to save and quit".into(),
|
||||
]),
|
||||
];
|
||||
rect.render_widget(
|
||||
Paragraph::new(text).block(Block::bordered().title("Save as")),
|
||||
area,
|
||||
);
|
||||
rect.set_cursor(
|
||||
// Put cursor past the end of the input text
|
||||
area.x + (input_file_name.visual_cursor() as u16) + 1,
|
||||
// Move one line own, from the border to the input line
|
||||
area.y + 1,
|
||||
)
|
||||
}
|
||||
})?;
|
||||
|
||||
match cursor_mode {
|
||||
CursorMode::Popup => {
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => match event.code {
|
||||
KeyCode::End => {
|
||||
terminal.clear()?;
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
break;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
terminal.clear()?;
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
let _ = save_to_xlsx(&user_model.model, input_file_name.value());
|
||||
break;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
popup_open = false;
|
||||
cursor_mode = CursorMode::Navigate;
|
||||
}
|
||||
_ => {
|
||||
input_file_name.handle_event(&CEvent::Key(event));
|
||||
}
|
||||
},
|
||||
Event::Tick => {}
|
||||
}
|
||||
}
|
||||
CursorMode::Navigate => {
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => match event.code {
|
||||
KeyCode::Char('q') => {
|
||||
popup_open = true;
|
||||
cursor_mode = CursorMode::Popup;
|
||||
}
|
||||
KeyCode::Down => {
|
||||
selected_row_index += 1;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
if selected_row_index > 1 {
|
||||
selected_row_index -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
selected_column_index += 1;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
if selected_column_index > 1 {
|
||||
selected_column_index -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
selected_row_index += 10;
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
if selected_row_index > 10 {
|
||||
selected_row_index -= 10;
|
||||
} else {
|
||||
selected_row_index = 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('s') => {
|
||||
selected_sheet += 1;
|
||||
if selected_sheet >= sheet_properties.len() {
|
||||
selected_sheet = 0;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('a') => {
|
||||
selected_sheet = selected_sheet.saturating_sub(1);
|
||||
}
|
||||
KeyCode::Char('u') => user_model.undo().unwrap(),
|
||||
KeyCode::Char('U') => user_model.redo().unwrap(),
|
||||
KeyCode::Char('c') => user_model
|
||||
.insert_column(selected_sheet as u32, selected_column_index as i32)
|
||||
.unwrap(),
|
||||
KeyCode::Char('C') => user_model
|
||||
.delete_column(selected_sheet as u32, selected_column_index as i32)
|
||||
.unwrap(),
|
||||
KeyCode::Char('r') => user_model
|
||||
.insert_row(selected_sheet as u32, selected_row_index as i32)
|
||||
.unwrap(),
|
||||
KeyCode::Char('R') => user_model
|
||||
.delete_row(selected_sheet as u32, selected_row_index as i32)
|
||||
.unwrap(),
|
||||
KeyCode::Char('e') => {
|
||||
cursor_mode = CursorMode::Input;
|
||||
let input_str = user_model
|
||||
.get_cell_content(
|
||||
selected_sheet as u32,
|
||||
selected_row_index as i32,
|
||||
selected_column_index,
|
||||
)
|
||||
.unwrap();
|
||||
// .unwrap_or_default();
|
||||
input_formula = input_formula.with_value(input_str);
|
||||
}
|
||||
KeyCode::Char('+') => {
|
||||
user_model.new_sheet();
|
||||
sheet_properties = user_model.get_worksheets_properties();
|
||||
}
|
||||
_ => {
|
||||
// println!("{:?}", event);
|
||||
}
|
||||
},
|
||||
Event::Tick => {}
|
||||
}
|
||||
}
|
||||
CursorMode::Input => match rx.recv()? {
|
||||
Event::Input(event) => match event.code {
|
||||
KeyCode::Enter => {
|
||||
cursor_mode = CursorMode::Navigate;
|
||||
let value = input_formula.value().to_string();
|
||||
let sheet = selected_sheet as i32;
|
||||
let row = selected_row_index as i32;
|
||||
let column = selected_column_index;
|
||||
user_model
|
||||
.set_user_input(sheet as u32, row, column, &value)
|
||||
.unwrap();
|
||||
user_model.evaluate();
|
||||
}
|
||||
_ => {
|
||||
input_formula.handle_event(&CEvent::Key(event));
|
||||
}
|
||||
},
|
||||
Event::Tick => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::vertical([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(r);
|
||||
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
//! Converts an xlsx file into the binary IronCalc format
|
||||
//! Tests an Excel xlsx file.
|
||||
//! Returns a list of differences in json format.
|
||||
//! Saves an IronCalc version
|
||||
//! This is primary for QA internal testing and will be superseded by an official
|
||||
//! IronCalc CLI.
|
||||
//!
|
||||
//! Usage: xlsx_2_icalc file.xlsx
|
||||
//! Usage: test file.xlsx
|
||||
|
||||
use std::path;
|
||||
|
||||
@@ -11,6 +15,7 @@ fn main() {
|
||||
if args.len() != 2 {
|
||||
panic!("Usage: {} <file.xlsx>", args[0]);
|
||||
}
|
||||
// first test the file
|
||||
let file_name = &args[1];
|
||||
|
||||
let file_path = path::Path::new(file_name);
|
||||
|
||||
@@ -68,6 +68,10 @@ pub fn save_to_xlsx(model: &Model, file_name: &str) -> Result<(), XlsxError> {
|
||||
|
||||
pub fn save_xlsx_to_writer<W: Write + Seek>(model: &Model, writer: W) -> Result<W, XlsxError> {
|
||||
let workbook = &model.workbook;
|
||||
let selected_sheet = match workbook.views.get(&0) {
|
||||
Some(view) => view.sheet,
|
||||
_ => 0,
|
||||
};
|
||||
let mut zip = zip::ZipWriter::new(writer);
|
||||
|
||||
let options = zip::write::FileOptions::default();
|
||||
@@ -94,7 +98,7 @@ pub fn save_xlsx_to_writer<W: Write + Seek>(model: &Model, writer: W) -> Result<
|
||||
zip.start_file("xl/styles.xml", options)?;
|
||||
zip.write_all(styles::get_styles_xml(workbook).as_bytes())?;
|
||||
zip.start_file("xl/workbook.xml", options)?;
|
||||
zip.write_all(workbook::get_workbook_xml(workbook).as_bytes())?;
|
||||
zip.write_all(workbook::get_workbook_xml(workbook, selected_sheet).as_bytes())?;
|
||||
|
||||
zip.add_directory("xl/_rels", options)?;
|
||||
zip.start_file("xl/_rels/workbook.xml.rels", options)?;
|
||||
@@ -108,17 +112,19 @@ pub fn save_xlsx_to_writer<W: Write + Seek>(model: &Model, writer: W) -> Result<
|
||||
.workbook
|
||||
.worksheet(sheet_index as u32)
|
||||
.unwrap()
|
||||
.get_dimension();
|
||||
.dimension();
|
||||
let column_min_str = number_to_column(dimension.min_column).unwrap();
|
||||
let column_max_str = number_to_column(dimension.max_column).unwrap();
|
||||
let min_row = dimension.min_row;
|
||||
let max_row = dimension.max_row;
|
||||
let sheet_dimension_str = &format!("{column_min_str}{min_row}:{column_max_str}{max_row}");
|
||||
let is_sheet_selected = selected_sheet as usize == sheet_index;
|
||||
zip.write_all(
|
||||
worksheets::get_worksheet_xml(
|
||||
worksheet,
|
||||
&model.parsed_formulas[sheet_index],
|
||||
sheet_dimension_str,
|
||||
is_sheet_selected,
|
||||
)
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
@@ -34,7 +34,7 @@ use ironcalc_base::types::{SheetState, Workbook};
|
||||
use super::escape::escape_xml;
|
||||
use super::xml_constants::XML_DECLARATION;
|
||||
|
||||
pub(crate) fn get_workbook_xml(workbook: &Workbook) -> String {
|
||||
pub(crate) fn get_workbook_xml(workbook: &Workbook, selected_sheet: u32) -> String {
|
||||
// sheets
|
||||
// <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
|
||||
let mut sheets_str: Vec<String> = vec![];
|
||||
@@ -80,6 +80,9 @@ pub(crate) fn get_workbook_xml(workbook: &Workbook) -> String {
|
||||
let defined_names = defined_names_str.join("");
|
||||
format!("{XML_DECLARATION}\n\
|
||||
<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\
|
||||
<bookViews>
|
||||
<workbookView activeTab=\"{selected_sheet}\"/>\
|
||||
</bookViews>
|
||||
<sheets>\
|
||||
{sheets}\
|
||||
</sheets>\
|
||||
|
||||
@@ -55,6 +55,7 @@ pub(crate) fn get_worksheet_xml(
|
||||
worksheet: &Worksheet,
|
||||
parsed_formulas: &[Node],
|
||||
dimension: &str,
|
||||
is_sheet_selected: bool,
|
||||
) -> String {
|
||||
let mut sheet_data_str: Vec<String> = vec![];
|
||||
let mut cols_str: Vec<String> = vec![];
|
||||
@@ -247,6 +248,38 @@ pub(crate) fn get_worksheet_xml(
|
||||
format!("<cols>{cols}</cols>")
|
||||
};
|
||||
|
||||
let tab_selected = if is_sheet_selected {
|
||||
" tabSelected=\"1\""
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let show_grid_lines = if !worksheet.show_grid_lines {
|
||||
" showGridLines=\"0\""
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let mut active_cell = "A1".to_string();
|
||||
let mut sqref = "A1".to_string();
|
||||
|
||||
let views = &worksheet.views;
|
||||
if let Some(view) = views.get(&0) {
|
||||
let range = view.range;
|
||||
let row = view.row;
|
||||
let column = view.column;
|
||||
let column_name = number_to_column(column).unwrap_or("A".to_string());
|
||||
active_cell = format!("{column_name}{row}");
|
||||
|
||||
let column_start = number_to_column(range[1]).unwrap_or("A".to_string());
|
||||
let column_end = number_to_column(range[3]).unwrap_or("A".to_string());
|
||||
if range[0] == range[2] && range[1] == range[3] {
|
||||
sqref = format!("{column_start}{}", range[0]);
|
||||
} else {
|
||||
sqref = format!("{}{}:{}{}", column_start, range[0], column_end, range[2]);
|
||||
}
|
||||
}
|
||||
|
||||
format!(
|
||||
"{XML_DECLARATION}
|
||||
<worksheet \
|
||||
@@ -254,8 +287,8 @@ xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" \
|
||||
xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\
|
||||
<dimension ref=\"{dimension}\"/>\
|
||||
<sheetViews>\
|
||||
<sheetView workbookViewId=\"0\">\
|
||||
<selection activeCell=\"A1\" sqref=\"A1\"/>\
|
||||
<sheetView workbookViewId=\"0\"{show_grid_lines}{tab_selected}>\
|
||||
<selection activeCell=\"{active_cell}\" sqref=\"{sqref}\"/>\
|
||||
</sheetView>\
|
||||
</sheetViews>\
|
||||
{cols}\
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::{
|
||||
use roxmltree::Node;
|
||||
|
||||
use ironcalc_base::{
|
||||
types::{Metadata, Workbook, WorkbookSettings},
|
||||
types::{Metadata, Workbook, WorkbookSettings, WorkbookView},
|
||||
Model,
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ fn load_xlsx_from_reader<R: Read + std::io::Seek>(
|
||||
let workbook = load_workbook(&mut archive)?;
|
||||
let rels = load_relationships(&mut archive)?;
|
||||
let mut tables = HashMap::new();
|
||||
let worksheets = load_sheets(
|
||||
let (worksheets, selected_sheet) = load_sheets(
|
||||
&mut archive,
|
||||
&rels,
|
||||
&workbook,
|
||||
@@ -88,6 +88,13 @@ fn load_xlsx_from_reader<R: Read + std::io::Seek>(
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut views = HashMap::new();
|
||||
views.insert(
|
||||
0,
|
||||
WorkbookView {
|
||||
sheet: selected_sheet,
|
||||
},
|
||||
);
|
||||
Ok(Workbook {
|
||||
shared_strings,
|
||||
defined_names: workbook.defined_names,
|
||||
@@ -100,6 +107,7 @@ fn load_xlsx_from_reader<R: Read + std::io::Seek>(
|
||||
},
|
||||
metadata,
|
||||
tables,
|
||||
views,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ use ironcalc_base::{
|
||||
utils::{column_to_number, parse_reference_a1},
|
||||
},
|
||||
types::{
|
||||
Cell, Col, Comment, DefinedName, Row, Selection, SheetData, SheetState, Table, Worksheet,
|
||||
Cell, Col, Comment, DefinedName, Row, SheetData, SheetState, Table, Worksheet,
|
||||
WorksheetView,
|
||||
},
|
||||
};
|
||||
use roxmltree::Node;
|
||||
@@ -543,6 +544,7 @@ struct SheetView {
|
||||
frozen_columns: i32,
|
||||
frozen_rows: i32,
|
||||
range: [i32; 4],
|
||||
show_grid_lines: bool,
|
||||
}
|
||||
|
||||
impl Default for SheetView {
|
||||
@@ -554,6 +556,7 @@ impl Default for SheetView {
|
||||
frozen_rows: 0,
|
||||
frozen_columns: 0,
|
||||
range: [1, 1, 1, 1],
|
||||
show_grid_lines: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -607,6 +610,7 @@ fn get_sheet_view(ws: Node) -> SheetView {
|
||||
|
||||
let sheet_view = sheet_view[0];
|
||||
let is_selected = sheet_view.attribute("tabSelected").unwrap_or("0") == "1";
|
||||
let show_grid_lines = sheet_view.attribute("showGridLines").unwrap_or("1") == "1";
|
||||
|
||||
let pane = sheet_view
|
||||
.children()
|
||||
@@ -653,6 +657,7 @@ fn get_sheet_view(ws: Node) -> SheetView {
|
||||
selected_row,
|
||||
selected_column,
|
||||
is_selected,
|
||||
show_grid_lines,
|
||||
range: [row1, column1, row2, column2],
|
||||
}
|
||||
} else {
|
||||
@@ -674,7 +679,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
|
||||
worksheets: &[String],
|
||||
tables: &HashMap<String, Table>,
|
||||
shared_strings: &mut Vec<String>,
|
||||
) -> Result<Worksheet, XlsxError> {
|
||||
) -> Result<(Worksheet, bool), XlsxError> {
|
||||
let sheet_name = &settings.name;
|
||||
let sheet_id = settings.id;
|
||||
let state = &settings.state;
|
||||
@@ -952,27 +957,38 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
|
||||
// pageSetup
|
||||
// <pageSetup orientation="portrait" r:id="rId1"/>
|
||||
|
||||
Ok(Worksheet {
|
||||
dimension,
|
||||
cols,
|
||||
rows,
|
||||
shared_formulas,
|
||||
sheet_data,
|
||||
name: sheet_name.to_string(),
|
||||
sheet_id,
|
||||
state: state.to_owned(),
|
||||
color,
|
||||
merge_cells,
|
||||
comments: settings.comments,
|
||||
frozen_rows: sheet_view.frozen_rows,
|
||||
frozen_columns: sheet_view.frozen_columns,
|
||||
selection: Selection {
|
||||
is_selected: sheet_view.is_selected,
|
||||
let mut views = HashMap::new();
|
||||
views.insert(
|
||||
0,
|
||||
WorksheetView {
|
||||
row: sheet_view.selected_row,
|
||||
column: sheet_view.selected_column,
|
||||
range: sheet_view.range,
|
||||
top_row: 1,
|
||||
left_column: 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
Ok((
|
||||
Worksheet {
|
||||
dimension,
|
||||
cols,
|
||||
rows,
|
||||
shared_formulas,
|
||||
sheet_data,
|
||||
name: sheet_name.to_string(),
|
||||
sheet_id,
|
||||
state: state.to_owned(),
|
||||
color,
|
||||
merge_cells,
|
||||
comments: settings.comments,
|
||||
frozen_rows: sheet_view.frozen_rows,
|
||||
frozen_columns: sheet_view.frozen_columns,
|
||||
show_grid_lines: sheet_view.show_grid_lines,
|
||||
views,
|
||||
},
|
||||
sheet_view.is_selected,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn load_sheets<R: Read + std::io::Seek>(
|
||||
@@ -981,7 +997,7 @@ pub(super) fn load_sheets<R: Read + std::io::Seek>(
|
||||
workbook: &WorkbookXML,
|
||||
tables: &mut HashMap<String, Table>,
|
||||
shared_strings: &mut Vec<String>,
|
||||
) -> Result<Vec<Worksheet>, XlsxError> {
|
||||
) -> Result<(Vec<Worksheet>, u32), XlsxError> {
|
||||
// load comments and tables
|
||||
let mut comments = HashMap::new();
|
||||
for sheet in &workbook.worksheets {
|
||||
@@ -1003,6 +1019,8 @@ pub(super) fn load_sheets<R: Read + std::io::Seek>(
|
||||
// load all sheets
|
||||
let worksheets: &Vec<String> = &workbook.worksheets.iter().map(|s| s.name.clone()).collect();
|
||||
let mut sheets = Vec::new();
|
||||
let mut selected_sheet = 0;
|
||||
let mut sheet_index = 0;
|
||||
for sheet in &workbook.worksheets {
|
||||
let sheet_name = &sheet.name;
|
||||
let rel_id = &sheet.id;
|
||||
@@ -1021,15 +1039,14 @@ pub(super) fn load_sheets<R: Read + std::io::Seek>(
|
||||
state: state.clone(),
|
||||
comments: comments.get(rel_id).expect("").to_vec(),
|
||||
};
|
||||
sheets.push(load_sheet(
|
||||
archive,
|
||||
&path,
|
||||
settings,
|
||||
worksheets,
|
||||
tables,
|
||||
shared_strings,
|
||||
)?);
|
||||
let (s, is_selected) =
|
||||
load_sheet(archive, &path, settings, worksheets, tables, shared_strings)?;
|
||||
if is_selected {
|
||||
selected_sheet = sheet_index;
|
||||
}
|
||||
sheets.push(s);
|
||||
sheet_index += 1;
|
||||
}
|
||||
}
|
||||
Ok(sheets)
|
||||
Ok((sheets, selected_sheet))
|
||||
}
|
||||
|
||||
BIN
xlsx/tests/NoGrid.xlsx
Normal file
BIN
xlsx/tests/NoGrid.xlsx
Normal file
Binary file not shown.
Binary file not shown.
@@ -16,39 +16,84 @@ fn test_example() {
|
||||
let workbook = model.workbook;
|
||||
let ws = &workbook.worksheets;
|
||||
let expected_names = vec![
|
||||
("Sheet1".to_string(), false),
|
||||
("Second".to_string(), false),
|
||||
("Sheet4".to_string(), false),
|
||||
("shared".to_string(), false),
|
||||
("Table".to_string(), false),
|
||||
("Sheet2".to_string(), false),
|
||||
("Created fourth".to_string(), false),
|
||||
("Frozen".to_string(), true),
|
||||
("Split".to_string(), false),
|
||||
("Hidden".to_string(), false),
|
||||
"Sheet1".to_string(),
|
||||
"Second".to_string(),
|
||||
"Sheet4".to_string(),
|
||||
"shared".to_string(),
|
||||
"Table".to_string(),
|
||||
"Sheet2".to_string(),
|
||||
"Created fourth".to_string(),
|
||||
"Frozen".to_string(),
|
||||
"Split".to_string(),
|
||||
"Hidden".to_string(),
|
||||
];
|
||||
let names: Vec<(String, bool)> = ws
|
||||
.iter()
|
||||
.map(|s| (s.name.clone(), s.selection.is_selected))
|
||||
.collect();
|
||||
let names: Vec<String> = ws.iter().map(|s| s.name.clone()).collect();
|
||||
|
||||
// One is not not imported and one is hidden
|
||||
assert_eq!(expected_names, names);
|
||||
|
||||
assert_eq!(workbook.views[&0].sheet, 7);
|
||||
|
||||
// Test selection:
|
||||
// First sheet (Sheet1)
|
||||
// E13 and E13:N20
|
||||
assert_eq!(ws[0].frozen_rows, 0);
|
||||
assert_eq!(ws[0].frozen_columns, 0);
|
||||
assert_eq!(ws[0].selection.row, 13);
|
||||
assert_eq!(ws[0].selection.column, 5);
|
||||
assert_eq!(ws[0].selection.range, [13, 5, 20, 14]);
|
||||
assert_eq!(ws[0].views[&0].row, 13);
|
||||
assert_eq!(ws[0].views[&0].column, 5);
|
||||
assert_eq!(ws[0].views[&0].range, [13, 5, 20, 14]);
|
||||
|
||||
let model2 = load_from_icalc("tests/example.ic").unwrap();
|
||||
let s = bitcode::encode(&model2.workbook);
|
||||
assert_eq!(workbook, model2.workbook, "{:?}", s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_grid() {
|
||||
let model = load_from_xlsx("tests/NoGrid.xlsx", "en", "UTC").unwrap();
|
||||
{
|
||||
let workbook = &model.workbook;
|
||||
let ws = &workbook.worksheets;
|
||||
|
||||
// NoGrid does not show grid lines
|
||||
let no_grid_sheet = &ws[0];
|
||||
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
|
||||
assert!(!no_grid_sheet.show_grid_lines);
|
||||
|
||||
let sheet2 = &ws[1];
|
||||
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
|
||||
assert!(sheet2.show_grid_lines);
|
||||
|
||||
let no_grid_no_headers_sheet = &ws[2];
|
||||
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
|
||||
// There is also no headers
|
||||
assert!(!no_grid_no_headers_sheet.show_grid_lines);
|
||||
}
|
||||
{
|
||||
// save it and check again
|
||||
let temp_file_name = "temp_file_no_grid.xlsx";
|
||||
save_to_xlsx(&model, temp_file_name).unwrap();
|
||||
let model = load_from_xlsx(temp_file_name, "en", "UTC").unwrap();
|
||||
let workbook = &model.workbook;
|
||||
let ws = &workbook.worksheets;
|
||||
|
||||
// NoGrid does not show grid lines
|
||||
let no_grid_sheet = &ws[0];
|
||||
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
|
||||
assert!(!no_grid_sheet.show_grid_lines);
|
||||
|
||||
let sheet2 = &ws[1];
|
||||
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
|
||||
assert!(sheet2.show_grid_lines);
|
||||
|
||||
let no_grid_no_headers_sheet = &ws[2];
|
||||
assert_eq!(no_grid_sheet.name, "NoGrid".to_string());
|
||||
// There is also no headers
|
||||
assert!(!no_grid_no_headers_sheet.show_grid_lines);
|
||||
fs::remove_file(temp_file_name).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_to_xlsx() {
|
||||
let mut model = load_from_xlsx("tests/example.xlsx", "en", "UTC").unwrap();
|
||||
@@ -62,6 +107,20 @@ fn test_save_to_xlsx() {
|
||||
assert_eq!(metadata.application, "IronCalc Sheets");
|
||||
// FIXME: This will need to be updated once we fix versioning
|
||||
assert_eq!(metadata.app_version, "10.0000");
|
||||
|
||||
let workbook = model.workbook;
|
||||
let ws = &workbook.worksheets;
|
||||
|
||||
assert_eq!(workbook.views[&0].sheet, 7);
|
||||
|
||||
// Test selection:
|
||||
// First sheet (Sheet1)
|
||||
// E13 and E13:N20
|
||||
assert_eq!(ws[0].frozen_rows, 0);
|
||||
assert_eq!(ws[0].frozen_columns, 0);
|
||||
assert_eq!(ws[0].views[&0].row, 13);
|
||||
assert_eq!(ws[0].views[&0].column, 5);
|
||||
assert_eq!(ws[0].views[&0].range, [13, 5, 20, 14]);
|
||||
// TODO: can we show it is the 'same' model?
|
||||
fs::remove_file(temp_file_name).unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user